회사에서 업무를 하나 받아버렸습니다. 해당 업무는 DB에서 특정 이벤트가 발생시 해당 로그를 메일로 전송해 하나의 증거 자료를 남기는 기능을 구현하는 것이었습니다. 처음에는 AOP, Mybatis Plugin과 관련있다 생각하여 관련 정보를 찾아서 테스트 해보았지만, sql 이벤트를 가져오기가 어려웠습니다. (물론 방법이 있을수도 있겠지만 저는 찾지 못했습니다..)
다른 방안을 찾던 도중 logback에 대해 알게 되었고 logback의 다양한 설정과 기능 중 sql 이벤트를 가져올 수 있는 방법이 있어 이를 적용해 보았고 짧은 코드로 원하는 목표를 이룰 수 있었습니다!
일을 진행하면서 logback의 구조가 궁금하였고 logback에는 어떤 기능을 추가로 존재하는지 알아보는 시간을 가져보았습니다.
이 글은 logback 관련 공부를 진행하면서 알게 된 내용을 정리한 글입니다.
logback을 알아보기에 앞서 Spring의 기본 로깅 방식에 대해 알아보고 시작하겠습니다.
스프링에서는 따로 설정하지 않는한 기본적으로 Apache의 Jakarta Common Logging(JCL) 을 사용합니다.
다음 그림을 보신다면 현재 Spring Framework에서는 JCL의 구현체인 log를 사용하는데 대표적인 JCL 구현체로는 log4j가 있습니다. JCL 구현체 라이브러리를 스프링 어플리케이션의 의존성으로 추가해주실 경우 다음 사진의 Log4j 위치에 해당 구현체가 추가되어 로깅 프레임워크가 동작하는 방식입니다.
지금부터 알아볼 logback은 slf4j 인터페이스의 구현체입니다. JCL의 구현체가 아니기 때문에 logback을 사용하고 싶으신 분들은 다음 두 개의 선택지 중 하나를 고르셔야 됩니다.
따라서 다음과 같이 jcl-over-slf4j
와 같은 어댑터 역할을 수행하는 라이브러리를 이용해야 logback을 이용하실 수 있습니다.
그럼 지금부터는 logback에 대해 알아보겠습니다.
logback은 log4j 1.x 버전에서 제공하는 기능을 보완하고 성능적으로도 향상시킨 로깅 시스템입니다.
logback은 3가지 모듈을 통해 다양한 기능을 제공합니다.
logback-core
: Appender와 Layout 인터페이스가 존재하는 모듈logback-classic
: logback-core
와 SLF4J API
라이브러리를 포함하고 있음, Logger 클래스가 포함된 모듈logback-access
: Servlet Container와 통합되어 HTTP 액세스에 대한 로깅 기능을 제공. Container 레벨에서 사용logback을 이용하고 싶은 경우, 아래의 의존성을 추가해주셔야 합니다. (Spring 로깅 구조 참고)
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.5</version>
</dependency>
logback의 기본 구성은 다음과 같습니다. 각각의 구성 요소의 역할은 아래 설명을 참고하시길 바랍니다.
logback 구성 요소 역할
Appender
인터페이스를 구현해 이벤트를 처리(Logger는 logging 이벤트를 처리) Appender는 logger로 정의한 내용들을 어떻게 처리할 것인지 처리 역할을 위임받은 클래스입니다. Appender는 태그를 통하여 구성되며 name와 class 속성을 필수적으로 가져야만 합니다.
appender와 관련된 클래스는 각각 다음과 같은 기능을 담당합니다.
logging 이벤트를 System.err
또는 System.out
에 추가합니다. 이 때, 사용자가 지정한 인코더를 사용해 이벤트 형식을 지정합니다.
예시
<!--System.out에 로그를 추가합니다. -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
</appender>
OutputStreamAppender
의 서브클래스로서 파일에 로그 이벤트를 추가합니다. 대상 파일은 파일 옵션을 통해 지정되며 속성에 따라 파일이 추가되거나 분할됩니다.
예시
<!--testFile.log에 로그를 남깁니다.-->
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>testFile.log</file>
<append>true</append>
<immediateFlush>true</immediateFlush>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
RollingFileAppender는 FileAppender
를 상속하며 파일을 롤오버하는 기능으로 확장합니다. 롤오버의 예시로는 날짜별 로그파일 작성, 시간별 로그파일 작성 등 일정 기간, 혹은 조건 등에 따라 로그 파일을 분리하는 것입니다.
예시
<appenders>
<rollingFile name="LogToFile" fileName="logs/application.log"
filePattern="logs/application.log.%d{yyyy-MM-dd-hh-mm}">
<patternLayout pattern="${sys:FILE_LOG_PATTERN}" />
<!-- 정책을 통해 로그파일 롤오버 =>1분마다 로그파일이 생성-->
<policies>
<timeBasedTriggeringPolicy interval="1" modulate="true" />
</policies>
<!-- 기본 롤오버 전략 => 생성된 로그 파일이 3개가 초과될 때 1개 삭제-->
<defaultRolloverStrategy>
<delete basePath="logs" maxDepth="1">
<ifAccumulatedFileCount exceeds="3"/>
</delete>
</defaultRolloverStrategy>
</rollingFile>
</appenders>
SMTP Appender는 하나 이상의 고정 버퍼에 로깅 이벤트를 누적하고 사용자 지정 이벤트가 발생한 이후 해당 버퍼의 내용을 이메일로 발송합니다.
기본적으로 이메일 전송은 ERROR 레벨의 로깅 이벤트에 의해 트리거되며, 비동기식으로 수행됩니다.
예시
<appender name="EMAIL" class="ch.qos.logback.classic.net.SMTPAppender">
<smtpHost>gelog.smtp.com</smtpHost>
<to>gelloger@naver.com</to>
<from>gelloger@naver.com</from>
<subject>TESTING: %logger{20} - %m</subject>
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%date %-5level %logger{35} - %message%n</pattern>
</layout>
</appender>
<root level="DEBUG">
<appender-ref ref="EMAIL" /> <!--에러 발생시 이메일 발송 트리거 -->
</root>
CustomAppender는 사용자가 직접 구현하는 appender 클래스를 의미합니다.
AppenderBase<ILoggingEvent>
를 상속해 클래스를 작성하는데 logback.xml에서 해당 appender를 호출 시 start()
메소드는 자동으로 실행되며 해당 logger가 동작할 경우 append(ILoggingEvent iLoggingEvent)
메소드가 동작을 수행합니다.
예시
package com
public class LoggingAppender extends AppenderBase<ILoggingEvent> {
private static final SimpleDateFormat datetimeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
protected void append(ILoggingEvent iLoggingEvent) {
System.out.println(iLoggingEvent.getFormattedMessage());
}
@Override
public void start() {
super.start();
}
}
<!-- GellogerMapper.create 호출시 trace level로 EvidenceLoggingAppender가 이벤트 실행 -->
<appender name="customlog" class="com.EvidenceLoggingAppender">
</appender>
<logger name="test.gelog.GellogerMapper.create" level="trace">
<appender-ref ref="customlog"/>
</logger>
지금까지는 Appender의 종류를 알아보았습니다. 지금부터는 Appender의 구성요소로서 log를 어떻게 처리할 것인가를 세부적으로 다룰 수 있는 구성 요소에 대해 알아보겠습니다.
Encoder는 들어오는 log 이벤트를 OutputStream
에서 바이트 배열로 변환시키는 작업을 수행합니다. logback 0.9.19
버전 이후 출시된 Encoder는 Layout과 함께 log 출력값을 정의합니다.
LayoutWrappingEncoder
는 Encoder와 Layout 간의 격차를 해소해주기 위해 Layout을 wrapping합니다.
package ch.qos.logback.core.encoder;
public class LayoutWrappingEncoder<E> extends EncoderBase<E> {
protected Layout<E> layout;
private Charset charset;
public byte[] encode(E event) {
String txt = layout.doLayout(event);
return convertToBytes(txt);
}
private byte[] convertToBytes(String s) {
if (charset == null) {
return s.getBytes();
} else {
return s.getBytes(charset);
}
}
}
다음 로직을 통해 Encoder는 Layout이 로그 이벤트를 정의해놓은 형식으로 변환한 문자열에 대해 바이트로 변환해 반환하는 역할을 수행합니다.
가장 일반적으로 사용되는 레이아웃으로, FileAppender 또는 FileAppender의 서브클래스는 PatternLayout으로 구성될 때마다 무조건 PatternLayoutEncoder를 사용해야 합니다.
logback은 로그 파일의 상단에 로그 출력 패턴을 정의할 수 있습니다. 기본적으로 패턴은 비활성되어 있어 outputPatternAsHeader
을 활성화 시켜야 이용이 가능합니다.
예시
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>foo.log</file>
<encoder>
<pattern>%d %-5level [%thread] %logger{0}: %msg%n</pattern>
<outputPatternAsHeader>true</outputPatternAsHeader>
</encoder>
</appender>
Layout이란 들어오는 이벤트를 문자열로 변환하는 역할을 하는 logback 구성 요소입니다.
우리는 appender와 encoder, layout의 조합으로 들어오는 로그를 원하는 형식대로 구성할 수 있습니다.
예시
package chapters.layouts;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.LayoutBase;
public class MySampleLayout extends LayoutBase<ILoggingEvent> {
public String doLayout(ILoggingEvent event) {
StringBuffer sbuf = new StringBuffer(128);
sbuf.append(event.getTimeStamp() - event.getLoggingContextVO.getBirthTime());
sbuf.append(" ");
sbuf.append(event.getLevel());
sbuf.append(" [");
sbuf.append(event.getThreadName());
sbuf.append("] ");
sbuf.append(event.getLoggerName();
sbuf.append(" - ");
sbuf.append(event.getFormattedMessage());
sbuf.append(CoreConstants.LINE_SEP);
return sbuf.toString();
}
}
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<!-- 요 놈이 위에서 만든 layout인데 log 이벤트가 다음 문자열로 들어옴 -->
<layout class="chapters.layouts.MySampleLayout" />
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
필터를 통해 Appender는 특정 이벤트에 대해 조건을 가지고 필터링을 수행할 수 있습니다.
필터 종류
예시
public class SampleFilter extends Filter<ILoggingEvent> {
@Override
public FilterReply decide(ILoggingEvent event) {
//"sample"이라는 문자열을 포함할 경우 ACCEPT, 아닐 경우 다음 필터 확인
if (event.getMessage().contains("sample")) {
return FilterReply.ACCEPT;
} else {
return FilterReply.NEUTRAL;
}
}
}
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="chapters.filters.SampleFilter" />
<encoder>
<pattern>
%-4relative [%thread] %-5level %logger - %msg%n
</pattern>
</encoder>
</appender>
logger는 log를 남길 대상들을 의미합니다. 우리는 logger와 appender의 조합으로 특정 classpath는 콘솔에 로그를 남기고, 어떤 로그는 에러 발생시 이메일을 발송하고, 어떤 상황에서는 custom한 로그 이벤트 처리를 할 수 있도록 다양한 처리를 가능하도록 구성할 수 있습니다.
Root는 최상단 logger로서 아무 설정도 안할 경우 root의 log level에 따라 로그 이벤트를 남길지 안남길지 설정이 가능해집니다.
예시
<!-- com.projectgelog 경로에서 발생하는 모든 이벤트는 debug로 로그를 남기고, 이벤트 처리는 gelog라는 이름을 가진 appender가 알아서 처리해줄거야 -->
<logger name="com.projectgelog" level="debug">
<appender-ref ref="gelog"/>
</logger>
log level은 크게 6가지로 나뉘어지며 각각 다른 의미를 가지고 있습니다. 다음 표를 보시고 필요하신 log level을 logger에 작성해주시면 됩니다.
로그 | Level |
---|---|
FATAL | 애플리케이션이 이벤트를 발생했거나 중요한 비즈니스 기능 중 하나가 더 이상 작동하지 않는 상태일 경우를 알려주는 로그 Level |
ERROR | 애플리케이션이 하나 이상의 기능이 제대로 작동하지 않는 문제에 부딪힐 때 사용해야 하는 로그 Level |
WARN | 애플리케이션이 문제 또는 프로세스에 방해가 될 수 있는 상황에 예기치 않은 일이 발생했음을 나타내는 로그 Level |
INFO | 애플리케이션이 특정 상태에 들어갔는지 등을 나타내는 표준 로그 Level, 일반적으로 정보제공을 위해 사용 |
DEBUG | 문제를 해결하는데 필요할 수 있는 정보 제공, 모든 것이 올바르게 정상적으로 동작하는지 확인하기 위해 테스트 환경에서 실행할 경우 사용 |
TRACE | 애플리케이션의 모든 상황을 완벽하게 파악하는 상황에서 사용, debug의 윗 수준으로 log정보가 매우 상세하게 나타냄 |
오늘은 logback.xml에 대해 알아보는 시간을 가졌습니다. 생각보다 알아볼 내용이 꽤나 있었지만 이것들만 알면 log 관련해 다루는 것은 매우 easy해질 것이라 생각됩니다.
원하는 appender가 없으시다면 직접 작성해 appender를 사용하시는 것도 좋은 방법일 것 같습니다.
appender는 filter, encoder, layout을 통해 좀 더 세부적으로 원하는 이벤트 처리기능을 구현할 수 있기 때문에 위의 내용을 읽어보시고 한 번 구성해보시는 것을 추천드립니다.
마지막으로는 타 사이트에서 읽은 logback.xml 파일인데 앞서 읽은 내용을 토대로 어떻게 로그 관련 구성을 진행했는지 확인해보시길 바라면서 글을 마무리하겠습니다.
감사합니다.
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOGS_ABSOLUTE_PATH" value="./logs"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>[%d{yyyy-MM-dd HH:mm:ss}:%-3relative][%thread] %-5level %logger{36} - %msg%n</Pattern>
</layout>
</appender>
<appender name="INFO_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>./logs/info.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>[%d{yyyy-MM-dd HH:mm:ss}:%-3relative][%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>./was-logs/info.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>180</maxHistory>
</rollingPolicy>
</appender>
<appender name="WARN_LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>./logs/warn.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>[%d{yyyy-MM-dd HH:mm:ss}:%-3relative][%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>./was-logs/warn.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>180</maxHistory>
</rollingPolicy>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
<logger name="LogController" additivity="false">
<level value = "DEBUG" />
<appender-ref ref="INFO_LOG" />
<appender-ref ref="WARN_LOG" />
</logger>
<logger name="org.hibernate.SQL" additivity="false">
<level value = "DEBUG" />
<appender-ref ref="INFO_LOG" />
</logger>
</configuration>
https://ckddn9496.tistory.com/79
https://www.baeldung.com/logback
https://www.baeldung.com/log4j2-custom-appender
https://logback.qos.ch/manual/appenders.html
https://tecoble.techcourse.co.kr/post/2021-08-07-logback-tutorial/