https://velog.io/@dooo_it_ly/SpringBoot-AOP로-Exception-감지-WebHook으로-디스코드-알림-연동하기
👆👆 이전 글에서 AOP와 WebHook을 이용해 디스코드 알림을 연동해보았습니다. 이번에는 로깅 라이브러리 중 하나이며 스프링에서 사용되는 Logback을 커스텀하여 알림을 전송해보겠습니다.
디스코드 연동 과정은 기존과 동일합니다.
알림을 보낼 채널에 web hook을 연동해줍니다.
logging:
discord:
web-hook-url: ${DISCORD_WEB_HOOK_URL}
config: classpath:logback-spring.xml
implementation ("com.github.napstr:logback-discord-appender:1.0.0")
profile이 local일 때 나오는 에러는 콘솔에, dev일 때 나오는 에러는 디스코드와 콘솔에 출력하고자 합니다.
resources
폴더에 logback-spring.xml
파일을 생성하고, 각 profile일 때의 로그 appender을 지정해봅시다
<configuration>
<!--local log-->
<springProfile name="local">
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<Pattern>%d{HH:mm:ss.SSS} %highlight(%-5level) [%t] %logger{36} - %msg%n</Pattern>
<charset>utf8</charset>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
<!--dev log-->
<springProfile name="dev">
<property resource="application-secret.yml"/>
<springProperty name="DISCORD_WEBHOOK_URL" source="logging.discord.web-hook-url"/>
<!--디스코드로 알림을 보내기 위한 appender-->
<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}] [%-5level] %logger{36} %n %msg%n```%ex{full}```</pattern>
</layout>
<username>[LearnMate]DEV SERVER ERROR</username>
<tts>false</tts>
</appender>
<!--디스코드 appender를 비동기로 처리하기 위한 상위 appender-->
<appender name="ASYNC_DISCORD" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="DISCORD" />
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!--로그가 ERROR LEVEL일 때만 디스코드 알림을 전송함-->
<level>ERROR</level>
</filter>
</appender>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<Pattern>%d{HH:mm:ss.SSS} %highlight(%-5level) [%t] %logger{36} - %msg%n</Pattern>
<charset>utf8</charset>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="ASYNC_DISCORD"/>
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
</configuration>
예외가 발생하면, 디스코드에 이렇게 알림이 옵니다. 단순히 발생할 로그의 형태를 지정한 후 그대로 알림을 전송하는거라, 어떤 요청으로 인해 에러가 발생한 건지 등의 부가 정보는 알 수 없어서 아쉽습니다.
요청과 응답에 대한 부가 정보를 더 가져올 수 있도록 커스텀을 추가해봅시다.
MDC는 로깅 라이브러리의 기능으로, 현재 실행 중인 쓰레드의 메타 정보를 관리할 수 있는 공간입니다.
OncePerRequestFilter
단에서 Request, Response 정보를 받아낸 후, 필요한 값들을 MDC
라는 컨텍스트에 저장해둡니다.
나중에 알림을 보낼 때 로그 정보와 저장해둔 이 정보들을 함께 조합해서 보낼 것입니다.
@Component
class RequestLoggingFilter: OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
// request 정보를 MDC 컨텍스트에 저장
MDC.put("requestURI", request.requestURL.toString())
MDC.put("requestMethod", request.method)
MDC.put("clientIP", request.remoteAddr)
try {
filterChain.doFilter(request, response)
} finally {
MDC.clear()
}
}
}
디스코드로 알림을 보낼 메세지 레이아웃을 생성합니다.
2번의 xml 커스텀은 레이아웃을 따로 만들지 못했어서 아쉬움이 있었는데, 이 아쉬움을 충족시키고자 레이아웃 커스텀을 시도하게 되었습니다.
LayoutBase<ILoggingEvent>()
이라는 Logback 라이브러리의 클래스를 상속해서 레이아웃을 커스텀 할 것이며, 이는 로그 이벤트를 받아 최종 문자열을 반환하는 클래스입니다.
class DiscordErrorLayout: LayoutBase<ILoggingEvent>() {
override fun doLayout(event: ILoggingEvent?): String {
val sb = StringBuilder()
val timestamp = LocalDateTime.ofInstant(Instant.ofEpochMilli(event!!.timeStamp), ZoneId.systemDefault())
sb.append("🚨 **서버 에러 발생** 🚨\n\n")
sb.append("**에러 정보**\n")
sb.append("```\n")
sb.append(event.formattedMessage)
sb.append("\n```\n")
sb.append("**에러 발생 시간**\n")
sb.append("`").append(timestamp.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH시 mm분 ss초"))).append("`\n\n")
sb.append("**요청 URL**\n")
sb.append("`[")
.append(event.mdcPropertyMap["requestMethod"])
.append("] ")
.append(event.mdcPropertyMap["requestURL"] ?: "")
.append("`\n\n")
sb.append("**요청 클라이언트**\n")
sb.append("`[IP]: ").append(event.mdcPropertyMap["clientIP"] ?: "").append("`\n\n")
if (event.throwableProxy != null) {
sb.append("**에러 스택 트레이스**\n")
sb.append("```\n")
sb.append(ThrowableProxyUtil.asString(event.throwableProxy))
sb.append("\n```")
}
return sb.toString()
}
}
아래와 같이 디스코드 appender의 layout을 위에서 생성한 layout의 클래스 명으로 변경해줍니다.
<!--dev log-->
<!-- 1. 디스코드에 보낼 로그용 로거 이름 명시 -->
<logger name="Logger" additivity="false" level="INFO">
<appender-ref ref="ASYNC_DISCORD"/>
</logger>
<springProfile name="dev">
<property resource="application-secret.yml"/>
<springProperty name="DISCORD_WEBHOOK_URL" source="logging.discord.web-hook-url"/>
<appender name="DISCORD" class="com.github.napstr.logback.DiscordAppender">
<webhookUri>${DISCORD_WEBHOOK_URL}</webhookUri>
<!-- 2. layout 변경 -->
<layout class="learn_mate_it.dev.common.log.dto.DiscordErrorLayout" />
<username>[LearnMate]DEV SERVER ERROR</username>
<tts>false</tts>
</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>
...
3번의 xml에서 명시한 Logger 로거를 불러와, 디스코드로 알림을 보낼 곳에 error 로그를 찍어줍니다.
private val discordLog = LoggerFactory.getLogger("Logger")
아래와 같이 레이아웃이 예쁘게 적용된 것을 확인할 수 있습니다.
Reference
https://leeeeeyeon-dev.tistory.com/3
https://velog.io/@cjh8746/Log-Back-%EC%A0%81%EC%9A%A9%EA%B8%B0
https://tech.kakaopay.com/post/podo-elk-threadcontext-part-1/#requestloggingfilter%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-request-%EC%9A%94%EC%B2%AD-%EB%A1%9C%EA%B9%85