eelseungmin

Slack으로 Logback 에러 로그 수집하기

by eelseungmin

도입 배경

서비스 운영 시에 갑자기 API 서버에서 500번대에 해당하는 응답이나 Error 레벨의 로그가 발생했다고 가정해 보았다.

아무런 대비가 되어있지 않다면 개발자는 에러가 발생한 줄도 모르고 있을 가능성이 높다. 이 사실을 알게 되었다 하더라도 에러와 관련된 로그를 찾기 위해 로그 파일을 일일이 열어서 찾아야 한다면 참 막막할 것 같다.

따라서 개발자가 에러 발생 시 적절히 대처할 수 있도록 Error 레벨의 로그가 기록되면 곧바로 설정한 Slack 채널로 관련 로그가 송신되도록 적용했고, 해당 과정을 소개하겠다.

 

과정

조사 결과 Slack에서 제공하는 API를 호출하는 방법과 maricn이라는 깃헙 유저가 개발한 SlackAppender를 사용하는 방법이 있었다. 사용법은 둘 다 어렵지 않으나 코드 상 구현 없이 좀 더 빠르게 적용할 수 있는 후자를 선택했다.

 

Slack 채널에 앱 추가

Slack에서 더보기 - 자동화 - 앱 순서로 들어간 뒤 검색 창에 incoming webhooks를 검색하면 위와 같은 앱이 나온다. 구성을 클릭한다.

 

에러 로그가 수신되길 원하는 채널을 선택하고 앱을 추가한다.

 

이어서 원하는 이름과 아이콘을 지정해 주면 된다. 이때 웹훅 URL은 프로젝트에 사용할 것이니 복사해 두자.

 

의존성 추가

// build.gardle
implementation group: 'com.github.maricn', name: 'logback-slack-appender', version: '1.6.0'

 

logback-spring.xml

현재 프로젝트에서는 profile 별로 로깅을 다르게 하고 있으므로 다음과 같이 적용할 수 있다.

<configuration>
    <!-- defaults.xml -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>

    <!-- log 파일 저장 위치 설정 -->
    <property name="LOG_PATH" value="./log"/>
    <!-- console 로그 패턴 -->
    <property name="CONSOLE_LOG_PATTERN"
              value="[%X{request_id:-internal}] [%d{yyyy-MM-dd HH:mm:ss}:%-3relative] %clr(%-5level) %magenta(${PID:-}) --- [%thread] %cyan(%-40.40logger{36}) : %msg%n"/>
    <!-- file 로그 패턴 -->
    <property name="FILE_LOG_PATTERN"
              value="[%X{request_id:-internal}] [%d{yyyy-MM-dd HH:mm:ss}:%-3relative] %-5level ${PID:-} --- [%thread] %-40.40logger{36} : %msg%n"/>

    <!-- 슬랙으로 Error 레벨 로그 전송 -->
    <appender name="SLACK" class="com.github.maricn.logback.SlackAppender">
        <!-- Slack 웹훅 URL -->
        <webhookUri>https://hooks.slack.com/services/{생략}</webhookUri>
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </layout>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <!-- 슬랙에 전송할 로그 레벨 지정 -->
            <level>ERROR</level>
        </filter>
        <colorCoding>true</colorCoding>
    </appender>

    <!-- console -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
        </encoder>
    </appender>

    <!-- file -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- log 파일 저장 위치 설정 -->
        <file>${LOG_PATH}/modu-menu.log</file>
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </encoder>
        <!-- 날짜를 기준으로 log 파일을 변경 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/modu-menu.%d{yyyy-MM-dd}.log</fileNamePattern>
            <!-- 아카이브 로그 파일 최대 저장 갯수, 초과시 오래된 파일부터 자동으로 삭제 -->
            <maxHistory>30</maxHistory>
            <!-- 아카이브 최대 크기, 초과시 오래된 파일부터 자동으로 삭제 -->
            <totalSizeCap>1GB</totalSizeCap>
        </rollingPolicy>
    </appender>

    <springProfile name="test">
        <property resource="application-test.yml"/>
        <logger name="modu.menu" level="debug">
            <appender-ref ref="CONSOLE"/>
        </logger>
        <!-- 쿼리 파라미터 출력 -->
        <logger name="org.hibernate.type.descriptor.sql" level="trace">
            <appender-ref ref="CONSOLE"/>
        </logger>
    </springProfile>

    <springProfile name="dev">
        <property resource="application-dev.yml"/>
        <logger name="modu.menu" level="debug">
            <appender-ref ref="CONSOLE"/>
            <appender-ref ref="SLACK"/>
        </logger>
        <!-- 쿼리 파라미터 출력 -->
        <logger name="org.hibernate.type.descriptor.sql" level="trace">
            <appender-ref ref="CONSOLE"/>
        </logger>
    </springProfile>

    <springProfile name="prod">
        <property resource="application-prod.yml"/>
        <logger name="modu.menu" level="debug">
            <appender-ref ref="CONSOLE"/>
        </logger>
        <logger name="modu.menu" level="info">
            <appender-ref ref="FILE"/>
            <appender-ref ref="SLACK"/>
        </logger>
    </springProfile>
</configuration>

FILE_LOG_PATTERN의 경우 해당 XML 내부에서 여러 번 사용할 로깅 패턴이라서 property로 따로 빼주었다. 로깅 패턴의 경우 각자 상황에 맞게 수정해서 사용하면 된다.

webhookUri태그 안에는 아까 복사했던 URL을 집어넣으면 된다.

Error 레벨의 로그를 Slack에 보낼 것이기 때문에 level 태그는 Error로 설정해 주었다.

colorCoding은 로그 레벨에 따라 메시지 주변부를 다른 색으로 칠해주는 설정인데, 어차피 하나의 레벨만 수집할 것이기 때문에 false로 설정해도 괜찮지만 색깔이 들어가는 게 더 보기 좋아서 설정을 켜두었다.

 

테스트

@RestController
public class SlackErrorLogTestController {

    @GetMapping("/api/slack")
    public String getSlackErrorLog() {
        throw new Exception500(ErrorMessage.ERROR_LOG_TEST_MESSAGE);
    }
}

테스트를 목적으로 커스텀 예외를 던지는 간단한 Controller를 작성하고 Postman을 통해 호출했다.

 

메시지가 잘 전송되었다.

 

적용해 보니 조금 아쉬운 점은 XML 설정을 통해 메시지 구조를 입맛대로 고치기 어렵다는 것인데, Slack API를 사용하고 코드로 구현하면 이 점을 해결할 수 있긴 하다.

다만 현재 구조도 알아보기 어렵진 않다고 판단되어, 추후 관련된 피드백이 나오지 않으면 이대로 적용하고자 한다.

 

참조

https://github.com/maricn/logback-slack-appender?tab=readme-ov-file

블로그의 정보

eel.log

eelseungmin

활동하기