본문 바로가기

우아한테크코스/프로젝트

Logback, Error 로그 Slack 알림 받기

팀 프로젝트에서 Logback 라이브러리로 로그를 관리한다.

기존에는 slf4j로 로그를 찍으면 EC2 인스턴스의 log파일에 저장하는 방식으로 설정해뒀었다.

이렇게 로그 관리를 하니 문제점이 많았다. 서비스를 운영하면서 error가 터질 때마다 ec2 인스턴스에 직접 접근해서 log파일을 뒤져야하는 번거로운 과정을 거쳐야했다. 또 실시간으로 error를 빨리 확인할 수 없었다.

매번 서비스가 터진 걸 모르고있다가 팀원 중 누군가 우연히 들어갔을 때 서비스가 터졌음을 확인했다. 그럼 인스턴스에 접근해서 log파일을 뒤져서 왜 에러가 났는지 확인하고, 고치고.. 이런 과정을 반복했다.

이러한 문제점을 해결하기 위해 Error가 터지면, 실시간으로 Error 로그를 Slack 메신저로 알림을 받는 환경을 구축했다.

이 글은 팀원들에게 환경을 구축한 방법을 공유하기 위해 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 레벨의 로그가 찍히면 아래와 같이 메세지가 오게 됩니다!

'우아한테크코스 > 프로젝트' 카테고리의 다른 글

instagram 데이터를 수집하며 겪었던 문제들  (13) 2020.10.04
Multi Module  (0) 2020.09.21
Logback 이해하기  (2) 2020.07.29