Logback, Error 로그 Slack 알림 받기 - woowacourse-teams/2020-songpa-people GitHub Wiki
Logback 설정으로 인스턴스에 찍히는 로그를 Slack으로 알림을 받으려면 우선 logback-slack-appender 의존성을 추가해줘야 합니다.
우리 프로젝트는 web 모듈과 admin 모듈에 logback-slack-appender 의존성을 추가 해줘야 합니다. 똑같은 의존성, 똑같은 코드가 중복으로 각 모듈에 있으면 안되겠죠? 한 곳에서 한 번에 적용시켜주기 위해 루트 build.gradle에 다음과 같은 코드를 작성했습니다.
def logbackSlack = [project(':hashtagmap-web'), project(':hashtagmap-admin')]
configure(logbackSlack) {
ext {
set("logbackSlackAppenderVersion", "1.4.0")
}
dependencies {
implementation "com.github.maricn:logback-slack-appender:${logbackSlackAppenderVersion}"
}
}
예전에 ascidoctor 의존성도 같은 방식으로 web 모듈과 admin 모듈에 적용시켜줬었습니다. 이렇게 해서 web 모듈과 admin 모듈에 logback-slack-appender 의존성이 추가되었습니다.
이제 slf4j로 발생시킨 로그를 슬랙으로 보내주는 설정을 추가해줘야 하는데 이 과정에서 로그를 보낼 slack의 링크, 즉 slack web hook URL이 필요합니다. 젠킨스할 때 hook 기억나시죠? 낚시할 때 물고기가 물면 물고기 입천장에 바늘을 제대로 걸기 위해 낚시대를 힘차게 당기는 것을 훅킹이라고 하는데 비슷한 맥락입니다. 로그를 가로채서 던져줄 slack url이 필요한겁니다. 물고기를 바늘로 가로채듯이 말이죠.
슬랙 메세지를 보내고 싶은 workspace에 아무 채널이나 들어가시면 아래 이미지처럼 오른쪽 상단에 느낌표가 있습니다. 그걸 클릭합니다.
...으로 되어있는 More을 클릭
Add apps 클릭
첫 번째로 검색창에 webhook이라고 검색해주시고 Incomming WebHooks를 Install해주시면 됩니다.
install을 누르면 위 이미지와 같은 링크로 이동하는 데 Add to Slack을 클릭해주면 됩니다.
알림을 보낼 채널을 선택합니다.
채널을 선택하셨으면 저 초록 버튼을 눌러주시면 됩니다.
초록 버튼을 누르면 위 이미지와 같은 화면으로 바뀌는데 저기서 Webhook URL을 복사해줍니다. 복사한 URL을 날리셨어도 걱정마세요. 다시 볼 수 있습니다
복사하셨으면 아래로 쭉쭉 내리셔서 Save Setting을 눌러주시면 됩니다. 다른 설정을 하시고 싶으면 하셔도 되는데 제가 봤을 땐 의미있는 설정은 없는 것 같아요. 우리에게 필요한건 web hook url 입니다.
이렇게 해서 slack에 원하는 workspace와 channel에 web hook url을 만들어 주면 slack 설정은 끝입니다.
그 다음엔 logback-spring.xml에 logback-slack-appender 관련 설정을 추가해줘야 합니다.
저는 logback-slack-appender github를 참고해서 logback-spring.xml에 설정을 추가했습니다.
<appender name="SLACK" class="com.github.maricn.logback.SlackAppender">
<webhookUri>https://hooks.slack.com/services/T015ELYER/B0198TVKM/zDq5yDQWnQAeVaFXQF6763</webhookUri>
<channel>#prod-admin</channel>
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>${LOG_PATTERN}</pattern>
</layout>
<username>${HOSTNAME}</username>
<iconEmoji>:bright-cow:</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="ASYNC_SLACK"/>
</root>
appender 태그는 로그를 어떻게 처리할 지 나타내는 태그입니다.
처음에 name이 SLACK인 appender에서 설정해준 태그들을 눈으로 쭉 읽어보시면 이해가 될 겁니다.
webhookUri는 앞서 봤던 알림을 낚아채서 날려줄 uri를 설정해주는 부분입니다. 위 예제의 uri는 유효하지 않은 uri입니다.
channel은 알림을 보낼 채널명을 적어줍니다.
layout과 pattern은 로그 메세지 형식을 정해주는 것입니다. 전에 정의해뒀던 LOG_PATTERN을 그대로 사용했습니다.
username은 슬랙 메세지를 누가 보냈는지를 지정해주는 겁니다. 특이한 점은 ${HOSTNAME}에서 HOSTNAME이라는 변수를 따로 정의해주지 않아도 아래의 이미지처럼 에러가 발생한 인스턴스의 호스트 네임을 받아와서 출력해줍니다.
icomEmoji도 위 그림에서 보내는 이의 프로필을 명소로 설정했듯이 보내는 이의 프로필 이모지를 정해줄 수 있는 태그입니다.
colorCoding은 true로 해뒀는데 적용이 안되는것 같아요. 애초에 슬랙에서 메세지 색을 바꿀 수 없지 않나요?..
그 다음 ASYNC_SLACK appender는 SLACK appender를 참조해서 ERROR 레벨의 로그만 걸러서 알림을 보내도록 설정해줍니다!
그 후 root에 ASYNC_SLACK 설정을 적용해주면 끝납니다.
이렇게 까지만 해도 로그는 슬랙으로 아주 잘 날라옵니다.
여기에 slack web hook uri를 서브 모듈인 secret 모듈에 숨기고 싶다는 생각이 들어서 추가 작업을 해줬습니다.
slack web hook uri를 사실 굳이 숨길 필요는 없지만 노출하면 누군가 우리 슬랙 채널에 맘대로 접근해서 메세지를 보낼 수 있기 때문에 숨기는 게 안 숨기는 것 보단 낫겠다 판단했습니다.
우선 secret 모듈에 slack/application-slack-web-hook.yml 파일을 만들고 아래와 같이 정의 해줬습니다.
web모듈과 admin모듈에서 secret모듈에 정의한 application-slack-web-hook.yml 의 property를 읽어오기 위해선 해당 모듈에 application-slack-web-hook.yml 를 copy 해줘야 합니다. 그래서 아까 작성한 루트 build.gradle에 아래와 같은 task를 추가로 정의해줍니다.
def logbackSlack = [project(':hashtagmap-web'), project(':hashtagmap-admin')]
configure(logbackSlack) {
ext {
set("logbackSlackAppenderVersion", "1.4.0")
}
dependencies {
implementation "com.github.maricn:logback-slack-appender:${logbackSlackAppenderVersion}"
}
task copySlackWebHook(type: Copy) {
description = "Copy slack web hook uri from hashtagmap-secret"
from '../hashtagmap-secret/slack/application-slack-web-hook.yml'
into 'src/main/resources/'
}
processResources.dependsOn 'copySlackWebHook'
}
merge된 코드는 web 모듈과 admin 모듈에서 각각 copy task를 추가해줬었는데 루트 build.gradle에서 한 번에 추가해주는 걸로 수정하겠습니다.
processResources — Copy
Copies production resources into the production resources directory.
processResources.dependsOn 'copySlackWebHook'은 task copySlackWebHook에서 copy한 결과물을 resource 폴더에 복사하라는 의미입니다. task에서 into 속성으로 resource 폴더가 아닌 다른 폴더로 지정해줄 수도 있습니다.
예전에 작성했던 아래의 kakao api key를 복사한 task에서 그 예를 볼 수 있습니다.
task copyKakaoApiKey(type: Copy) {
description = "Copy kakao map api key from hashtagmap-secret"
from '../hashtagmap-secret/kakao/index.js'
into 'front/src/secret/'
}
이렇게 build.gradle에 copy task를 만들어주면 해당 모듈에서 build를 할 때
아래와 같이 secret 모듈의 application-slack-web-hook.yml 이 copy 됩니다.
copy한 application-slack-web-hook.yml 이 github에 올라가면 secret모듈에 숨긴 의미가 없기 때문에 gitignore에 아래와 같이 추가해 줬습니다.
/hashtagmap-web/src/main/resources/application-slack-web-hook.yml
/hashtagmap-admin/src/main/resources/application-slack-web-hook.yml
이제 다시 logback-spring.xml로 돌아와서 application-slack-web-hook.yml에 정의한 property를 불러오는 코드를 추가해줍니다.
우선 application-slack-web-hook.yml 파일을 아래와 같이 불러와줍니다.
<property resource="application-slack-web-hook.yml" />
외부 파일을 property로 등록하는 방법 중 되는 방법을 찾는데 애를 먹었습니다.
그렇게 stackoverflow를 뒤져서 겨우 찾아낸 되는 방법이 저property resource입니다.
그 후 등록한 property로 webhookUri 태그를 대체해줍니다.
<appender name="SLACK" class="com.github.maricn.logback.SlackAppender">
<webhookUri>${prod-web}</webhookUri>
...
</appender>
또 해맸던 부분이 .yml 파일로 등록하게 되면 ${prod-web}으로 property를 불러와줘야하고 .properties 파일로 등록을 하면 ${uri.prod-web}으로 불러와줘야 합니다.
이렇게 전에 해준 logback-spring.xml 설정에 slack 알림 관련 모든 설정을 추가해준 최종 logback-spring.xml의 코드는 아래와 같습니다.
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<timestamp key="BY_DATE" datePattern="yyyy-MM-dd"/>
<property name="LOG_PARENT_PATH" value="../log"/>
<property name="LOG_CHILD_INFO" value="info"/>
<property name="LOG_CHILD_WARN" value="warn"/>
<property name="LOG_CHILD_ERROR" value="error"/>
<property name="LOG_BACKUP" value="../log/backup"/>
<property name="MAX_HISTORY" value="30"/>
<property name="LOG_PATTERN"
value="[%d{yyyy-MM-dd HH:mm:ss}:%-4relative] %green([%thread]) %highlight(%-5level) [%C.%M:%line] - %msg%n"/>
<property resource="application-slack-web-hook.yml" />
<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="SLACK" class="com.github.maricn.logback.SlackAppender">
<webhookUri>${prod-web}</webhookUri>
<channel>#prod-web</channel>
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>${LOG_PATTERN}</pattern>
</layout>
<username>${HOSTNAME}</username>
<iconEmoji>:bright-cow:</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>
<appender name="FILE-INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PARENT_PATH}/${LOG_CHILD_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.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_BACKUP}/${LOG_CHILD_INFO}/info-%d{yyyy-MM-dd}.zip</fileNamePattern>
<maxHistory>${MAX_HISTORY}</maxHistory>
</rollingPolicy>
</appender>
<appender name="FILE-WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PARENT_PATH}/${LOG_CHILD_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.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_BACKUP}/${LOG_CHILD_WARN}/warn-%d{yyyy-MM-dd}.zip</fileNamePattern>
<maxHistory>${MAX_HISTORY}</maxHistory>
</rollingPolicy>
</appender>
<appender name="FILE-ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PARENT_PATH}/${LOG_CHILD_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.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_BACKUP}/${LOG_CHILD_ERROR}/error-%d{yyyy-MM-dd}.zip</fileNamePattern>
<maxHistory>${MAX_HISTORY}</maxHistory>
</rollingPolicy>
</appender>
<root level="INFO">
<appender-ref ref="FILE-INFO"/>
<appender-ref ref="FILE-WARN"/>
<appender-ref ref="FILE-ERROR"/>
<appender-ref ref="ASYNC_SLACK"/>
</root>
</springProfile>
</configuration>
이제 prod 환경에서 ERROR 레벨의 로그가 찍히면 아래와 같이 메세지가 오게 됩니다!