티스토리 뷰

Backend

Java Logger의 또다른 식구, tinylog

지마켓 박규민 2023. 3. 2. 09:36

안녕하세요. Advanced Technology 팀 박규민입니다.

 

오늘은 Java, Kotlin, Scala와 같은 JVM 언어에서 사용할 수 있는 오픈소스 경량 로깅 프레임워크를 소개드리려고 합니다.

 

Log4j, Logback에 비해 상대적으로 빠른 로깅 속도와 간단한 구성, 다양한 출력 옵션 등의 메리트가 있는 tinylog에 대해 알아보겠습니다.

 

 

What is tinylog?


tinylog는 Java 플랫폼에서 동작하는 로깅 프레임워크입니다.

 

JVM, GraalVM(Oracle에서 만든 OpenJDK 기반 JVM), Android에서 동작이 가능합니다.

 

 

보통은 Slf4j라는 Logging API와 바인딩하여 Log4j, Logback과 같은 로깅 프레임워크를 많이 쓰는데요.

 

tinylog도 Slf4j에 바인딩할 수 있는 로깅 프레임워크 중 하나입니다.

 

 

Version


작성일 기준으로 2.6.0이 최신 버전이며, 1.0.0 버전은 2015년 3월 30일, 2.0.0 버전은 2019년 8월 20일에 공식 릴리즈되었습니다.

 

2001년에 출시된 Log4j, 2006년에 출시된 Logback에 비해 상대적으로 최근에 나온 것이라고 볼 수 있겠습니다.

 

 

 

Features


1. 경량

이름에서 알 수 있듯이, tinylog 자체 라이브러리의 Jar 파일 용량이 매우 가볍습니다.

 

tinylog-api, tinylog-impl 두 개 다 합쳐서 183KB에 불과합니다.

다른 로깅 프레임워크의 용량은?
logback classic : 259KB, logback core : 563KB
apache log4j-core : 1.8MB, apache log4j-api : 310KB

 

 

2. 호환성

tinylog는 Java 6 버전이상이면 동작하는 로깅 프레임워크입니다.

 

람다와 Lazy Logging 지원을 하며 JVM 기반의 여러 환경에서 사용할 수 있습니다.

 

 

3. 상용구 코드(Boilerplate Code) 방지

tinylog는 유틸리티 클래스로 static Logger가 구현되어 있어 로그문을 출력하려는 각 클래스에 대해서 Logger 인스턴스를 만들 필요가 없습니다.

 

Logback with Slf4j

import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Logger;

public class SampleClass {
  Logger logger = LoggerFactory.getLogger(SampleClass.class);

  public static void main(String[] args) {
    logger.info("hihi");
  }
}

Tinylog

import org.tinylog.Logger;

public class SampleClass {

  public static void main(String[] args) {
    Logger.info("hihi");
  }
}

 

 

4. 다양한 출력 옵션

tinylog에는 로그 항목들을 출력하는 Writer라는 객체가 있습니다. ( ≒ Appender)

 

이 Writer를 통해서 로그를 콘솔, DB 테이블, .log 파일, Json, Logcat으로 다양한 형식으로 출력할 수 있습니다.

 

현재 지원하는 옵션들 목록은 다음과 같습니다. (괄호는 설정값)

 

  • Console (console)
  • File (file)
  • Jdbc (jdbc)
  • Json (json)
  • Logcat (logcat)
  • Rolling File (rolling file)
  • Shared File (shared file)

 

 

5. 간단한 구성(Configuration)

tinylog는 .properties 파일을 통해 간단하게 구성할 수 있습니다.

level = info                                    // severity level
level@org.tinylog = debug                       // severity level by package or class

writer = file                                   // option
writer.file = application.log                   // log file name
writer.format = {class}.{method}() {message}    // 로그 출력 패턴

 

severity level은 다음과 같이 나눠져 있습니다. (Default는 TRACE 입니다.)

 

level을 지정하면 상위 level들이 포함됩니다. 만약에 INFO로 설정하였다면 INFO, WARN, ERROR 로그문만 출력하게 됩니다.

TRACE, DEBUG, INFO, WARN, ERROR (오른쪽으로 갈수록 상위 level)

추가로 OFF가 있는데, 설정할 시에 모든 level의 로그문들을 비활성화합니다.

 

 

그리고 2.1 버전부터 tinylog.properties를 test/dev profile로 나눠서 분리할 수 있습니다.

 

각 profile의 구성은 지정된 디렉토리에 위치해 있어야 합니다.

  • tinylog-test.properties : src/test/resources
  • tinylog-dev.properties : src/main/resources

 

적용 우선 순위는 일반 < test < dev 순입니다.

 

 

Custom Profile Configuration 적용

 

총 2가지 방법이 있습니다.

 

1. System Property

경로를 포함한 .properties 파일 이름을 시스템 프로퍼티로 -Dtinylog.configuration 키값으로 적용할 수 있습니다.

$ java -jar -Dtinylog.configuration=/path/tinylog.properties application.jar
 
# tinylog.properties 명은 optional로, 아무 이름이나 가능.
$ java -jar -Dtinylog.configuration=/path/abcdefg.properties application.jar
 
# tinylog. prefix로 추가적인 구성 설정도 가능
$ java -jar -Dtinylog.writer.level=debug application.jar

 

2. Programmatically Configure

 

유틸리티 클래스인 org.tinylog.configuration.Configuration을 이용하여 코드로 작성하여 설정하실 수도 있습니다.

 

import org.tinylog.configuration.Configuration;
import org.tinylog.Logger;

public class SampleClass {
  public static void main(String[] args) {
    Configuration.set("writer", "console");
    Configuration.set("writer.format", "{level}: {message}");

    Logger.info("hihi");
  }
}

 

 

JSON 또는 YAML 파일 형식 설정

 

원래 tinylog는 properties 파일 형식만 지원합니다.

 

하지만 2.3.0 버전부터 다른 파일 형식도 지원하기 위해 tinylog에서 제공해주는 org.tinylog.configuration.ConfigurationLoader를 implements하여 custom configuration loader를 직접 구현할 수도 있습니다.

 

구현한 다음에는 loader를 resources/META-INF/services/org.tinylog.configuration.ConfigurationLoader 파일에 loader 클래스의 풀 네임을 넣으면 런타임에 적용할 수 있습니다.

 

예를 들어 org.tinylog.example.configuration 패키지에서 YamlConfigurationLoader라는 이름으로 클래스를 만들었다면 다음과 같이 집어넣으시면 됩니다.

 

org.tinylog.example.configuration.YamlConfigurationLoader

 

JSON/YAML configuration loader는 이미 다른 개발자 분이 만들어 주신 예제가 있어 아래와 같이 활용하시면 됩니다.

 

 

그 외에 다른 구성 설정은 공식문서( https://tinylog.org/v2/configuration/#configuration )를 참조하시기 바랍니다.

 

 

 

6. Fast

로깅은 특히나 클래스 및 메서드 이름과 같은 Caller 정보의 출력이 성능에 큰 영향을 미칠 수 있는데요.

 

tinylog는 클래스 및 메소드 이름과 같은 Caller 정보와 함께 로그문을 출력할 때 다른 로깅 프레임워크보다 몇배 더 빠릅니다.

 

그리고 비활성화된 severity level의 로그문을 폐기했을 때, 로직이 없는 no-op empty 메소드의 호출하는 것과 동일합니다.

 

공식 홈페이지에서 게재된 벤치마크가 있는데, 파일 형식으로 로그문을 출력했을 때 초당 로깅 처리량을 표로 정리해봤습니다.

(https://tinylog.org/v2/benchmark/)

 

 

 

 

 

Spring Boot에서의 사용법


Pure

Gradle 기반의 Spring Boot 프로젝트를 생성하여 build.gradle에 다음과 같이 2개의 dependency들을 추가합니다.

 

implementation 'org.tinylog:tinylog-api:2.6.0'
implementation 'org.tinylog:tinylog-impl:2.6.0'

 

tinylog 로거를 통해 로그문들을 출력하는 LogController 클래스를 만들어서 http 테스트를 합니다.

 

import org.tinylog.Logger;
 
@RestController
@RequestMapping("/log")
public class LogController {
 
  // 온전한 평문 로그 출력
  @GetMapping("/plain")
  public ResponseEntity<String> logByPlainText() {
    Logger.trace("Hello World!");
    Logger.debug("Hello World!");
    Logger.info("Hello World!");
    Logger.warn("Hello World!");
    Logger.error("Hello World!");
 
    return ResponseEntity.ok("Hello World!");
  }
 
  ...
 
}

 

 

 

Slf4j 바인딩

로깅 프레임워크 인터페이스인 Slf4j와 바인딩이 가능합니다.

 

필요한 dependencies는 다음과 같습니다.

implementation 'org.slf4j:slf4j-api:1.7.36'
runtimeOnly 'org.tinylog:slf4j-tinylog:2.6.0'
implementation 'org.tinylog:tinylog-api:2.6.0'
implementation 'org.tinylog:tinylog-impl:2.6.0'

slf4j-tinylog는 1.6부터 모든 버전을 지원하는 SLF4J의 바인딩 라이브러리입니다.

 

slf4j-tinylog는 slf4j-api가 클래스 경로에도 존재해야 하지만, slf4j-tinylog 이외의 다른 바인딩은 없어야 합니다.

 

모든 로그문들은 org.slf4j.Logger에서 tinylog로 전달되고 tinylog 구현체에 의해 처리됩니다.

 

org.slf4j.Logger의 Marker는 tinylog의 tag에 매핑됩니다.

 

MDC는 tinylog에서의 Thread Context Value를 공유합니다.

 

 

 

다양한 로깅 방식

평문(Plain Text)

위 Pure에서의 LogController 클래스가 평문 로그문 출력의 예입니다.

 

 

중괄호를 이용한 인자 추가

"{}" 기호를 통해 인자를 추가하여 로그문에 출력시킬 수 있습니다.

import org.tinylog.Logger;
 
@RestController
public class LogController {
  
  // {}를 통한 인자가 포함된 로그 출력
  @GetMapping("/args")
  public ResponseEntity<String> logByTextWithArguments() {
    Logger.trace("{} + {}", 1, 2);
    Logger.debug("{} + {}", 1, 2);
    Logger.info("{} + {}", 1, 2);
    Logger.warn("{} + {}", 1, 2);
    Logger.error("{} + {}", 1, 2);
 
    // DecimalFormat 패턴 지원
    Logger.trace("방어율 : {0.00} ", (double) 10 / 6);
 
    // 숫자 조건문 패턴 지원
    Logger.debug("숫자는 {0#0입니다 | 1#1입니다 | 1<1보다 큽니다}", 0);
    Logger.info("숫자는 {0#0입니다 | 1#1입니다 | 1<1보다 큽니다}", 1);
    Logger.warn("숫자는 {0#0입니다 | 1#1입니다 | 1<1보다 큽니다}", 2);
 
    // 2.1부터 Single quote 안에 중괄호 출력이 가능(Escaping 이라함).
    // 단, tinylog.properties 에서 escaping.enabled = true로 설정해야 한다.
    // Logger.error("Curly brackets as placeholder {} or escaped '{}'", 11);
     
    return ResponseEntity.ok("Hello World!");
  }
}

 

객체 전달

tinylog에는 객체를 로깅하기 위한 자체 메소드가 있으며, 로그문이 실제로 출력되는 경우에만 toString() 메소드를 호출합니다.

import org.tinylog.Logger;
 
@RestController
public class LogController {

  // 객체를 통째로 전달하여 출력
  @GetMapping("/obj")
  public ResponseEntity<String> logByTextWithObjects() {
    Logger.trace(LocalDate.now());
    Logger.debug(LocalDate.now());
    Logger.info(LocalDate.now());
    Logger.warn(LocalDate.now());
    Logger.error(LocalDate.now());

    return ResponseEntity.ok("Hello World!");
  }
}

 

Exception 전달

Exception과 다른 Throwable들을 직접 로그문에 전달할 수 있습니다.

 

Optional하게 메시지도 추가할 수도 있습니다.

import org.tinylog.Logger;
 
@RestController
public class LogController {

  // 예외 출력
  @GetMapping("/ex")
  public ResponseEntity<String> logByTextWithEx() {
    Logger.trace(new RuntimeException());

    // 메시지 추가
    Logger.debug(new ArithmeticException(), "Cannot divide {} by {}", 1, 0);

    return ResponseEntity.ok("Hello World!");
  }
}

 

Lazy Logging

메시지나 인자를 로그문에 출력하려고 특별하게 가공하고 싶을 때, 람다 함수를 이용할 수도 있습니다.

 

import org.tinylog.Logger;
 
@RestController
public class LogController {

  // Lazy Logging
  @GetMapping("/lazy")
  public ResponseEntity<String> lazyLog() {
    Logger.trace(() -> compute());
    Logger.debug(this::compute);

    // {}로 표현 가능
    Logger.info("Expensive computation: {}", () -> compute());
    Logger.warn("Expensive computation: {}", this::compute);

    return ResponseEntity.ok("Hello World!");
  }

  private String compute() {
    return "Hello World!";
  }
}

 

 

Tags

tinylog는 Logger를 유틸리티 클래스로 쓰고 있어 Logger마다 이름을 붙일 수 없습니다.

 

이러한 이유로 로그문들을 카테고리화시키기 위해 tag 기능을 제공하고 있습니다.

 

그리고 아래와 같이 Tag는 패턴의 일부로 출력되거나 로그 항목을 다른 Writer에게 전달하는 데 사용될 수도 있습니다.

import org.tinylog.Logger;
 
@RestController
public class LogController {

  // tags
  @GetMapping("/tag")
  public ResponseEntity<String> logByTags() {
    Logger.tag("SYSTEM").trace("Hello World!");
    Logger.tag("SYSTEM").debug("Hello World!");
    Logger.tag("SYSTEM").info("Hello World!");
    Logger.tag("SYSTEM").warn("Hello World!");
    Logger.tag("SYSTEM").error("Hello World!");

    // tag를 광범위하게 사용하고 싶은 경우, TaggedLogger 인스턴스를 이용한다.
    TaggedLogger system = Logger.tag("SYSTEM");

    // Tinylog 2.4부터 정의된 모든 태그에 각 로그 항목을 발행하는 여러 태그로 로거를 만들 수도 있다.
    TaggedLogger system2 = Logger.tags("FOO", "BAR", "BAZ");

    // 일반적인 정적 로거와 똑같이 작동하는 태그가 지정되지 않은 로거 인스턴스를 얻는 것도 가능하다.
    TaggedLogger logger = Logger.tag(null);

    return ResponseEntity.ok("Hello World!");
  }
}

 

Configuration으로도 설정 가능합니다.

 

writer = console
writer.tag = SYSTEM

 

Context Values

Tinylog는 추가로 스레드 기반 컨텍스트를 가지고 있습니다.

 

컨텍스트 값은 패턴의 일부로 출력될 수 있고, 저장된 값은 값이 설정된 스레드와 자식 스레드에서만 볼 수 있습니다.

 

 

 

 

결론


현재는 Java 기반의 로깅 프레임워크를 Logback을 많이 사용하고 있습니다.

 

하지만 몇년 전에 Log4j 취약점이 발견되고 잇달아 Logback에서조차도 취약점이 발견되었는데요.

 

tinylog가 두 개의 로깅 프레임워크와는 독립된 소스 코드로 되어 있어 아직까지는 취약점이 발견되지 않았습니다.

 

하지만 취약점의 늪에서 벗어날 수 있다는 보장할 수는 없지만, 그래도 지금으로써 알려진 Log4j나 Logback 등 JVM 기반의 로깅 프레임워크들을 대체할 수 있는 오픈소스 JVM 로깅 프레임워크라는 점은 틀림없어 보인다고 생각합니다.

 

 

 

참고


 

 

 

 

 

댓글