Skip to content

Commit

Permalink
[FEAT] 예외 발생 시, Slack 채널에 메세지를 보내는 기능 추가 (#214)
Browse files Browse the repository at this point in the history
* chore: slack 의존성 추가 및 불필요한 주석 제거

* chore: favicon error 해결을 위해 favicon.ico 추가

* feat: BusinessException 발생 시, Slack 채널에 메세지 보내는 기능 추가

- BusinessException 발생 시, Slack의 #exception-log 채널에 메세지를 보내는 기능 추가
- GlobalException에 대한 추가 처리도 필요
- .yml 파일에 slack 정보 추가  필요
- 스택 트레이스 가독성 개선 필요

* feat: Exception 발생 시 보내는 메세지 포멧 변경

- Exception 발생 시 전송하는 Slack 메세지에 예외 발생 시각 데이터 추가

* feat: 처리하지 않은 예외 외에 다른 예외 발생 시에도 Slack 메세지 보내도록 설정

* fix: prod 프로파일이 활성화되어 있을 때에만 메세지를 보내도록 설정

- Environment를 의존성 주입받아 prod(application-prod.yml) 프로파일이 활성화되어 있을 때에만 Slack 메세지를 보내도록 설정
- 각 ExceptionHandler가 MessageSender 인터페이스를 상속받고, sendSlackMessage() 메서드를 구현
- 테스트 및 개발 환경에서 Slack 메세지를 보내지 않는 점 확인
  • Loading branch information
SSung023 authored Jul 9, 2024
1 parent 3e26e8f commit 36c6ce5
Show file tree
Hide file tree
Showing 10 changed files with 218 additions and 11 deletions.
8 changes: 4 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ dependencies {

// AWS
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.1.RELEASE'

// json
implementation 'com.googlecode.json-simple:json-simple:1.1.1'

Expand All @@ -53,16 +53,16 @@ dependencies {

// MongoDB
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
// testImplementation 'de.flapdoodle.embed:de.flapdoodle.embed.mongo'

// H2
//implementation 'com.h2database:h2'
//runtimeOnly 'com.h2database:h2:2.2.222'
testRuntimeOnly 'com.h2database:h2:2.2.222'

// Github API for Java
implementation 'org.kohsuke:github-api:1.318'

// Slack
implementation 'com.slack.api:slack-api-client:1.25.1'

compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,32 @@ public static LocalDate convertToKST(Date date) {
);
}

public static LocalDate convertToKST(LocalDateTime nowLocal) {
public static LocalDateTime getKstLocalTime() {
ZoneId systemZone = ZoneId.systemDefault();
ZoneId koreaZone = ZoneId.of("Asia/Seoul");

// LocalDate.now()를 호출하여 LocalDateTime을 생성
LocalDateTime nowLocal = LocalDateTime.now();
// 현재 시스템의 ZoneId를 이용하여 ZonedDateTime을 생성
ZonedDateTime nowZone = ZonedDateTime.of(nowLocal, systemZone);

// KST(Asia/Seoul)로 변환
ZonedDateTime koreaTime = nowZone.withZoneSameInstant(koreaZone);

// LocalDateTime으로 변환하여 반환
return koreaTime.toLocalDateTime();
}

public static LocalDate convertToKST(LocalDateTime nowLocal) {
ZoneId systemZone = ZoneId.systemDefault();
ZoneId koreaZone = ZoneId.of("Asia/Seoul");

// 현재 시스템의 ZoneId를 이용하여 ZonedDateTime을 생성
ZonedDateTime nowZone = ZonedDateTime.of(nowLocal, systemZone);

// KST(Asia/Seoul)로 변환
ZonedDateTime koreaTime = nowZone.withZoneSameInstant(koreaZone);

return koreaTime.toLocalDate();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
package com.genius.gitget.global.util.exception;
package com.genius.gitget.global.util.exception.handler;

import com.genius.gitget.global.util.exception.BusinessException;
import com.genius.gitget.global.util.response.dto.CommonResponse;
import com.genius.gitget.slack.service.SlackService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class BusinessExceptionHandler {
public class BusinessExceptionHandler implements MessageSender {
private final Environment env;
private final SlackService slackService;

@ExceptionHandler(BusinessException.class)
protected ResponseEntity<CommonResponse> globalBusinessExceptionHandler(BusinessException e) {
log.error("[ERROR]" + e.getMessage(), e);
sendSlackMessage(e);

return ResponseEntity.badRequest().body(
new CommonResponse(e.getStatus(), e.getMessage())
);
}

@Override
public void sendSlackMessage(Exception exception) {
if (!env.matchesProfiles("prod")) {
return;
}
slackService.sendMessage(exception);
}

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.genius.gitget.global.util.exception;
package com.genius.gitget.global.util.exception.handler;

import static com.genius.gitget.global.util.exception.ErrorCode.FILE_MAX_SIZE_EXCEED;

import com.genius.gitget.global.util.response.dto.CommonResponse;
import com.genius.gitget.slack.service.SlackService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
Expand All @@ -12,11 +15,25 @@

@Slf4j
@RestControllerAdvice
public class FileExceptionHandler {
@RequiredArgsConstructor
public class FileExceptionHandler implements MessageSender {
private final Environment env;
private final SlackService slackService;

@ExceptionHandler(MaxUploadSizeExceededException.class)
protected ResponseEntity<CommonResponse> globalExceptionHandler(Exception e) {
log.error("Multipart 용량이 최대 크기를 초과하여 예외가 발생했습니다.");
sendSlackMessage(e);

return ResponseEntity.badRequest().body(
new CommonResponse(HttpStatus.BAD_REQUEST, FILE_MAX_SIZE_EXCEED.getMessage()));
}

@Override
public void sendSlackMessage(Exception exception) {
if (!env.matchesProfiles("prod")) {
return;
}
slackService.sendMessage(exception);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.genius.gitget.global.util.exception;
package com.genius.gitget.global.util.exception.handler;

import com.genius.gitget.global.util.response.dto.CommonResponse;
import com.genius.gitget.slack.service.SlackService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
Expand All @@ -11,13 +13,25 @@
@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {
public class GlobalExceptionHandler implements MessageSender {
private final Environment env;
private final SlackService slackService;

@ExceptionHandler(Exception.class)
protected ResponseEntity<CommonResponse> globalExceptionHandler(Exception e) {
log.error("예외처리 되지 않은 Exception 발생 - 처리 필요");
log.error("[UNHANDLED ERROR] " + e.getMessage(), e);
sendSlackMessage(e);

return ResponseEntity.badRequest().body(
new CommonResponse(HttpStatus.BAD_REQUEST, e.getMessage()));
}

@Override
public void sendSlackMessage(Exception exception) {
if (!env.matchesProfiles("prod")) {
return;
}
slackService.sendMessage(exception);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.genius.gitget.global.util.exception.handler;

public interface MessageSender {
/**
* 예외가 발생했을 때 Slack에 예외 발생에 대한 메세지를 보내는 메서드
* <p>
* 주의 사항!!
* 활성화 된 profile이 "prod"일 때에만 작동하도록 해야 합니다.
* Environment의 matchProfiles()를 통해 특정 프로파일이 활성화되어 있는지 확인 가능
* if(!environment.matchesProfiles("prod")) return;
*
* @param exception 발생한 예외
*/
void sendSlackMessage(Exception exception);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.genius.gitget.slack.service;

import static com.slack.api.model.block.Blocks.divider;
import static com.slack.api.model.block.Blocks.section;
import static com.slack.api.model.block.composition.BlockCompositions.markdownText;

import com.genius.gitget.challenge.certification.util.DateUtil;
import com.slack.api.model.Attachment;
import com.slack.api.model.block.LayoutBlock;
import com.slack.api.model.block.composition.TextObject;
import java.util.ArrayList;
import java.util.List;

public class SlackMessageUtil {

private static final String ERROR_TITLE = "*Exception 발생 시각:* ";
private static final String ERROR_MESSAGE = "*Exception Message:*\n";
private static final String ERROR_STACK = "*Exception Stack:*\n";
private static final String FILTER_STRING = "gitget";


public static String createErrorTitle() {
return ERROR_TITLE + DateUtil.getKstLocalTime();
}

public static List<Attachment> createAttachments(String color, List<LayoutBlock> data) {
List<Attachment> attachments = new ArrayList<>();
Attachment attachment = new Attachment();
attachment.setColor(color);
attachment.setBlocks(data);
attachments.add(attachment);
return attachments;
}

public static List<LayoutBlock> createProdErrorMessage(Exception exception) {
StackTraceElement[] stacks = exception.getStackTrace();

List<LayoutBlock> layoutBlockList = new ArrayList<>();

List<TextObject> sectionInFields = new ArrayList<>();
sectionInFields.add(markdownText(ERROR_MESSAGE + exception.getMessage()));
sectionInFields.add(markdownText(ERROR_STACK + exception));
layoutBlockList.add(section(section -> section.fields(sectionInFields)));

layoutBlockList.add(divider());
layoutBlockList.add(section(section -> section.text(markdownText(filterErrorStack(stacks)))));
return layoutBlockList;
}

private static String filterErrorStack(StackTraceElement[] stacks) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("```");
for (StackTraceElement stack : stacks) {
if (stack.toString().contains(FILTER_STRING)) {
stringBuilder.append(stack).append("\n");
}
}
stringBuilder.append("```");
return stringBuilder.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.genius.gitget.slack.service;

public interface SlackService {
void sendMessage(String message);

void sendMessage(Exception exception);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.genius.gitget.slack.service;

import com.slack.api.Slack;
import com.slack.api.methods.MethodsClient;
import com.slack.api.methods.SlackApiException;
import com.slack.api.methods.request.chat.ChatPostMessageRequest;
import com.slack.api.model.Attachment;
import com.slack.api.model.block.LayoutBlock;
import java.io.IOException;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class SlackServiceImpl implements SlackService {
private static final String ATTACHMENTS_ERROR_COLOR = "#eb4034";
@Value("${slack.token}")
private String token;
@Value("${slack.channel}")
private String channel;

@Override
public void sendMessage(String message) {
try {
MethodsClient methods = Slack.getInstance().methods(token);
ChatPostMessageRequest request = ChatPostMessageRequest.builder()
.channel(channel)
.text(message)
.build();

methods.chatPostMessage(request);

} catch (SlackApiException | IOException e) {
log.error(e.getMessage());
}
}

@Override
public void sendMessage(Exception exception) {
try {
String errorTitle = SlackMessageUtil.createErrorTitle();
List<LayoutBlock> layoutBlocks = SlackMessageUtil.createProdErrorMessage(exception);
List<Attachment> attachments = SlackMessageUtil.createAttachments(ATTACHMENTS_ERROR_COLOR,
layoutBlocks);

MethodsClient methods = Slack.getInstance().methods(token);
ChatPostMessageRequest request = ChatPostMessageRequest.builder()
.channel(channel)
.attachments(attachments)
.text(errorTitle)
.build();

methods.chatPostMessage(request);
log.info("slack 메세지 전송 성공");

} catch (SlackApiException | IOException e) {
log.error(e.getMessage());
}
}
}
Binary file added src/main/resources/static/favicon.ico
Binary file not shown.

0 comments on commit 36c6ce5

Please sign in to comment.