공부방

에러 로그 발생시 슬랙 알림 보내기

beomsic 2022. 11. 23. 14:48

💬 Slack 설정


워크 스페이스 생성 및 채널 생성

새로운 워크스페이스를 만들거나 기존의 워크스페이스 사용

에러 로그를 받을 새로운 채널 생성

 

채널 생성 후 우클릭 → View channel details 를 클릭해 상세 정보 페이지로 이동

Webhooks 추가

Integrations 항목에 들어가 App 을 추가

  • Github 앱을 통해서 commit 이나 pull request 등도 확인할 수 있다.

Webhook App을 추가

  • 슬랙을 통해 알림을 받을 예정이므로 Incoming Webhooks 을 install

추가 해둔 채널에 Incoming WebHooks integration을 추가한다.

추가를 하면 WebHook URL 과 사용 방법에 대한 안내를 해준다.

Webhook 을 통한 알림 전송

 

  • 웹훅을 통해 알림을 전송하는 방법은 2가지가 있다.
    • POST 요청에 JSON 문자열을 payload 파라미터 형태로 전송
    • POST 요청에 JSON 문자열을 body로 전송

test

curl -X POST --data-urlencode "payload={\\"channel\\": \\"#error-log\\", \\"username\\": \\"webhookbot\\", \\"text\\": \\"This is posted to #error-log and comes from a bot named webhookbot.\\", \\"icon_emoji\\": \\":ghost:\\"}" <https://hooks.slack.com/services/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>

  • slack에 메시지 도착

 

🖥️ Spring boot 설정


Spring boot에서 에러 로그가 발생했을 때 웹훅으로 슬랙에 알림 메시지를 보내도록 설정

다른 사용자들이 사용하기 좋게 만든 프로젝트를 사용

에러 발생시 슬랙 알림

slack-webhook 을 사용

 

의존성 추가

implementation ("net.gpedro.integrations.slack:slack-webhook:1.4.0")

 

Config 설정

  • SlackApi 를 빈으로 설정
@Configuration
public classSlackLogAppenderConfig {

  @Value("${logging.slack.webhook.uri}")
	privateStringslackWebhookURI;

  @Value("${logging.slack.token}")
	privateStringtoken;

  @Bean
	publicSlackApi getSlackApi() {
		return newSlackApi(slackWebhookURI+token);
  }
}

 

컨트롤러 어드바이스를 통해 특정 예외가 발생시 슬랙으로 알림을 오도록 설정

@ControllerAdvice
public class ExceptionAdvice {
  private final SlackApi slackApi;

  @ExceptionHandler(Exception.class)
  public ResponseEntity<ErrorResponse> handleRuntimeException(HttpServletRequest req, Exception e) {
    ErrorResponse response = ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR);
    sendSlackMessage(req, e);
    return ResponseEntity.internalServerError().body(response);
  }

  private void sendSlackMessage(HttpServletRequest req, Exception e) {
    SlackAttachment slackAttachment = new SlackAttachment();

    slackAttachment.setFallback("Error");
    slackAttachment.setColor("danger");
    slackAttachment.setTitle("⚠️ ERROR DETECT");
    slackAttachment.setTitleLink(req.getContextPath());
    slackAttachment.setText(e.getStackTrace().toString());
    slackAttachment.setColor("danger");
    slackAttachment.setFields(getSlackField(req));

    SlackMessage slackMessage = new SlackMessage();
    slackMessage.setAttachments(Collections.singletonList(slackAttachment));
    slackMessage.setIcon(":ghost:");
    slackMessage.setText("⚠️ SERVER EXCEPTION DETECT");
    slackMessage.setUsername("Beomsic");

    slackApi.call(slackMessage);
  }

  private List getSlackField (HttpServletRequest req) {

    List<SlackField> fields = new ArrayList<>();

    fields.add(new SlackField().setTitle("Request URL").setValue(req.getRequestURI().toString()));
    fields.add(new SlackField().setTitle("Request Method").setValue(req.getMethod()));
    fields.add(new SlackField().setTitle("Request Time").setValue(new Date().toString()));
    fields.add(new SlackField().setTitle("Request IP").setValue(req.getRemoteUser()));

    return fields;
  }
}

 

test

@GetMapping("/slack")
public void testSlack() {
	throw Exception("test");
}

 

에러 로그 발생시 슬랙 알림

logback-slack-appender 사용

 

의존성 추가

implementation 'com.github.maricn:logback-slack-appender:1.6.1'

 

logback.xml

<appender name="SLACK" class="com.github.maricn.logback.SlackAppender">
    <webhookUri>${SLACK_WEBHOOK_URI}</webhookUri>
    <channel>#error-log</channel>
    <layout class="ch.qos.logback.classic.PatternLayout">
      <pattern>${ERROR_LOG_PATTERN}</pattern>
    </layout>
    <username>${USER_NAME}</username>
    <iconEmoji>${EMOJI}</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>

 

실행 결과

📌 별도 스레드에서 처리하기


슬랙 웹 훅에 요청을 보내고 그 응답을 받는 시간을 비즈니스 로직이 기다리는 것은 비효율적이다.

따라서, 별도의 스레드에서 슬랙 알림 요청을 처리하도록 변경한다.

@GetMapping("/slackTest")
  public ResponseEntity<Void> slack() throws Exception {
    long startTime = System.nanoTime();
    RestTemplate restTemplate = new RestTemplate();
    for (int i = 0; i < 10; i++) {
      try {
        String result = restTemplate
            .getForObject("<http://localhost:8080/api/v1/users/slack>", String.class);
      } catch(Exception e) {
      }

    }
    long finishTime = System.nanoTime();
    long elapsedTime = finishTime - startTime;
    System.out.println("startTime(ms) : " + startTime);
    System.out.println("finishTime(ms) : " + finishTime);
    System.out.println("elapsedTime(ms) : " + elapsedTime);

    return ResponseEntity.ok().build();
  }

  @GetMapping("/slack")
  public void slackk() throws Exception{
    throw new Exception();
  }
  • 컨트롤러에서 여러번 Exception을 발생 시키는 코드

반복문을 10번 실행했을 경우 발생 결과

 

15번 실행 결과

 

TaskExecutor Bean 등록

스프링이 제공하는 TaskExecutor을 Bean으로 등록

  • ThreadPoolTaskExecutor 등록
@EnableAsync
@Configuration
public class AsyncConfig {

  private static int CORE_POOL_SIZE = 15;
  private static int MAX_POOL_SIZE = 25;
  private static int QUEUE_CAPACITY = 10;
  private static String THREAD_NAME_PREFIX = "async-task";

  @Bean
  public TaskExecutor createTaskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

    executor.setCorePoolSize(CORE_POOL_SIZE);
    executor.setMaxPoolSize(MAX_POOL_SIZE);
    executor.setQueueCapacity(QUEUE_CAPACITY);
    executor.setThreadNamePrefix(THREAD_NAME_PREFIX);
    executor.initialize();

    return executor;
  }
}

corePoolSize

  • 동시에 실행시킬 스레드의 개수

maxPoolSize

  • 스레드 풀의 최대사이즈를 지정

queueCapacity

  • 큐의 사이즈를 지정

 

비동기 호출

private final SlackApi slackApi;
private final TaskExecutor taskExecutor;

public SlackUtils(SlackApi slackApi, TaskExecutor taskExecutor) {   
  this.slackApi = slackApi;
  this.taskExecutor = taskExecutor;
}

public void sendSlackMessage(HttpServletRequest req, Exception e) {
    Runnable runnable = () -> {
      SlackAttachment slackAttachment = new SlackAttachment();

      slackAttachment.setFallback("Error");
      slackAttachment.setColor("danger");
      slackAttachment.setTitle("⚠️ ERROR DETECT");
      slackAttachment.setTitleLink(req.getContextPath());
      slackAttachment.setText(e.getStackTrace().toString());
      slackAttachment.setColor("danger");
      slackAttachment.setFields(getSlackField(req));

      SlackMessage slackMessage = new SlackMessage();
      slackMessage.setAttachments(Collections.singletonList(slackAttachment));
      slackMessage.setIcon(":ghost:");
      slackMessage.setText("⚠️ SERVER EXCEPTION DETECT");
      slackMessage.setUsername("Server Exception!!");

      slackApi.call(slackMessage);
    };
    taskExecutor.execute(runnable);
  }

빈으로 주입한 TaskExecutor를 통해 비동기 호출을 한다.

 

10번 실행 결과

 

15번 실행 결과

 

⇒ 걸리는 시간이 줄어들었다.

 

참고 자료

https://dundung.tistory.com/232

https://shanepark.tistory.com/430

https://github.com/gpedro/slack-webhook

https://github.com/maricn/logback-slack-appender

https://tecoble.techcourse.co.kr/post/2021-08-07-logback-tutorial/

https://velog.io/@server30sopt/슬랙에서-서버-에러-알림-받고-유연하게-에러-대응하기

https://kim-jong-hyun.tistory.com/104