Study/Logging

[Logging] Logback이란?

검프 2021. 7. 14. 22:29

블로그를 작성하고, 테코톡을 진행했어요. 더 쉽게 이해하고 싶다면 아래 영상을 시청해주세요!

[10분 테코톡] ☂️ 검프의 Logging(로깅) #2

Logback

로깅 프레임워크 중 하나로, SLF4J의 구현체에요.
SLF4J에 관한 설명은 앞선 포스팅으로 대신할게요.
[Logging] SLF4J란?


Logback 구조

Logback은 아래 3가지 모듈로 나뉘어요

  • logback-core
  • logback-classic
  • logback-access

logback-core: 다른 두 모듈을 위한 기반 역할을 하는 모듈이에요. AppenderLayout 인터페이스가 이 모듈에 속해요.

logback-classic: logback-core에서 확장된 모듈로, logback-core와 SLF4J API 라이브러리를 가져요.(추가적으로 포함된 라이브러리들은 해당 artifact의 올바른 버전 사용이 필요하고, 모두 명시적으로 선언하는 것이 좋기에 logback-classic추가시 exclude에 추가). Logger 클래스가 이 모듈에 속해요

logback-access: Servlet Container와 통합되어 HTTP 액세스에 대한 로깅 기능을 제공해요. logback-core는 기반 기술이기에 필요하지만 logback-classic 및 slf4j와 무관해요. 웹 애플리케이션 레벨이 아닌 컨테이너 레벨에서 설치돼야해요.


설정요소

Logback을 이용해 로깅을 수행하기 위해서 필요한 주요 설정요소로는 Logger, Appender, Layout(Encoder)의 3가지가 있어요. 각각은 아래와 같은 역할을 수행해요.

우선 출력하게될 로그의 Pattern 옵션에 대해 알아보고, 설정요소 각각에 대해 알아보도록 해요.

Log Pattern

%logger: 패키지 포함 클래스 정보
%logger{0}: 패키지를 제외한 클래스 이름만 출력
%logger{length}: Logger name을 축약할 수 있음. {length}는 최대 자리 수, ex)logger{35}
%-5level: 로그 레벨, -5는 출력의 고정폭 값(5글자), 로깅레벨이i nfo일 경우 빈칸 하나 추가
${PID:-}: 프로세스 아이디
%d: 로그 기록시간 출력
%p: 로깅 레벨 출력
%F: 로깅이 발생한 프로그램 파일명 출력
%M: 로깅일 발생한 메소드의 명 출력
%line: 로깅이 발생한 호출지의 라인
%L: 로깅이 발생한 호출지의 라인
%thread: 현재 Thread 명
%t: 로깅이 발생한 Thread 명
%c: 로깅이 발생한 카테고리
%C: 로깅이 발생한 클래스 명 (%C{2}는 somePackage.SomeClass 가 출력됨)
%m: 로그 메시지
%msg: - 로그 메시지 (=%message)
%n: 줄바꿈(new line)
%%: %를 출력
%r : 애플리케이션 시작 이후부터 로깅이 발생한 시점까지의 시간(ms)
%d{yyyy-MM-dd-HH:mm:ss:sss}: %d는 date를 의미하며 중괄호에 들어간 문자열은 dateformat을 의미. 따라서 [2021-07-12 12:42:78]과 같은 날짜가 로그에 출력됨.
%-4relative: %relative는 초 아래 단위 시간(밀리초)을 나타냄. -4를하면 4칸의 출력폼을 고정으로 가지고 출력. 따라서 숫자에 따라 [2021-07-12 12:42:78:232] 혹은 [2021-07-12 12:42:78:2332]와 같이 표현됨

Extra

<springProfile>: logback 설정 파일에서 복수개의 프로파일 설정 가능 (아래 예시는 실행 환경이 prod이 아닐시 ConsoleAppender를 사용한다는 것임. 설정 또한 환경에 따라 불러올 수 있음)

<springProfile name="!prod">

        <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>${LOG_PATTERN}</pattern>
            </encoder>
        </appender>

        <root level="INFO">
            <appender-ref ref="CONSOLE"/>
        </root>

</springProfile>

<springProfile name="local">
        <property resource="logback-local.properties"/>
</springProfile>

<Filter>: 해당 패키지에 무조건 로그를 찍는 것 말고도 필터링이 필요한 경우에 사용하는 기능
(예시는 레벨 필터를 추가해서 level이 error인 것만 찍게 만듬)

<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%-4relative [%thread] %-5level %logger{35} - %msg %n</pattern>
    </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
        <level>ERROR</level>
        <onMatch>ACCEPT</onMatch>
        <onMismatch>DENY</onMismatch>
    </filter>
  </appender>

  <root level="DEBUG">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

1. Logger

실제 로깅을 수행하는 구성요소에요.
ch.qos.logback.classic.Level 클래스에는 로거가 사용 가능한 다섯 가지 레벨인 TRACE, DEBUG, INFO, WARN, ERROR가 정의되어 있어요. 즉, Level 속성을 통해서 출력할 로그의 레벨을 조절할 수 있어요.
기본 로그의 레벨은 아래와 같으며, 지정된 레벨 이하의 메서드는 기록되지 않아요. 기본 레벨은 DEBUG에요.

TRACE < DEBUG < INFO < WARN < ERROR

예를 들어 INFO 레벨로 지정한 로거는 INFO, WARN, ERROR 로그만 기록하게 돼요.

사용법
LoggerFactory에서 로거 객체를 불러온 후, 로거 객체를 이용해서 코드의 원하는 부분에 상황에 맞는 레벨의 로그를 찍으면 돼요.

package lab;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LabApplication {
    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger(LabApplication.class);

        for (int count = 1; count <= 10; count++) {
            logger.trace("trace 로깅이야!!! {}", count);
            logger.debug("debug 로깅이야!!! {}", count);
            logger.info("info 로깅이야!!! {}", count);
            logger.warn("warn 로깅이야!!! {}", count);
            logger.error("error 로깅이야!!! {}", count);
        }
    }
}

2. Appender

Logback은 로그 이벤트를 wirte하는 작업을 Appender에게 위임해요.
즉, Appender는 로그 메세지가 출력될 대상을 결정해요.

Appender의 구현 클래스 중 OutputStreamAppender의 하위 클래스에 대해 알아보도록 해요. (이외에도 더 많은데 이는 공식 문서를 확인하길 바라요..)

http://logback.qos.ch/manual/appenders.html

https://user-images.githubusercontent.com/48986787/125202000-965f1c80-e2ac-11eb-8c40-ae4d0cec9c19.png

  • ch.qos.logback.core.UnsynchronizedAppenderBase:
    synchronized한 동작이 필요하지 않을때 사용해요.(다른 추상 클래스인 ApeenderBase의 doAppend()는 synchronized임)
  • ch.qos.logback.core.OutputStreamAppender:
    java.io.OutputStream에 로그 이벤트를 append 해요.
    추상클래스이기 때문에 이를 직접 사용하지 않고, 하위 클래스에 책임을 위임해요.
  • ch.qos.logback.core.ConsoleAppender:
    콘솔에 System.out 또는 System.err를 이용하여 로그 이벤트를 append해요.
    사용자가 지정한 endoer를 통해 이벤트의 포맷 형식을 지정할 수 있어요.
<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%-4relative [%thread] %-5level %logger{35} - %msg %n</pattern>
    </encoder>
  </appender>

  <root level="DEBUG">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>
  • ch.qos.logback.core.FileAppender:
    파일에 로그 이벤트를 append해요.
    매 실행마다 Unique한 이름의 새로운 로그 파일을 만드는것이 좋기에 아래와 같이 timestamp를 이용하여 타깃 파일을 동적으로 설정할 수 있어요.
<configuration>
  <timestamp key="bySecond" datePattern="yyyyMMdd'T'HHmmss"/>

  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>log-${bySecond}.txt</file>
    <encoder>
      <pattern>%logger{35} - %msg%n</pattern>
    </encoder>
  </appender>

  <root level="DEBUG">
    <appender-ref ref="FILE" />
  </root>
</configuration>
  • ch.qos.logback.core.rolling.RollingFileAppender:
    FileAppender를 상속하여 로그 파일을 rollover해요. 여기서 rollover는 타겟 파일을 바꾸는 것으로 이해할 수 있어요.
    예를들어, log.txt를 타겟파일로 로그 메시지를 append하다가 어느 조건(시간, 용량)에 다다르면, 이전파일을 저장하고, 타겟 파일을 바꿔요.

Rolling Policies

  1. TimeBasedRollingPolicy
    가장 많이 알려진 RollingPolicy 종류 중 하나에요.
    시간에 기반하여 rollover 정책을 정의할 수 있으며, 주로 일 또는 월 단위로 rollover 해요.
    rollover 뿐만 아닌 Trigger에 대한 책임도 지기 때문에 RollingPolicy와 TriggeringPolicy 인터페이스를 모두 implements 해요.

TimeBasedRollingPolicy는 필수적으로 fileNamePattern 속성을 가져요.

<configuration>
  <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>logFile.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <!-- 하루 동안의 log를 남김 -->
      <fileNamePattern>logFile.%d{yyyy-MM-dd}.log</fileNamePattern>

      <!-- 30일동안, 총 최대 3GB의 log를 저장함-->
      <maxHistory>30</maxHistory>
      <totalSizeCap>3GB</totalSizeCap>

    </rollingPolicy>

    <encoder>
      <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
    </encoder>
  </appender> 

  <root level="DEBUG">
    <appender-ref ref="FILE" />
  </root>
</configuration>
  • <fileNamePattern> : 파일 쓰기가 종료된 log 파일명의 패턴을 지정해요.

fileNamePattern에 명시된 dateTime 패턴의 최소 단위에 따라 rollover 단위가 달라져요. 아래의 예시를 통해 rollover가 trigger 되는 시점을 확인할 수 있어요.

fileName의 dateTime 패턴

.%d: default %d는 yyyy-MM-dd에요. 매일 자정에 새로운 로그 파일로 rollover 해요.
.%d{yyyy-MM-dd_HH-mm}:매 분 새로운 로그 파일로 rollover해요.
/%d{yyyy/MM}/foo.txt: 매월 새로운 디렉토리를 만들어 하위에 foo.txt 파일로 rollover해요
.%d.gz: 매일 새로운 로그 파일로 rollover하고, 이전 로그파일은 GZIP으로 압축해요.

  • <maxHistory>: 최대 파일 생성 갯수를 정의해요.

maxHistory가 30이고, Rolling 정책을 일 단위로 하면 30일 동안만 저장되고, 월 단위로 하면 30개월간 저장돼요. 예를들어 30일동안 30개의 파일이 유지됐다면 오래된 파일부터 삭제돼요.

  • <totalSizeCap>: 저장소의 최대 크기를 지정해요. maxHistory와 함께 쓰일 경우 1순위로 maxHistory에 대하여 처리된 후 totalSizeCap 이 적용돼요.

2. SizeAndTimeBasedRollingPolicy
TimeBasedRollingPolicy에서 각각의 로그 파일에 대한 크기를 제한을 하는 부분이 추가됐어요.
fileNamePattern에서 %i%d가 필수적으로 포함돼야해요

<configuration>
  <appender name="ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>mylog.txt</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
      <!-- 하루 동안의 log를 남김 -->
      <fileNamePattern>mylog-%d{yyyy-MM-dd}.%i.zip</fileNamePattern>
      <!-- 각각의 파일은 100MB로 저장되고, 30일동안, 최대 3GB의 log를 저장함-->
       <maxFileSize>100MB</maxFileSize>    
       <maxHistory>30</maxHistory>
       <totalSizeCap>3GB</totalSizeCap>
    </rollingPolicy>
    <encoder>
      <pattern>%msg%n</pattern>
    </encoder>
  </appender>

  <root level="DEBUG">
    <appender-ref ref="ROLLING" />
  </root>

</configuration>
  • <maxFileSize>: 한 파일당 최대 파일 용량을 저장해요. log 내용의 크기도 IO성능에 영향을 미치기 때문에 되도록 너무 크지 않는 사이즈로 지정하는게 좋아요(10MB 권장)
    용량의 단위는 KB, MB, GB 3가지를 지정할 수 있어요.
  • %i: 롤링 순번을 자동적으로 지정함 (ex) 0, 1, 2 , 3 ...)

3. Encoder(Layout)

Encoder는 로그 이벤트를 바이트 배열로 변환하고, 해당 바이트 배열을 OutputStream에 쓰는 작업을 담당해요.
즉, Appender에 포함되어 사용자가 지정한 형식으로 표현 될 로그메시지를 변환하는 역할을 담당하는 요소라 말할 수 있어요.
FileAppender와 하위 클래스는 encoder를 필요로 하고, 더 이상 layout은 사용하지 않기에 이제는 layout보다는 encoder를 사용하면 돼요.

https://user-images.githubusercontent.com/48986787/125275279-cc9ea980-e349-11eb-8463-4e4315dd38cc.png

PatternLayoutEncoder

<appender name="FILE" class="ch.qos.logback.core.FileAppender">
  <file>testFile.log</file>
  ...
  <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
    <pattern>%msg%n</pattern>
  </encoder>
</appender>

위와 같이 사용하면되는데, 부분은 로 대체할 수 있어요.

<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>

또한 위와 같이 사용한다면, 아래와 같이, 헤더에 패턴을 출력할 수 있어요.

#logback.classic pattern: %d [%thread] %-5level %logger{36} - %msg%n
2012-04-26 14:54:38,461 [main] DEBUG com.foo.App - Hello world
2012-04-26 14:54:38,461 [main] DEBUG com.foo.App - Hi again
...

프로젝트에 적용해보기

예제 및 실습파일은 아래 링크에서 확인할 수 있어요.

https://github.com/Livenow14/slf4j-logback-lab

Log 설정 참조 순서

1) classpath(resources디렉토리 밑)에 logback-spring.xml파일이 있으면 설정파일을 읽음.
2) logback-spring.xml파일이 없다면 .yml(.properties)파일의 설정을 읽음.
3) logback-spring.xml파일과 .yml(.properties)파일이 동시에 있으면 .yml(.properties) 설정 파일을 적용 후 xml파일이 적용됨.
스프링 부트 에서는 logback-spring.xml을 사용해서 Spring이 logback을 구동할 수 있도록 지원해 주며 이를 이용하여 profile, 즉 배포 환경에 따른(spring.profiles.active을 활용하여) application.xml에 설정된 properties를 읽어올 수 있음.

콘솔에 출력하기

private Team findTeam(String teamName) {
        Optional<Team> team = teamRepository.findByName(teamName);
        if (team.isEmpty()) {
            String detailMessage = String.format("존재하지 않는 팀입니다. 입력값: %s", teamName);
            logger.info(detailMessage);
            throw new IllegalArgumentException(detailMessage);
        }
        return team.get();
    }
<?xml version="1.0" encoding="UTF-8"?>

<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>[%d{yyyy-MM-dd HH:mm:ss}:%-4relative] %green([%thread]) %highlight(%-5level) %boldWhite([%C.%M:%yellow(%L)]) - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>
[2021-07-12 21:11:18:9095] [http-nio-8080-exec-2] INFO  [com.livenow.slf4jlogbacklab.service.TeamService.findTeam:54] - 존재하지 않는 팀입니다. 입력값: 좋은 팀
[2021-07-12 21:11:18:9099] [http-nio-8080-exec-2] ERROR [com.livenow.slf4jlogbacklab.Slf4jRestControllerAdvice.illegalArgumentException:25] - IllegalArgumentException: 존재하지 않는 팀입니다. 입력값: 좋은 팀

파일에 출력하기

테스트환경이 아닌, 실제 프로덕션 환경에서는 로그를 파일로 저장해야한다는 요구사항이 들어왔어요.
우리는 이때 <springProfile>을 통해 설정할 수 있어요.

<?xml version="1.0" encoding="UTF-8"?>

<configuration>
    <timestamp key="BY_DATE" datePattern="yyyy-MM-dd"/>
    <property name="LOG_PATTERN"
              value="[%d{yyyy-MM-dd HH:mm:ss}:%-4relative] %green([%thread]) %highlight(%-5level) %boldWhite([%C.%M:%yellow(%L)]) - %msg%n"/>

    <springProfile name="!prod">
        <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>${LOG_PATTERN}</pattern>
            </encoder>
        </appender>

        <root level="INFO">
            <appender-ref ref="CONSOLE"/>
        </root>
    </springProfile>

    <springProfile name="prod">
        <appender name="FILE-INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>./log/info/info-${BY_DATE}.log</file>
            <filter class = "ch.qos.logback.classic.filter.LevelFilter">
                <level>INFO</level>
                <onMatch>ACCEPT</onMatch>
                <onMismatch>DENY</onMismatch>
            </filter>
            <encoder>
                <pattern>${LOG_PATTERN}</pattern>
            </encoder>
            <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
                <fileNamePattern> ./backup/info/info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
                <maxFileSize>100MB</maxFileSize>
                <maxHistory>30</maxHistory>
                <totalSizeCap>3GB</totalSizeCap>
            </rollingPolicy>
        </appender>

        <appender name="FILE-WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>./log/warn/warn-${BY_DATE}.log</file>
            <filter class = "ch.qos.logback.classic.filter.LevelFilter">
                <level>WARN</level>
                <onMatch>ACCEPT</onMatch>
                <onMismatch>DENY</onMismatch>
            </filter>
            <encoder>
                <pattern>${LOG_PATTERN}</pattern>
            </encoder>
            <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
                <fileNamePattern> ./backup/warn/warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
                <maxFileSize>100MB</maxFileSize>
                <maxHistory>30</maxHistory>
                <totalSizeCap>3GB</totalSizeCap>
            </rollingPolicy>
        </appender>

        <appender name="FILE-ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>./log/error/error-${BY_DATE}.log</file>
            <filter class = "ch.qos.logback.classic.filter.LevelFilter">
                <level>ERROR</level>
                <onMatch>ACCEPT</onMatch>
                <onMismatch>DENY</onMismatch>
            </filter>
            <encoder>
                <pattern>${LOG_PATTERN}</pattern>
            </encoder>
            <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
                <fileNamePattern> ./backup/error/error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
                <maxFileSize>100MB</maxFileSize>
                <maxHistory>30</maxHistory>
                <totalSizeCap>3GB</totalSizeCap>
            </rollingPolicy>
        </appender>

        <root level="INFO">
            <appender-ref ref="FILE-INFO"/>
            <appender-ref ref="FILE-WARN"/>
            <appender-ref ref="FILE-ERROR"/>
        </root>
    </springProfile>

</configuration>

참고

Http 통신에 대한 로그는 아래 라이브러리를 사용하는 것을 추천

https://user-images.githubusercontent.com/48986787/125629853-5b6bffa5-4cea-4c5c-bb5e-249762ae08d8.png

Refer

https://goddaehee.tistory.com/206
https://ckddn9496.tistory.com/77?category=428336
https://ozymaxx.github.io/blog/2020/06/19/logback-access-en/
https://github.com/ozymaxx/spring-redis-demo/blob/master/build.gradle#L24
https://dennis-xlc.gitbooks.io/the-logback-manual/content/en/
https://stackoverflow.com/questions/61080281/how-to-include-xml-in-log-back-xml