logback과 Sentry로 에러 모니터링하기
들어가며
현재 진행중인 프로젝트에서는 logback과 팀 내 협업 툴인 디스코드를 연동하여 애플리케이션 단에서 에러 로그가 발생하면 디스코드로 알림이 오도록 환경을 구축하였다.
빠르게 에러를 파악할 수 있다는 장점이 있지만, 디스코드는 메신저 툴이기 때문에 로그를 관리하기 어렵다는 단점이 있다. 그렇기 때문에 에러 트래킹 및 성능 모니터링 도구로 유명한 Sentry를 프로젝트에 도입하여 로그를 보다 체계적으로 관리하고자 한다.
참고로 Sentry는 완전 무료 서비스가 아니다. 무료 Plan을 제공하긴 하나 사용할 수 있는 기능이 한정적이다.
- 멤버 수 제한 - 유료 사용의 경우 무제한
- 에러 수 제한(월 5,000개) - 유료 사용의 경우 100,000개 이상
- 히스토리 제한(30일) - 유료 사용의 경우 90일
주의 !!!
Developer Plan 아래 있는 GET STARTED를 눌러 프로젝트를 생성해주자.
처음에 신경 쓰지 않고 바로 프로젝트를 만드니 Business Free Trial 플랜으로 등록되어 있었고, Plan 중도 변경이 되지 않아 프로젝트를 삭제하고 다시 만들어주었다 ;ㅅ;
Sentry로 에러 로그를 수집할 때 아래 코드처럼 원하는 에러에 대해서만 전용 예외를 던지는 방법도 있지만, 이번 포스트에서는 전용 예외 코드를 사용하지 않고 logback을 활용한 방법을 다뤄보겠다.
import io.sentry.Sentry;
try {
aMethodThatMightFail();
} catch(Exception e) {
Sentry.captureException(e);
}
Sentry 프로젝트 생성
플랫폼, 알람 설정, 이름을 설정하여 프로젝트를 생성해주자.
나는 logback을 활용해줄 것이기 때문에 Server 탭에서 logback을 선택하였다.
나는 디스코드로 에러 알림을 따로 받기 때문에 알림 설정은 하지 않았다.
의존성 추가
build.gradle에 아래와 같이 의존성을 추가해주자.
// sentry
implementation 'io.sentry:sentry-logback:6.19.0'
Sentry 설정 정보 작성
공식 문서에 따르면
- properties 파일 작성
- 자바 시스템 프로퍼티에 작성
- 환경 변수에 작성
- 코드로 작성
이렇게 4가지 방법을 제공하고 있는데, 나는 그 중 설정 파일에 작성하는 방식을 사용해보겠다.
자바 시스템 프로퍼티 vs 환경 변수
자바 시스템 프로퍼티는 자바 커맨드 라인에 의해 설정된다 (ex. -DpropertyName=value)
환경 변수는 OS에 의해 설정된다.
resources 디렉토리에 sentry.properties를 만들고 DSN 주소를 작성해준다.
dsn={dsn 주소}
다른 옵션들도 추가할 수 있는데, 나는 기본 정보인 DSN 주소만 작성하였다.
logback.xml
resources 디렉토리 아래에 logback.xml을 만들어준다.
나는 디스코드와 연동할 때 만들어두었기 때문에 생략한다.
logback.xml 설정을 어떻게 구성했는지는 아래 포스트에 기록해두었다.
기존에 있던 logback.xml에 Sentry Appender를 추가해주었다.
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<springProfile name="local">
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
<springProfile name="dev">
<property resource="application-dev.yml"/>
<springProperty name="DISCORD_WEBHOOK_URL" source="logging.discord.webhook-url"/>
<appender name="DISCORD" class="com.github.napstr.logback.DiscordAppender">
<webhookUri>${DISCORD_WEBHOOK_URL}</webhookUri>
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{HH:mm:ss} [%thread] [%-5level] %logger{36} - %msg%n```%ex{full}```</pattern>
</layout>
<username>감자야...에러 났대...</username>
<avatarUrl>https://jjal.today/data/file/gallery/1889155643_NZHvkRLz_e0292b65bb682075bfdb752a4d8f4062f0b7738a.png</avatarUrl>
<tts>false</tts>
</appender>
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
<charset>utf8</charset>
</encoder>
</appender>
<appender name="ASYNC_DISCORD" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="DISCORD" />
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<appender name="Sentry" class="io.sentry.logback.SentryAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="ASYNC_DISCORD"/>
<appender-ref ref="Console"/>
<appender-ref ref="Sentry"/>
</root>
</springProfile>
<springProfile name="prod">
<property resource="application-prod.yml"/>
<springProperty name="DISCORD_WEBHOOK_URL" source="logging.discord.webhook-url"/>
<appender name="DISCORD" class="com.github.napstr.logback.DiscordAppender">
<webhookUri>${DISCORD_WEBHOOK_URL}</webhookUri>
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{HH:mm:ss} [%thread] [%-5level] %logger{36} - %msg%n```%ex{full}```</pattern>
</layout>
<username>감자야...에러 났대...</username>
<avatarUrl>https://jjal.today/data/file/gallery/1889155643_NZHvkRLz_e0292b65bb682075bfdb752a4d8f4062f0b7738a.png</avatarUrl>
<tts>false</tts>
</appender>
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
<charset>utf8</charset>
</encoder>
</appender>
<appender name="ASYNC_DISCORD" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="DISCORD" />
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<appender name="Sentry" class="io.sentry.logback.SentryAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="ASYNC_DISCORD"/>
<appender-ref ref="Console"/>
<appender-ref ref="Sentry"/>
</root>
</springProfile>
</configuration>
결과 확인
API 서버로 에러가 나는 URL을 보냈을 때, 에러가 잘 트래킹되는 모습을 확인할 수 있다.
Issue를 클릭하면 아래 사진처럼 로그에 대해 자세히 확인할 수 있다.
어느 환경(dev, prod)에서 에러가 발생했는지, 어느 서버(blabla-api-01)에서 에러가 발생했는지도 자세히 확인할 수 있다!
현재 로드밸런싱으로 2개의 서버를 운영하고 있고 server_name으로 각 서버를 구분하고 있어 server_name으로 어느 서버에 에러가 발생했는지 확인할 수 있는 것이다.
Stack Trace도 아래처럼 깔끔하게 해준다 :)
마치며
처음에 개발 블로그 글을 참고하며 application.yml에 dsn 주소를 입력하여 Sentry 연동이 되지 않았다.
공식 문서에서 'logback.xml에 dsn 주소가 없다면 sentry.properties에서 dsn 주소를 읽어온다'라는 문구를 보고 application.yml에 기입한 정보를 sentry.properties로 변경해주니 연동이 잘 되었다.
물론 처음부터 공식 문서를 참고하여 하는 것이 베스트이지만 :) 아직은 나의 영어나 개발 실력이 좀 부족하다 ... ㅎㅎ
개발 블로그를 따라하다가 안될 때는 포스트와 나의 개발 환경(버전 정보 등)이 일치하는지 확인해보고, 다른 것 같으면 공식 문서를 참고해보자!
이번 프로젝트에서 버전 차이에 따른 트러블 슈팅을 유독 많이 겪은 것 같은데, 그 덕분에 미약하게나마 공식 문서를 읽는 습관을 키우고 있는 것 같다!
Reference