Skip to content

Commit

Permalink
[feat] 에러 슬랙 알림 구현 (#150)
Browse files Browse the repository at this point in the history
* [feat] slack 에러 알림 구현 #149

* [chore] spotless 적용 #149

* [feat] prod 환경 조건 추가 #149
  • Loading branch information
wjdtkdgns authored Aug 28, 2023
1 parent 469f257 commit 4c9699e
Show file tree
Hide file tree
Showing 11 changed files with 245 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -98,6 +100,7 @@ public ResponseEntity<ErrorResponse> 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);
}

Expand All @@ -109,6 +112,7 @@ protected ResponseEntity<ErrorResponse> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
4 changes: 2 additions & 2 deletions Domain/src/main/resources/application-domain.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions Infrastructure/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<LayoutBlock> 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<LayoutBlock> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ aws:
bucket: ${AWS_S3_BUCKET:bucket}
base-url: ${AWS_S3_BASEURL:baseUrl}

slack:
webhook:
slackUrl: ${SLACK_URL}


---
Expand Down

0 comments on commit 4c9699e

Please sign in to comment.