logback 홈페이지의 매뉴얼을 읽으며 내용들을 정리한 글입니다.
Appender란?
Logback은 로그 이벤트를 write 하는 작업을 Appender에게 위임(delegate)합니다. Appender로 이용되기 위해서는 반드시 아래의 ch.qos.logback.core.Appender 인터페이스를 구현해야만 합니다.
package ch.qos.logback.core;
import ch.qos.logback.core.spi.ContextAware;
import ch.qos.logback.core.spi.FilterAttachable;
import ch.qos.logback.core.spi.LifeCycle;
public interface Appender<E> extends LifeCycle, ContextAware, FilterAttachable {
public String getName();
public void setName(String name);
void doAppend(E event);
}
doAppend() 메서드는 파라미터로 E 타입의 인스턴스만을 전달받습니다. 실제 E 인스턴스는 타입은 logback 모듈에 따라 달라집니다. logback-classic 모듈에서는 ILoggingEvent이며 logback-access 모듈에서는 AccessEvent입니다. 이는 출력기기에 맞게 로그 이벤트를 알맞은 포맷으로 맞춰야 하기 때문입니다.
AppenderBase
ch.qos.logback.core.AppenderBase 클래스는 Appender 인터페이스를 구현한 추상 클래스입니다. AppenderBase는 모든 appender가 사용할 기본 기능들을 제공합니다(name, activation status, layout and filters에 대한 getter setter). 그렇기에 AppenderBase는 logback에게 전달되는 모든 appender의 상위 클래스입니다. 비록, AppenderBase가 추상 클래스이긴 하지만 Appender 인터페이스의 doAppend() 메서드를 구현하고 있습니다. 실제 코드를 확인해봅시다.
public synchronized void doAppend(E eventObject) {
// prevent re-entry.
if (guard) {
return;
}
try {
guard = true;
if (!this.started) {
if (statusRepeatCount++ < ALLOWED_REPEATS) {
addStatus(new WarnStatus(
"Attempted to append to non started appender [" + name + "].",this));
}
return;
}
if (getFilterChainDecision(eventObject) == FilterReply.DENY) {
return;
}
// ok, we now invoke the derived class's implementation of append
this.append(eventObject);
} finally {
guard = false;
}
}
doAppend() 메서드가 synchronized인 것을 확인할 수 있습니다. 이로써 같은 appender에 다른 스레드들이 로깅하는 동작이 안전함을 확인할 수 있습니다. 만약 synchronized 한 동작이 필요하지 않다면 UnsynchronizedAppenderBase를 사용합니다.
doAppend 메서드는 처음 guard 필드가 true인지 확인합니다. guard는 doAppend() 메서드가 동작 중 인지를 나타내는 변수입니다. true 상태라면 다른 쓰레드가 doAppend()를 먼저 선점하여 동작중이기에 즉시 메서드를 빠져나옵니다. false라면 guard를 true로 수정합니다. guard 변수를 통한 이러한 작업들은 doAppend 메서드가 재귀적으로 호출되어 무한 루프와 stack overflow가 발생하는 일을 방지합니다.
이후 started에 대하여 체크합니다. Appender 인터페이스는 start, stop, isStarted 메서드를 가진 LifeCycle 인터페이스를 상속합니다. Configuration Framework인 Joran은 Appender의 모든 properties가 세팅되었다면, start() 메서드를 호출하여 appender가 활성화되도록 합니다. started는 Appender의 상태에 대한 변수이며 시작되지 못하거나 중지 상태라면, Appender가 아직 활성화되지 않았기 때문에 경고 메시지를 전달합니다. 경고 메시지는 Logback의 내부 상태 관리 시스템을 통해 보고되며 여러 번의 검사 후 과도한 경고 메시지 출력을 방지하기 위해 메서드를 빠져나오도록 합니다.
다음 if문은 appender에 부착된 filter를 확인합니다. filterChain의 decision에 따라 로그 이벤트는 무시되거나 수락될 수 있습니다. filterChain에 대한 decision이 없다면 기본적으로 로그 이벤트는 수락됩니다.
이후 doAppend() 메서드는 파행 클래스가 구현한 append() 메서드를 호출합니다. 이 부분에서 실제 로그 이벤트를 알맞은 장치에 append 하는 작업을 수행합니다.
일련의 작업을 마친 후 guard 상태를 false로 수정하고 다음 doAppend() 메서드가 이어서 호출되도록 만들어 줍니다.
Logback-core
Logback-core는 다른 logback 모듈의 기반이 되는 components들을 제공합니다. 일반적으로 logback-core의 구성에는 최소한의 사용자 정의만이 필요합니다. Logback-core에서 제공하는 다양한 Appender에 대하여 알아봅시다.
OutputStreamAppender
OutputStreamAppender는 java.io.OutputStream에 로그 이벤트를 append 합니다. 일반적으로 사용자들은 OutputStream을 쉽게 String으로 변환시킬 수 없기 때문에, 이 Appender를 직접적으로 사용하지 않습니다. 하지만 직접적으로 사용하게 될 ConsoleAppender와 FileAppender 등에서 구현해야할 기본적인 서비스들을 미리 준비해놓았기 때문에 알아둘 필요가 있습니다. 이를 직접 사용하기 위해서는 encoder와 immediateFlush 프로퍼티를 설정해주어야 합니다.
Property | Type | Description |
encoder | Encoder (ch.qos.logback.core.encoder.Encoder) | 로그 이벤트가 OutputStreamAppender에 기록되는 방식 |
immediateFlush | boolean (default: true) | 로그 이벤트 발생 이후, 즉시 outputStream을 flush하는지에 대한 여부. false일 경우 4배의 성능상의 이점이 있지만, 적절한 시간때에 appender가 close되지 못한다면 로그 이벤트의 유실이 발생할 수 있다. |
OutputStreamAppender는 이후 살펴볼 ConsoleAppender, FileAppender, RollingFileAppender의 상위 클래스입니다. 아래 클래스 다이어그램을 통해 Appender의 구성을 확인하시길 바랍니다.
ConsoleAppender
이름에서 알 수 있듯이 ConsoleAppender는 콘솔에 System.out 또는 System.err를 이용하여 로그 이벤트를 append합니다 (기본적으로 System.out을 사용합니다). ConsoleAppender는 사용자가 지정한 encoder를 통해 이벤트의 format을 지정합니다. System.out과 System.err는 모두 java.io.PrintStream 타입이고, 그렇기에 I/O 작업을 버퍼링 하는 OutputStreamWriter 내부에 wrapping 됩니다.
Property | Type | Description |
encoder | Encoder | OutputStreamAppender와 동일 |
target | String (default: "System.out") | 출력 타겟. 값은 "System.out" 또는 "System.err" 중 하나를 갖는다. |
withJansi | boolean (default: false) | console 출력에 대하여 ANSI color를 지원여부. Window의 경우 true이면 classpath에 org.fusesource.jansi:jansi:1.17 이 존재하여야 한다. Unix 기반 OS일 경우 기본으로 지원된다. |
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg %n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
</configuration>
FileAppender
FileAppender는 파일에 로그 이벤트를 append 합니다. 타깃 파일은 File 옵션을 통해 명시해줄 수 있습니다. 만약 명시한 파일이 이미 존재한다면 append 속성 값에 따라 마지막부터 내용이 추가되거나, 덮어씁니다.
property | Type | Description |
append | boolean (default: true) | true이면 존재하는 파일 뒤에 이어 붙인다. false일 경우 기존의 파일 내용이 덮어쓰여진다. |
encoder | Encoder | OutputStreamAppender와 동일 |
file | String | 출력할 파일의 이름. 파일이 존재하지 않으면 생성한다. MS일 경우 파일 경로에 대하여 '\'문자를 사용함에 유의해야한다 ('/' 혹은 '\\'로 대체) 경로 상 상위 폴더가 존재하지 않아도 생성된다. |
prudent | boolean (default: false) | true일 경우 타겟 파일이 다른 JVM에서 참조하고 있더라도 안전하게 write하도록 한다. preduent=true일 경우 자동으로 append=true로 세팅된다. prudent mode는 배타적 파일 락을 이용하기 때문에 write에 3배의 cost가 소요된다 (local hard disk에 append 시). prudent mode 사용에 있어서는 초당 요청 개수가 중요한 요인이 될 수 있다. 초당 20개 정도의 로그 요청의 경우 prudent mode 사용은 문제가 되지 않지만, 초당 100개의 요청이 올 경우 prudent mode를 최대한 사용하지 않아야한다. 타겟 파일이 local이 아닌 networked 파일일 경우 그 cost는 더욱 커진다. prudent mode의 성능은 network 스피트와 OS implementation에도 많은 의존성이 있기에 logback에서 제공하는 FileLockSimulator를 이용하여 자신에 환경에서 동작이 가능할지 테스트 하도록 하자. |
기본적으로 로그 이벤트는 outputStream에 즉시 flush 됩니다. 이런 방법은 appender가 close 되더라도 로그 이벤트가 유실되지 않도록 하는데 좋은 방법입니다. 하지만 로깅에 대한 성능을 더 높이고 싶다면 immediateFlush 속성을 false로 지정하여 줍시다 (OutputStreamAppender의 속성입니다).
<configuration>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>testFile.log</file>
<append>true</append>
<!-- set immediateFlush to false for much higher logging throughput -->
<immediateFlush>true</immediateFlush>
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="FILE" />
</root>
</configuration>
위의 Appender 설정을 분석해봅시다. 출력할 타겟 파일의 이름은 "testFile.log"이며 이미 같은 이름의 파일이 존재한다면 끝에서부터 이어서 로그를 출력하도록 할 것입니다. 로그 이벤트 발생 즉시 outputStream을 flush 하여 appender가 닫히더라도 로그 이벤트의 유실이 일어나지 않도록 합니다.
> Uniquely name files (by timestamp)
batch application처럼 짧은 수명주기를 가진 애플리케이션의 경우, 매 실행마다 Unique 한 이름의 새로운 로그 파일을 만드는 것이 좋습니다. 아래의 예시는 timestamp를 이용하여 타깃 파일을 동적으로 정해준 Appender 설정입니다.
<configuration>
<!-- Insert the current time formatted as "yyyyMMdd'T'HHmmss" under
the key "bySecond" into the logger context. This value will be
available to all subsequent configuration elements. -->
<timestamp key="bySecond" datePattern="yyyyMMdd'T'HHmmss"/>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<!-- use the previously created timestamp to create a uniquely
named log file -->
<file>log-${bySecond}.txt</file>
<encoder>
<pattern>%logger{35} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="FILE" />
</root>
</configuration>
timestamp 태그는 logback에서 정의할 수 있는 variable의 특수한 형태로 시간에 대한 값을 저장하는 변수입니다. timestamp는 필수로 key와 datePattern 속성을 가지며 선택적으로 timeReference 속성을 가집니다. key 속성은 선언한 변수의 이름을 지정하며 일반적인 variable을 사용하듯이 ${ } 내에 지정해 준 key값을 문자열로 넘겨 사용합니다. datePattern 속성은 현재 시간을 문자열로 변환할 때의 패턴을 지정합니다. 패턴을 지정하는 규칙은 SimpleDateFormat의 기준을 따릅니다. timeReference 속성은 timestamp가 사용할 시간이며, 지정하지 않을 경우 기본값으로는 현재 시간을 사용합니다. 이 속성을 사용하는 경우는 LoggerContext의 동작 시작 시간을 사용할 때입니다. 이 경우는 timeReference="contextBirth"로 처리해줄 수 있습니다.
위 Appender는 애플리케이션 시작 시 타깃 파일을 log-{yyyyMMdd'T'HHmmss}.txt 로 지정합니다 (ex. log-20201008T213000.txt).