에러 로그 발생시 슬랙 알림 보내기
💬 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/슬랙에서-서버-에러-알림-받고-유연하게-에러-대응하기