서비스를 운영하다 보면 에러가 발생할 수밖에 없다.
에러가 발생하면 직접 EC2에 접속해서 로그를 확인하고.. 매우 번거롭다.
이러한 문제를 해소하고자 에러가 발생하면 Slack에 알림이 전송되게끔 구현해 봤다.
1. Slack Webhooks 추가하기
알림이 전송될 채널 우클릭 -> 채널 세부정보 보기 -> 통합 -> 앱 추가 -> "Incoming Webhooks" 추가
2. 구성 설정 해주기
3. 의존성 추가하기
implementation 'net.gpedro.integrations.slack:slack-webhook:1.4.0'
4. Slack API 사용 설정하기
application.yml
slack:
webhook:
url: URL 적어주기
SlackLogAppenderConfig.java
@Configuration
public class SlackLogAppenderConfig {
@Value("${slack.webhook.url}")
private String webhookUrl;
@Bean
public SlackApi slackApi() {
return new SlackApi(webhookUrl);
}
}
5. AOP, 비동기 처리하기
특정 작업이 수행될 때마다 로그를 남기듯이 알림을 보내도록 설정한다.
원하는 이벤트들을 효율적으로 관리하기 위해 AOP를 활용한다.
요청을 보낸 후 Slack의 응답이 애플리케이션의 다른 동작에 즉시 영향을 미치지 않으므로 비동기로 처리한다.
SlackNotification.java
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SlackNotification {
}
ThreadPoolConfig.java
@EnableAsync
@Configuration
public class ThreadPoolConfig {
private static final int MAX_POOL_SIZE = 5; // 최대 스레드 수
private static final int CORE_POOL_SIZE = 5; // 기본 스레드 수
@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setMaxPoolSize(MAX_POOL_SIZE);
threadPoolTaskExecutor.setCorePoolSize(CORE_POOL_SIZE);
threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;
}
}
SlackNotificationAspect.java
추후 prod 환경으로 변경하기
@RequiredArgsConstructor
@Profile(value = "dev")
@Aspect
@Component
public class SlackNotificationAspect {
private final SlackApi slackApi;
private final ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Around(
value = "@annotation(com.twoclock.gitconnect.global.slack.annotation.SlackNotification) && args(request, e)",
argNames = "proceedingJoinPoint,request,e"
)
public Object slackNotification(
ProceedingJoinPoint proceedingJoinPoint, HttpServletRequest request, Exception e
) throws Throwable {
threadPoolTaskExecutor.execute(() -> sendSlackErrorMessage(request, e));
return proceedingJoinPoint.proceed();
}
private void sendSlackErrorMessage(HttpServletRequest request, Exception e) {
String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"));
SlackAttachment slackAttachment = new SlackAttachment();
slackAttachment.setFallback("Error");
slackAttachment.setColor("danger");
slackAttachment.setTitle("Error Log");
slackAttachment.setTitleLink(request.getContextPath());
slackAttachment.setText(Arrays.toString(e.getStackTrace()));
slackAttachment.setFields(
Arrays.asList(
new SlackField().setTitle("Request URL").setValue(request.getRequestURL().toString()),
new SlackField().setTitle("Request Method").setValue(request.getMethod()),
new SlackField().setTitle("Request Time").setValue(now),
new SlackField().setTitle("Request IP").setValue(request.getRemoteAddr()),
new SlackField().setTitle("Request User-Agent").setValue(request.getHeader("User-Agent"))
)
);
SlackMessage slackMessage = new SlackMessage();
slackMessage.setAttachments(Collections.singletonList(slackAttachment));
slackMessage.setIcon(":x:");
slackMessage.setText("에러가 발생했습니다.");
slackMessage.setUsername("에러 봇");
slackApi.call(slackMessage);
}
}
6. 원하는 예외에 Annotation 달아주기
@SlackNotification
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponseDto> handleException(HttpServletRequest request, Exception e) {
ErrorResponseDto responseDto =
new ErrorResponseDto(
ErrorCode.INTERNAL_SERVER_ERROR.getCode(),
ErrorCode.INTERNAL_SERVER_ERROR.getMessage(),
null
);
log.error(e.getMessage(), e);
return new ResponseEntity<>(responseDto, HttpStatus.INTERNAL_SERVER_ERROR);
}
7. 결과
참고
https://shanepark.tistory.com/430