[SpringBoot] Logback 커스텀해 디스코드 알림 전송하기

다은·2025년 9월 1일
0

SpringBoot

목록 보기
11/12
post-thumbnail

https://velog.io/@dooo_it_ly/SpringBoot-AOP로-Exception-감지-WebHook으로-디스코드-알림-연동하기

👆👆 이전 글에서 AOP와 WebHook을 이용해 디스코드 알림을 연동해보았습니다. 이번에는 로깅 라이브러리 중 하나이며 스프링에서 사용되는 Logback을 커스텀하여 알림을 전송해보겠습니다.


1. 디스코드 연동

디스코드 연동 과정은 기존과 동일합니다.

1. WebHook URL 생성

알림을 보낼 채널에 web hook을 연동해줍니다.


2. 환경변수 설정

logging:
  discord:
    web-hook-url: ${DISCORD_WEB_HOOK_URL}
  config: classpath:logback-spring.xml

3. 의존성 추가

	implementation ("com.github.napstr:logback-discord-appender:1.0.0")



2. Logback xml 커스텀

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>

예외가 발생하면, 디스코드에 이렇게 알림이 옵니다. 단순히 발생할 로그의 형태를 지정한 후 그대로 알림을 전송하는거라, 어떤 요청으로 인해 에러가 발생한 건지 등의 부가 정보는 알 수 없어서 아쉽습니다.



3. Logback 커스텀하기

요청과 응답에 대한 부가 정보를 더 가져올 수 있도록 커스텀을 추가해봅시다.

1. Filter

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. Error Layout

디스코드로 알림을 보낼 메세지 레이아웃을 생성합니다.

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()
    }

}

3. Layout 적용

아래와 같이 디스코드 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>
        
        ...

4. 테스트

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

profile
CS 마스터를 향해 ..

0개의 댓글