Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] 예외 발생 시, Slack 채널에 메세지를 보내는 기능 추가 #214

Merged
merged 7 commits into from
Jul 9, 2024
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.