JAVA/JAVA | Spring 학습기록

[Spring] 서버 에러 시 Slack Api Client로 슬랙에 알림 보내기

kth990303 2023. 4. 15. 13:45
반응형

해당 글에서는 slack-api-client 라이브러리 선정 이유,

@ControllerAdvice에서 슬랙 연동 방법에 대해 다룹니다.


사이드 프로젝트 `모카콩`의 Wiki에 작성한 글에 해당된다.


 

해당 프로젝트 github: https://github.com/mocacong/Mocacong-Backend

 

GitHub - mocacong/Mocacong-Backend: 모카콩 백엔드

모카콩 백엔드. Contribute to mocacong/Mocacong-Backend development by creating an account on GitHub.

github.com


들어가며

모카콩에서는 Slack을 통해 팀원들과 소통하고 있습니다. 개발 관련 회의나 데일리 스크럼, 이슈 등. 모카콩 관련 업무연락은 모두 slack으로 하고 있다고 봐도 무방할 정도입니다. 이는 버그가 발생한 상황에서도 마찬가지인데요.

백엔드 서버 에러 발생 시 슬랙 채널

위 사진처럼 서버 에러라고 추정되는 경우에는, issue 채널에 상황을 공유하곤 했습니다. 이러한 방법에는 몇 가지 단점이 존재합니다.

  • 슬랙 이슈 채널에 직접 상황을 공유해야 하는 번거로움
  • (다른 파트에서 먼저 발견한 경우) 백엔드 측 에러임에도 불구하고, 백엔드 팀원은 비교적 뒤늦게 확인

위 두 가지의 단점이 크게 느껴져서 internal server error 발생 시에 슬랙으로 알림을 보내주도록 설정했습니다.


라이브러리 선택

MavenRepository를 참고하여 취약점이 없는 라이브러리이자 최근에도 update가 활발하게 진행되고 있는 라이브러리인 slack-api-client를 선택했습니다. 버전은 1.29.0을 선택했습니다.

 

슬랙 웹훅 설정

Slack에서 서버 에러 로그 알림을 띄워줄 채널을 우클릭하여 아래 프로세스대로 들어가줍니다.

 

채널 세부정보 보기 → 통합 → 앱 → 앱 추가

 

`앱 추가`에서 incoming webhook 을 추가해줍시다.

여기서 보이는 웹후크 URL이 스프링 애플리케이션 yml 환경변수로 들어가므로 잘 복사해두도록 합시다.

해당 페이지에서 밑으로 내리면 예시 POST 코드가 있습니다. 복사해서 터미널에 붙여넣기하여 실제로 슬랙에 테스트 알림이 잘 오는지 확인해봅시다.

슬랙에 알림이 잘 온다면 연동이 문제없이 잘 된 것입니다.


스프링 애플리케이션 추가

위에서 살펴본 `slack-api-client:1.29.0` 의존성을 추가해줍시다.

build.gradle

1
2
3
dependencies {
    implementation 'com.slack.api:slack-api-client:1.29.0'
}
cs

application.yml

slack:
  webhook:
    url: ${SLACK_WEBHOOK_URL}

아까 환경변수로 주입해야 한다고 했던 웹훅 URL입니다.

`slack.webhook.url` 값으로 해도 되고, 편한대로 `slack.webhook.token` 등 다른 값으로 작성해도 괜찮습니다.

 

모카콩에서는 서버 에러 발생 시에 ControllerAdvice에서 예외 관련 response를 반환해주고 있습니다.

따라서 Internal Server Error 발생 시 핸들링하는 예외 메서드에 slack api 의존성이 생기게 됩니다.

Slack SDK for Java 홈페이지(https://slack.dev/java-slack-sdk/guides/web-api-basics)의 예시 코드를 살펴보자면 아래와 같습니다.

Slack 객체를 생성하고, 여기에서 send 메서드를 보낼 때 webhookUrl, payload를 첨부해주는 모습입니다.

환경변수는 System.getenv() 메서드로도 가져올 수 있지만, 저는 스프링부트의 편리한 @Value 어노테이션을 이용했습니다.

 

ControllerAdvice.class

1
2
3
4
private final Slack slackClient = Slack.getInstance();
 
@Value("${slack.webhook.url}")
private String webhookUrl;
cs

 

payload는 어떻게 작성하는 것이 좋을까요?

https://slack.dev/java-slack-sdk/guides/incoming-webhooks 에 간단한 작성법이 설명돼있습니다.

Payload.builder() 또는 직접 json 형태로 텍스트를 작성하여 payload를 만든 후, slack.send(webhookUrl, payload) 로 보내줄 수도 있습니다. 하지만, Java8의 함수형 프로그래밍을 이용한다면 더 간편하게 이용이 가능합니다.

하지만 단순히 text만 담아서 보내주는 것은 가독성이 그리 좋지 않다고 판단했습니다. 단순 Error의 getMessage() 값만 텍스트로 담아서 슬랙에 알림을 보내준 예시 화면은 아래와 같습니다.

단순 text에만 담아줘서 알림을 보낸 화면

위 알림만 봐서는 어떠한 에러인지, 어느 URL에서 발생했는지, 위급한 에러인지 한눈에 알기가 어렵습니다.

 
실제 모카콩에서 운영중인 에러 로그 슬랙 알림 화면

위처럼 좀 더 세부적인 정보를 예쁘게 담고, 빨간색으로 위험성을 부각시켜주게 하려면 어떻게 해야 할까요?

 

slack-api-client에는 text 뿐만 아니라 Attachment, Field도 존재합니다.

위 화면에서 빨간색 라인이 하나의 Attachment에 해당되고, Request URL, Error Message 등의 영역이 한 Field에 해당된다고 보시면 됩니다.

더 자세한 내용은 https://api.slack.com/reference/messaging/attachments#example 를 참고해주세요.

 

Reference: Secondary message attachments

Another way to attach content to messages is the old attachments system. We prefer Block Kit now.

api.slack.com

위 화면처럼 보이게 조금 더 꾸며보겠습니다.

 

ControllerAdvice.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> unhandledException(Exception e, HttpServletRequest request) {
    log.error("UnhandledException: {} {} errMessage={}\n",
            request.getMethod(),
            request.getRequestURI(),
            e.getMessage()
    );
    sendSlackAlertErrorLog(e, request); // 슬랙 알림 보내는 메서드
    return ResponseEntity.internalServerError()
            .body(new ErrorResponse(9999"일시적으로 접속이 원활하지 않습니다. 모카콩 서비스 팀에 문의 부탁드립니다."));
}
 
// 슬랙 알림 보내는 메서드
private void sendSlackAlertErrorLog(Exception e, HttpServletRequest request) {
    try {
        slackClient.send(webhookUrl, payload(p -> p
                .text("서버 에러 발생! 백엔드 측의 빠른 확인 요망")
                // attachment는 list 형태여야 합니다.
                .attachments(
                        List.of(generateSlackAttachment(e, request))
                )
        ));
    } catch (IOException slackError) {
        // slack 통신 시 발생한 예외에서 Exception을 던져준다면 재귀적인 예외가 발생합니다.
        // 따라서 로깅으로 처리하였고, 모카콩 서버 에러는 아니므로 `error` 레벨보다 낮은 레벨로 설정했습니다.
        log.debug("Slack 통신과의 예외 발생");
    }
}
 
// attachment 생성 메서드
private Attachment generateSlackAttachment(Exception e, HttpServletRequest request) {
    String requestTime = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS").format(LocalDateTime.now());
    String xffHeader = request.getHeader("X-FORWARDED-FOR");  // 프록시 서버일 경우 client IP는 여기에 담길 수 있습니다.
    return Attachment.builder()
            .color("ff0000")  // 붉은 색으로 보이도록
            .title(requestTime + " 발생 에러 로그")
// Field도 List 형태로 담아주어야 합니다.
           .fields(List.of(
                            generateSlackField("Request IP", xffHeader == null ? request.getRemoteAddr() : xffHeader),
                            generateSlackField("Request URL", request.getRequestURL() + " " + request.getMethod()),
                            generateSlackField("Error Message", e.getMessage())
                    )
            )
            .build();
}
 
// Field 생성 메서드
private Field generateSlackField(String title, String value) {
    return Field.builder()
            .title(title)
            .value(value)
            .valueShortEnough(false)
            .build();
}
cs

최종 결과

최종적으로 알림이 문제없이 잘 보내지는 것을 확인할 수 있습니다.

 

반응형