본문 바로가기

개발/스프링부트

LogBack과 디스코드 봇 사용하기 with Springboot

안녕하세요.  스프링 부트 기반 서버에서 에러 발생 시 LogBack 을 통해서 디스코드 봇이 해당 에러를 채널로 전달하는 기능을 구현해보려고 합니다. 또한 디스코드 뿐만 아니라 슬랙과 텔레그램에 대해서도 다음 시간에 정리할 예정입니다 :)


HTTP REQUEST 가공 필터 구현

제가 디스코드 봇에서 채널로 보낼 메세지에 양식은 아래와 같습니다. 후에 진행할 슬랙과 텔레그램에서도 유사하니 참고하시기 바랍니다.

디스코드 봇 메세지 - 1
디스코드 봇 메세지 - 2

 

위 메세지 내용을 살펴보면 http request에 대한 정보를 필요로 합니다. 그러나 http body 정보는 inputstream으로 한 번 읽으면 다시 사용할 수 없기 때문에 필터에서 한 번 사용하고도 이후에도 사용할 수 있도록 HttpServletRequest 객체를 변경하려고 합니다.

필터에서는 request 객체와 response 객체를 건드릴 수 있기 때문에 필터에서 해당 작업을 진행한다고 생각하시면 될 것 같습니다. 자세한 내용은 필터와 인터셉터 차이에 대해서 공부해 보시면 좋을 것 같습니다 :)

public class HttpFilter extends OncePerRequestFilter {
   private final Logger logger = LoggerFactory.getLogger(HttpFilter.class);

   @Override
   protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
      FilterChain filterChain) throws ServletException, IOException {

      CustomHttpRequest customHttpRequest = new CustomHttpRequest(request);
      MDCUtil.put(customHttpRequest);
      filterChain.doFilter(customHttpRequest, response);
      MDC.clear();

   }
}`

`public class CustomHttpRequest extends HttpServletRequestWrapper {

   private final byte[] body;

   */**
    * Create a new {@code HttpRequest} wrapping the given request object.
    *
    * @param request the request object to be wrapped
    */*
   public CustomHttpRequest(HttpServletRequest request) throws IOException {
      super(request);
      InputStream inputStream = request.getInputStream();
      this.body = StreamUtils.copyToByteArray(inputStream);
   }

   @Override
   public ServletInputStream getInputStream() throws IOException {
      return new CachedServletInputStream(body);
   }
}

필터와 request 구현은 위와 같습니다. 특별히 복잡한 구현은 없고 단순히 body 부분을 위와 같이 byte[]배열에 캐싱해놓고 계속 사용할 수 있도록 바꿔준다고 생각하면 될 것 같습니다.

그리고 MDCUtil에서는 request 내 정보를 꺼내서 MDC에 넣어줍니다. 이때 MDC는 각각의 요청 즉, 쓰레드마다 독립적으로 관리되기 때문에 정보가 섞일 우려는 하지 않으셔도 됩니다 :)


Appender 구현

이번에는 스프링 애플리케이션에서 에러 이벤트가 발생하면 에러 내용이 appender를 통해 디스코드로 전달될 수 있도록 appender 를 구현하도록 하겠습니다.

@RequiredArgsConstructor
public class ErrorAppender extends AppenderBase<ILoggingEvent> {

   private final static boolean LOGGING_DISCORD = true;

   private final static boolean LOGGING_SLACK = true;

   private final static boolean LOGGING_TELEGRAM = true;

   @Override
   protected void append(ILoggingEvent eventObject) {
      if (eventObject.getLevel().isGreaterOrEqual(Level.ERROR)) {
         IThrowableProxy throwableProxy = eventObject.getThrowableProxy();

         if (LOGGING_DISCORD)
            DiscordWebhook.generate(throwableProxy);
         if (LOGGING_SLACK)
            SlackWebhook.generate(throwableProxy);
         if (LOGGING_TELEGRAM)
            TelegramWebhook.generate(throwableProxy);
      }
   }
}
<configuration>
    <appender name="DiscordAppender" class="com.giggal.initask.config.appender.ErrorAppender">
        <!-- myCustomAppender 설정 --></appender>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS}[%-5level] : %msg%n</pattern>
        </encoder>
    </appender>

    <!-- root logger 설정 --><root level="DEBUG">
        <appender-ref ref="DiscordAppender" />
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

appender를 스프링에서 정상적으로 인식하기 위해서는 logback.xml 혹은 logback-spring.xml 내에 설정이 되어있어야 합니다. 위 설정 코드를 보면 기본적으로 logback에 구현되어 있는 콘솔어펜더와 제가 구현한 어펜더 총 두개의 어펜더가 존재하는데요. 제 디스코드 어펜더는 ERROR로그만 처리하게 되어 있고, 콘솔 어펜더는 디버그 레벨 이상의 로그는 모두 처리하게 됩니다.


디스코드 웹 훅 설정

이번에는 디스코드 앱에서 메세지를 정상적으로 수신받기 위해 설정을 해보도록 하겠습니다.

위와 같이 서버를 만들고 서버 설정 > 연동 > 웹후크 로 이동해줍니다.

새 웹후크를 누르면 위와 같이 웹훅을 보낼 수 있는 URL이 생성됩니다!


디스코드 메세지 보내기

이제 메세지를 보내기 위한 준비가 거의 다 됐습니다. 필터에서 MDC 내에 저장해두었던 정보들과 디스코드 웹훅 URL을 통해 메세지를 생성하고 보내보도록 하겠습니다.

public class DiscordWebhook {

   private static Logger logger = (Logger)LoggerFactory.getLogger(DiscordWebhook.class);

   private static final int DISCORD_MAX_LENGTH = 1000;

   private static final String url = "https://discord.com/api/webhooks/1096011716480991262/pARnmPlUEIwDIJ_gfoJQoHnTRyLltnP1aAbiSf7Ht4EIFKZwu-CGend2I2VcpkMhk_-Q";

   public static void generate(IThrowableProxy throwableProxy) {
      DiscordMessage discordMessage;
      DiscordMessage discordError;

      try {
         discordMessage = new DiscordMessage(throwableProxy);
         discordError = new DiscordMessage(
            ThrowableProxyUtil.asString(throwableProxy).substring(0, DISCORD_MAX_LENGTH));
      } catch (JsonProcessingException e) {
         throw new RuntimeException(e);
      }

      AppenderUtil.send(url, discordMessage, logger);
      AppenderUtil.send(url, discordError, logger);
   }
}

디스코드 웹 훅에 보낼 양식은 이 곳에서 확인할 수 있습니다.에러 로그의 내용이 길기도 하고, 디스코드 메세지 자체 길이 제한도 있다보니 두 번에 나누어 전송하도록 구현했습니다.

public class AppenderUtil {
   public static void send(String url, Object object, Logger logger) {
      WebClient.create(url)
         .post()
         .contentType(MediaType.APPLICATION_JSON)
         .bodyValue(object)
         .retrieve()
         .toBodilessEntity()
         .subscribe((e) -> {
         }, (error) -> {
            logger.error(error.getMessage());
         });
   }
}

메세지를 생성한 후 디스코드에서 만든 url과 양식에 맞게 만든 메세지를 스프링 WebFlux에서 제공하는 WebClient를 통해 메세지를 전달할 수 있도록 했습니다.


결과

이제 결과를 확인해 보겠습니다.

@GetMapping("/test")
public String test() throws IOException {
   String s = null;
   s.charAt(0);
   return ":/";
}

위 method를 통해 NullPointer 예외가 발생하도록 했습니다.

스프링 애플리케이션 콘솔에 위와 같이 웹 훅을 보내고 response를 받았고,

위와 같이 디스코드에 정상적으로 발송이 된 것을 확인할 수 있습니다.


 

참고자료

**🔗 - https://medium.com/chequer/springboot-slack-logback을-이용한-실시간-에러-모니터링-구현하기-7f231812d3fc**

**🔗 - https://junspapa-itdev.tistory.com/34**

**🔗 - https://jaehoney.tistory.com/174**

**🔗 - https://logback.qos.ch/**