SW 개발/Spring

Spring Boot, Kotlin, 로깅 정책 수립 및 방법 정리

지단로보트 2021. 3. 18. 08:22

개요

  • 로그는 매우 중요하다. 나는 감히 로그가 애플리케이션의 시작과 끝이라고 단언할 수 있다. 잘 설계된 로그는 빠른 장애 대응 뿐만 아니라 회사 매출과도 직결되는 유의미한 데이터로서도 활용될 수 있다. Spring Boot의 로깅은 기본적으로 SLF4J 인터페이스를 기반으로 하여 Logback을 구현체로 작동하도록 설계되어 있다. 이번 글에서는 여기에 더하여 Kotlin 환경에서 kotlin-logging을 이용하여 로깅 정책을 수립하고 로그 적재의 예를 설명하고자 한다.

SLF4J 로그 레벨의 이해

  • 로그 레벨을 명확히 이해하면 개발환경, 운영환경 단위로 어느 수준으로 로그를 남길지를 결정할 수 있다. 로그 레벨은 계층 구조로서 아래와 같다. (상위 계층은 하위 계층을 모두 포함하기 때문에 상위로 갈수록 상세한 로그를 남기게 된다.)
TRACE (TRACE, DEBUG, INFO, WARN, ERROR, FATAL) // 특정 케이스에 대한 트러블슈팅에 적합
DEBUG (DEBUG, INFO, WARN, ERROR, FATAL) // 개발 환경에 적합
INFO (INFO, WARN, ERROR, FATAL) // 운영 환경에 적합
WARN (WARN, ERROR, FATAL)
ERROR (ERROR, FATAL)
FATAL (FATAL)

build.gradle

  • 프로젝트 루트의 /build.gradle 파일에 아래 내용을 추가한다.
dependencies {
    implementation group: 'io.github.microutils', name: 'kotlin-logging-jvm', version: '2.0.6'
}

application.yml

  • 프로젝트의 /src/main/resources/application.yml 파일에 아래 내용을 추가한다.
logging:
  config: classpath:logback.xml
  • 위는 로깅 정책을 Logback의 환경 설정 파일에 위임하는 조치로, Logback이 제공하는 다양한 로깅 정책을 세밀하게 지정할 수 있다. 프로파일 단위로 다른 환결 설정 파일을 지정하면 개발 환경, 운영 환경을 특성에 맞게 분리하여 정책을 수립하는 것도 가능하다.

logback.xml

  • 가장 중요한 로그 환경 설정 파일을 작성할 차례이다. 공통적으로 많이 사용되는 로깅 패턴인 TEXT_CONSOLE, TEXT_FILE에 추가로 원격지 서버에 GELF 형식의 메시지를 UDP로 전송하는 GELF_UDP를 추가하였다. 프로젝트의 /src/main/resources/logback.xml 파일에 아래 내용을 추가한다.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration>

<configuration>

    <!-- 로그 파일을 적재할 디렉토리 경로를 지정 -->
    <property name="LOG_FILE_PATH" value="/home/foobar/logs"/>
    <!-- 로그 파일을 적재할 파일 이름을 지정 -->
    <property name="LOG_FILE_NAME" value="foobar"/>

    <!-- TEXT_CONSOLE 어펜더 정의 -->
    <!-- 로컬 개발 환경에 적합 -->
    <appender name="TEXT_CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <charset>UTF-8</charset>
            <Pattern>%d %-4relative [%thread] %-5level %logger{35} - %msg%n</Pattern>
        </encoder>
    </appender>

    <!-- TEXT_FILE 어펜더 정의 -->
    <!-- 운영 환경에 적합 -->
    <!-- 일자별로 로그 파일을 적재하되, 100MB를 초과하면 로그 파일 분할 -->
    <!-- 최대 7일치를 보관하고 나머지는 삭제, 최대 1GB를 초과해도 나머지를 삭제 -->
    <appender name="TEXT_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_FILE_PATH}/${LOG_FILE_NAME}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${LOG_FILE_PATH}/${LOG_FILE_NAME}_%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxFileSize>100MB</maxFileSize>
            <maxHistory>7</maxHistory>
            <totalSizeCap>1GB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <charset>UTF-8</charset>
            <pattern>%d %-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- GELF_UDP 어펜더 정의 -->
    <!-- 로그를 원격지의 Graylog 서버로 전송 -->
    <appender name="GELF_UDP" class="biz.paluch.logging.gelf.logback.GelfLogbackAppender">
        <host>udp:{hostname}</host>
        <port>{port}</port>
        <version>1.1</version>
        <extractStackTrace>true</extractStackTrace>
        <filterStackTrace>true</filterStackTrace>
        <mdcProfiling>true</mdcProfiling>
        <timestampPattern>yyyy-MM-dd HH:mm:ss,SSSS</timestampPattern>
        <includeFullMdc>true</includeFullMdc>
        <additionalFields>appender=GELF_UDP</additionalFields>
        <additionalFieldTypes>appender=String</additionalFieldTypes>
        <maximumMessageSize>8192</maximumMessageSize>
    </appender>

    <!-- 로거 이름이 GELF_UDP로 발생된 모든 INFO 레벨 이하 로그를 GELF_UDP 어펜더로 로그 전송 -->
    <!-- 다른 어펜더에는 적재되지 않도록 제한 -->
    <logger name="GELF_UDP" level="INFO" additivity="false">
        <appender-ref ref="GELF_UDP"/>
    </logger>

    <!-- INFO 레벨 이하 로그는 TEXT_FILE 어펜더로 로그 적재 -->
    <root level="INFO">
        <appender-ref ref="TEXT_FILE"/>
    </root>

</configuration>

로그 사용 예

  • 애플리케이션 소스 코드 내에서의 로그 사용 예는 아래와 같다.
// 방법 1. 클래스 외부에 로거 선언
private val logger = KLogging()

class Foo {

    // 방법 2. 클래스 내부에 로거 선언
    companion object : KLogging()

    // 방법 3. (앞서 logback.xml에서 정의한) 특정 로거 이름을 지정하여 로거 선언
    companion object : NamedKLogging("TEXT_CONSOLE")

    fun bar() {

        val message = "로그 메시지 테스트"

        logger.info("정상 로그: $message")
        logger.warn("경고 로그: $message")
        logger.error("오류 로그: $message")
    }
}

추가로 읽어볼만한 글

참고 글