From 4c9699eb8721f1108c7cf091ddaf86d6c9dc5066 Mon Sep 17 00:00:00 2001 From: Sanghoon Jeong <67852689+wjdtkdgns@users.noreply.github.com> Date: Mon, 28 Aug 2023 22:18:08 +0900 Subject: [PATCH] =?UTF-8?q?[feat]=20=EC=97=90=EB=9F=AC=20=EC=8A=AC?= =?UTF-8?q?=EB=9E=99=20=EC=95=8C=EB=A6=BC=20=EA=B5=AC=ED=98=84=20(#150)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [feat] slack 에러 알림 구현 #149 * [chore] spotless 적용 #149 * [feat] prod 환경 조건 추가 #149 --- .../api/config/GlobalExceptionHandler.java | 4 + .../async/CustomAsyncExceptionHandler.java | 3 + .../server/core/config/EnableAsyncConfig.java | 5 + .../events/slack/SlackAsyncErrorEvent.java | 27 ++++ .../event/events/slack/SlackErrorEvent.java | 19 +++ .../src/main/resources/application-domain.yml | 4 +- Infrastructure/build.gradle | 1 + .../infrastructure/slack/SlackHelper.java | 31 +++++ .../slack/SlackMessageGenerater.java | 116 ++++++++++++++++++ .../slack/SlackSendMessageHandler.java | 34 +++++ .../resources/application-infrastructure.yml | 3 + 11 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 Core/src/main/java/allchive/server/core/event/events/slack/SlackAsyncErrorEvent.java create mode 100644 Core/src/main/java/allchive/server/core/event/events/slack/SlackErrorEvent.java create mode 100644 Infrastructure/src/main/java/allchive/server/infrastructure/slack/SlackHelper.java create mode 100644 Infrastructure/src/main/java/allchive/server/infrastructure/slack/SlackMessageGenerater.java create mode 100644 Infrastructure/src/main/java/allchive/server/infrastructure/slack/SlackSendMessageHandler.java diff --git a/Api/src/main/java/allchive/server/api/config/GlobalExceptionHandler.java b/Api/src/main/java/allchive/server/api/config/GlobalExceptionHandler.java index a1826b6a..db9dd5b0 100644 --- a/Api/src/main/java/allchive/server/api/config/GlobalExceptionHandler.java +++ b/Api/src/main/java/allchive/server/api/config/GlobalExceptionHandler.java @@ -7,6 +7,8 @@ import allchive.server.core.error.BaseErrorException; import allchive.server.core.error.ErrorResponse; import allchive.server.core.error.GlobalErrorCode; +import allchive.server.core.event.Event; +import allchive.server.core.event.events.slack.SlackErrorEvent; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -98,6 +100,7 @@ public ResponseEntity BaseDynamicExceptionHandler( BaseDynamicException e, HttpServletRequest request) { ErrorResponse errorResponse = ErrorResponse.from(ErrorReason.of(e.getStatus(), e.getCode(), e.getMessage())); + Event.raise(SlackErrorEvent.from(e)); return ResponseEntity.status(HttpStatus.valueOf(e.getStatus())).body(errorResponse); } @@ -109,6 +112,7 @@ protected ResponseEntity handleException( final GlobalErrorCode globalErrorCode = GlobalErrorCode._INTERNAL_SERVER_ERROR; final ErrorReason errorReason = globalErrorCode.getErrorReason(); final ErrorResponse errorResponse = ErrorResponse.from(errorReason); + Event.raise(SlackErrorEvent.from(e)); return ResponseEntity.status(INTERNAL_SERVER_ERROR).body(errorResponse); } } diff --git a/Core/src/main/java/allchive/server/core/async/CustomAsyncExceptionHandler.java b/Core/src/main/java/allchive/server/core/async/CustomAsyncExceptionHandler.java index b00e1389..c737426c 100644 --- a/Core/src/main/java/allchive/server/core/async/CustomAsyncExceptionHandler.java +++ b/Core/src/main/java/allchive/server/core/async/CustomAsyncExceptionHandler.java @@ -1,6 +1,8 @@ package allchive.server.core.async; +import allchive.server.core.event.Event; +import allchive.server.core.event.events.slack.SlackAsyncErrorEvent; import java.lang.reflect.Method; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -19,5 +21,6 @@ public void handleUncaughtException(Throwable throwable, Method method, Object.. for (Object param : params) { log.error("Parameter value - " + param); } + Event.raise(SlackAsyncErrorEvent.of(method.getName(), throwable, params)); } } diff --git a/Core/src/main/java/allchive/server/core/config/EnableAsyncConfig.java b/Core/src/main/java/allchive/server/core/config/EnableAsyncConfig.java index aad17c92..4d6f773b 100644 --- a/Core/src/main/java/allchive/server/core/config/EnableAsyncConfig.java +++ b/Core/src/main/java/allchive/server/core/config/EnableAsyncConfig.java @@ -40,6 +40,11 @@ public Executor s3ImageTaskExecutor() { return createTaskExecutor("S3_IMAGE_TASK_EXECUTOR"); } + @Bean(name = "slackTaskExecutor") + public Executor slackTaskExecutor() { + return createTaskExecutor("SLACK_TASK_EXECUTOR"); + } + private Executor createTaskExecutor(String name) { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(CORE_POOL_SIZE); diff --git a/Core/src/main/java/allchive/server/core/event/events/slack/SlackAsyncErrorEvent.java b/Core/src/main/java/allchive/server/core/event/events/slack/SlackAsyncErrorEvent.java new file mode 100644 index 00000000..f5654070 --- /dev/null +++ b/Core/src/main/java/allchive/server/core/event/events/slack/SlackAsyncErrorEvent.java @@ -0,0 +1,27 @@ +package allchive.server.core.event.events.slack; + + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class SlackAsyncErrorEvent { + private String name; + private Throwable throwable; + private Object[] params; + + @Builder + private SlackAsyncErrorEvent(String name, Throwable throwable, Object[] params) { + this.name = name; + this.throwable = throwable; + this.params = params; + } + + public static SlackAsyncErrorEvent of(String name, Throwable throwable, Object[] params) { + return SlackAsyncErrorEvent.builder() + .name(name) + .throwable(throwable) + .params(params) + .build(); + } +} diff --git a/Core/src/main/java/allchive/server/core/event/events/slack/SlackErrorEvent.java b/Core/src/main/java/allchive/server/core/event/events/slack/SlackErrorEvent.java new file mode 100644 index 00000000..0aea123b --- /dev/null +++ b/Core/src/main/java/allchive/server/core/event/events/slack/SlackErrorEvent.java @@ -0,0 +1,19 @@ +package allchive.server.core.event.events.slack; + + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class SlackErrorEvent { + private Exception exception; + + @Builder + private SlackErrorEvent(Exception exception) { + this.exception = exception; + } + + public static SlackErrorEvent from(Exception exception) { + return SlackErrorEvent.builder().exception(exception).build(); + } +} diff --git a/Domain/src/main/resources/application-domain.yml b/Domain/src/main/resources/application-domain.yml index 7e9d10eb..6ec6aaf7 100644 --- a/Domain/src/main/resources/application-domain.yml +++ b/Domain/src/main/resources/application-domain.yml @@ -34,8 +34,8 @@ logging: com.zaxxer.hikari: TRACE org.springframework.orm.jpa: DEBUG org.springframework.transaction: DEBUG - org.hibernate.SQL: debug - org.hibernate.type: trace +# org.hibernate.SQL: debug +# org.hibernate.type: trace --- # dev spring: diff --git a/Infrastructure/build.gradle b/Infrastructure/build.gradle index 86d0d1eb..dc4f5d97 100644 --- a/Infrastructure/build.gradle +++ b/Infrastructure/build.gradle @@ -8,6 +8,7 @@ dependencies { api 'org.springframework.cloud:spring-cloud-starter-openfeign:3.1.4' api 'com.amazonaws:aws-java-sdk-s3control:1.12.372' api 'com.nimbusds:nimbus-jose-jwt:3.10' + api("com.slack.api:slack-api-client:1.28.0") testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2' diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/slack/SlackHelper.java b/Infrastructure/src/main/java/allchive/server/infrastructure/slack/SlackHelper.java new file mode 100644 index 00000000..9d761f1c --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/slack/SlackHelper.java @@ -0,0 +1,31 @@ +package allchive.server.infrastructure.slack; + + +import allchive.server.core.helper.SpringEnvironmentHelper; +import com.slack.api.Slack; +import com.slack.api.webhook.Payload; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class SlackHelper { + private final SpringEnvironmentHelper springEnvironmentHelper; + + @Value("${slack.webhook.slackUrl}") + String slackUrl; + + public void sendErrorNotification(Payload payload) { + final Slack slack = Slack.getInstance(); + + try { + if (springEnvironmentHelper.isProdProfile()) { + slack.send(slackUrl, payload); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/slack/SlackMessageGenerater.java b/Infrastructure/src/main/java/allchive/server/infrastructure/slack/SlackMessageGenerater.java new file mode 100644 index 00000000..4fb99e08 --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/slack/SlackMessageGenerater.java @@ -0,0 +1,116 @@ +package allchive.server.infrastructure.slack; + +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.plainText; + +import allchive.server.core.event.events.slack.SlackAsyncErrorEvent; +import allchive.server.core.event.events.slack.SlackErrorEvent; +import com.slack.api.model.block.Blocks; +import com.slack.api.model.block.DividerBlock; +import com.slack.api.model.block.HeaderBlock; +import com.slack.api.model.block.LayoutBlock; +import com.slack.api.model.block.composition.MarkdownTextObject; +import com.slack.api.model.block.composition.TextObject; +import com.slack.api.webhook.Payload; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class SlackMessageGenerater { + private final int MAX_LEN = 500; + + public Payload generateErrorMsg(SlackErrorEvent event) throws IOException { + final Exception e = event.getException(); + + List layoutBlocks = new ArrayList<>(); + // 제목 + layoutBlocks.add(HeaderBlock.builder().text(plainText("에러 알림")).build()); + // 구분선 + layoutBlocks.add(new DividerBlock()); + // timeØ + layoutBlocks.add(getTime()); + // IP + Method, Addr + layoutBlocks.add(makeSection(getErrMessage(e), getErrStack(e))); + + return Payload.builder().text("에러 알림").blocks(layoutBlocks).build(); + } + + private LayoutBlock getTime() { + MarkdownTextObject timeObj = + MarkdownTextObject.builder().text("* Time :*\n" + LocalDateTime.now()).build(); + return Blocks.section(section -> section.fields(List.of(timeObj))); + } + + private LayoutBlock makeSection(TextObject first, TextObject second) { + return Blocks.section(section -> section.fields(List.of(first, second))); + } + + private MarkdownTextObject getErrMessage(Exception e) { + final String errorMessage = e.getMessage(); + return MarkdownTextObject.builder().text("* Message :*\n" + errorMessage).build(); + } + + private MarkdownTextObject getErrStack(Throwable throwable) { + String exceptionAsString = Arrays.toString(throwable.getStackTrace()); + int cutLength = Math.min(exceptionAsString.length(), MAX_LEN); + String errorStack = exceptionAsString.substring(0, cutLength); + return MarkdownTextObject.builder().text("* Stack Trace :*\n" + errorStack).build(); + } + + public Payload generateAsyncErrorMsg(SlackAsyncErrorEvent event) { + String name = event.getName(); + Throwable throwable = event.getThrowable(); + Object[] params = event.getParams(); + List layoutBlocks = new ArrayList<>(); + layoutBlocks.add( + Blocks.header( + headerBlockBuilder -> headerBlockBuilder.text(plainText("비동기 에러 알림")))); + layoutBlocks.add(divider()); + + MarkdownTextObject errorUserIdMarkdown = + MarkdownTextObject.builder().text("* 메소드 이름 :*\n" + name).build(); + MarkdownTextObject errorUserIpMarkdown = + MarkdownTextObject.builder() + .text("* 요청 파라미터 :*\n" + getParamsToString(params)) + .build(); + layoutBlocks.add( + section( + section -> + section.fields(List.of(errorUserIdMarkdown, errorUserIpMarkdown)))); + + layoutBlocks.add(divider()); + layoutBlocks.add(getTime()); + String errorStack = getErrorStack(throwable); + String message = throwable.toString(); + MarkdownTextObject errorNameMarkdown = + MarkdownTextObject.builder().text("* Message :*\n" + message).build(); + MarkdownTextObject errorStackMarkdown = + MarkdownTextObject.builder().text("* Stack Trace :*\n" + errorStack).build(); + layoutBlocks.add( + section(section -> section.fields(List.of(errorNameMarkdown, errorStackMarkdown)))); + return Payload.builder().text("비동기 에러 알림").blocks(layoutBlocks).build(); + } + + private String getParamsToString(Object[] params) { + StringBuilder paramToString = new StringBuilder(); + for (Object param : params) { + paramToString.append(param.toString()); + } + return paramToString.toString(); + } + + private String getErrorStack(Throwable throwable) { + String exceptionAsString = Arrays.toString(throwable.getStackTrace()); + int cutLength = Math.min(exceptionAsString.length(), MAX_LEN); + return exceptionAsString.substring(0, cutLength); + } +} diff --git a/Infrastructure/src/main/java/allchive/server/infrastructure/slack/SlackSendMessageHandler.java b/Infrastructure/src/main/java/allchive/server/infrastructure/slack/SlackSendMessageHandler.java new file mode 100644 index 00000000..6adb2d84 --- /dev/null +++ b/Infrastructure/src/main/java/allchive/server/infrastructure/slack/SlackSendMessageHandler.java @@ -0,0 +1,34 @@ +package allchive.server.infrastructure.slack; + + +import allchive.server.core.event.events.slack.SlackAsyncErrorEvent; +import allchive.server.core.event.events.slack.SlackErrorEvent; +import com.slack.api.webhook.Payload; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class SlackSendMessageHandler { + private final SlackMessageGenerater slackMessageGenerater; + private final SlackHelper slackHelper; + + @Async(value = "slackTaskExecutor") + @EventListener(SlackErrorEvent.class) + public void HandleError(SlackErrorEvent event) throws IOException { + Payload payload = slackMessageGenerater.generateErrorMsg(event); + slackHelper.sendErrorNotification(payload); + } + + @Async(value = "slackTaskExecutor") + @EventListener(SlackAsyncErrorEvent.class) + public void HandleAsyncError(SlackAsyncErrorEvent event) throws IOException { + Payload payload = slackMessageGenerater.generateAsyncErrorMsg(event); + slackHelper.sendErrorNotification(payload); + } +} diff --git a/Infrastructure/src/main/resources/application-infrastructure.yml b/Infrastructure/src/main/resources/application-infrastructure.yml index 0bfceb65..08dff465 100644 --- a/Infrastructure/src/main/resources/application-infrastructure.yml +++ b/Infrastructure/src/main/resources/application-infrastructure.yml @@ -5,6 +5,9 @@ aws: bucket: ${AWS_S3_BUCKET:bucket} base-url: ${AWS_S3_BASEURL:baseUrl} +slack: + webhook: + slackUrl: ${SLACK_URL} ---