프로젝트를 진행하다보면 여러 가지 에러를 마주하게 된다. 처리해준 예외들에 대해서는 괜찮지만, 미처 처리하지 못한 부분에서 에러가 발생하면 그건 곧 서비스 전체의 장애로 이어질 수 있다. 이런 상황을 피할 수는 없다. 인간은 완벽한 존재가 아니니까. 다만, 빠르게 대응할 수는 있다. 그래서 어떻게 하면 빠르게 대응할 수 있을까 고민하다가, 협업툴로 사용하는 slack과 연동하여 에러 로그를 남기는 설정을 진행했다(연동한지는 꽤 시간이 지났지만 이제서야 글을 쓴다). 그리고 그 과정을 공유하려고 한다.
위 사진처럼 Spring Boot 서버와 연동할 채널(본 글에서는 error-log-sample)을 하나 만들거나 선정한다.
위 사진에 표시된 부분의 앱 추가 버튼을 누르면,
요런 화면이 나오는데, 우리가 사용할 것은 Incoming WebHooks이다.
클릭 클릭...하면 어렵지 않게 해당 앱을 slack에 추가할 수 있다. 이때 웹훅을 통해 메시지를 받을 채널을 알맞게 선택해주면 된다.
다음으로는 이런 화면을 마주하게 될텐데, 여기서 WebHooks URL을 이어서 있을 Spring Boot의 logback url에 설정해주면 된다. 잘 복사해두도록 하고 설정을 저장하도록 한다.
먼저, 위 사진과 같이 build.gradle에 logback의 slack 관련 의존성을 추가해준다.
그 다음으로는, application.yml에 slack 설정을 진행하면서 복사해둔 WebHook Url을 설정해준다.
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<springProperty name="filePath" source="logging.file.path"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level [File:%F] [Func:%M] [Line:%L] [Message:%m] %n</pattern>
</encoder>
</appender>
<appender name="APP_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/app.%d{yyyy-MM-dd}.%i.gz</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level [File:%F] [Func:%M] [Line:%L] [Message:%m] %n</pattern>
</encoder>
</appender>
<appender name="WARN_FILE" 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>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/warn.%d{yyyy-MM-dd}.%i.gz</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level [File:%F] [Func:%M] [Line:%L] [Message:%m] %n</pattern>
</encoder>
</appender>
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/error.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/error-%d{yyyy-MM-dd}.%i.gz</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level [File:%F] [Func:%M] [Line:%L] [Message:%m] %n</pattern>
</encoder>
</appender>
<!-- slack -->
<springProperty name="SLACK_WEBHOOK_URI" source="logging.slack.webhook-url"/>
<appender name="SLACK" class="com.github.maricn.logback.SlackAppender">
<webhookUri>${SLACK_WEBHOOK_URI}</webhookUri>
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %msg %n</pattern>
</layout>
<username>server-error-log</username>
<iconEmoji>:anger:</iconEmoji>
<colorCoding>true</colorCoding>
</appender>
<appender name="ASYNC_SLACK" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="SLACK"/>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
<logger name="com.example.demo" level="INFO">
<appender-ref ref="APP_FILE"/>
<appender-ref ref="WARN_FILE"/>
<appender-ref ref="ERROR_FILE"/>
<appender-ref ref="ASYNC_SLACK"/>
</logger>
</configuration>
이제 resources 디렉토리 내에 logback-spring.xml 파일을 만들고 logback 설정을 진행한다(설정이 미흡해도 이해해주길 바란다). 각각의 설정에 대해 자세한 내용은 참고자료를 살펴보거나 구글링해보길 바란다.
slack appender 설정에 대한 자세한 내용은 logback-slack-appender를 참고하도록 한다(아래의 코드 블록은 logback-slack-appender repository의 README.md에 있는 것을 그대로 가져온 것이다).
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
...
<appender name="SLACK" class="com.github.maricn.logback.SlackAppender">
<!-- Slack API token -->
<token>1111111111-1111111-11111111-111111111</token>
<!-- Slack incoming webhook uri. Uncomment the lines below to use incoming webhook uri instead of API token. -->
<!--
<webhookUri>https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX</webhookUri>
-->
<!-- Channel that you want to post - default is #general -->
<channel>#api-test</channel>
<!-- Formatting (you can use Slack formatting - URL links, code formatting, etc.) -->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%-4relative [%thread] %-5level %class - %msg%n</pattern>
</layout>
<!-- Username of the messages sender -->
<username>${HOSTNAME}</username>
<!-- Emoji to be used for messages -->
<iconEmoji>:stuck_out_tongue_winking_eye:</iconEmoji>
<!-- If color coding of log levels should be used -->
<colorCoding>true</colorCoding>
</appender>
<!-- Currently recommended way of using Slack appender -->
<appender name="ASYNC_SLACK" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="SLACK" />
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<root>
<level value="ALL" />
<appender-ref ref="ASYNC_SLACK" />
</root>
</configuration>
이제 Controller를 간단하게 작성해서 제대로 동작하는지 확인해본다.
두번째 사진은 인텔리제이에서 제공해주는 http-client-in-product-code-editor라는 것으로, 간단하게 HTTP 요청을 만들고 보낼 수 있다.
요청을 보내면!
위 사진과 같이 log가 채널에 메시지로 날아오는 것을 확인할 수 있다. 현재는 error 로그만 메시지로 날아오게끔 설정해두었기 때문에 error 로그만 보인다.