From f82dc4087f3c609b283f722de4349c3aaf897b39 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 3 Aug 2024 14:04:53 +0900 Subject: [PATCH 1/4] feat: create global exception handler --- .../user/handler/GlobalExceptionHandler.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 user-service/src/main/java/kr/mafoo/user/handler/GlobalExceptionHandler.java diff --git a/user-service/src/main/java/kr/mafoo/user/handler/GlobalExceptionHandler.java b/user-service/src/main/java/kr/mafoo/user/handler/GlobalExceptionHandler.java new file mode 100644 index 00000000..de20ae45 --- /dev/null +++ b/user-service/src/main/java/kr/mafoo/user/handler/GlobalExceptionHandler.java @@ -0,0 +1,44 @@ +package kr.mafoo.user.handler; + +import kr.mafoo.user.slack.SlackNotificationService; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.net.InetSocketAddress; + +@ControllerAdvice +@RequiredArgsConstructor +public class GlobalExceptionHandler { + private final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); + private final SlackNotificationService slackNotificationService; + + @ExceptionHandler(Exception.class) + public Mono> handleException(ServerWebExchange exchange, Exception ex) { + String userAgent = exchange.getRequest().getHeaders().getFirst("User-Agent"); + String proxyIp = exchange.getRequest().getHeaders().getFirst("X-Forwarded-For"); + InetSocketAddress address = exchange.getRequest().getRemoteAddress(); + String originIp = proxyIp != null ? proxyIp : (address != null ? address.toString() : "UNKNOWN SOURCE"); + String fullPath = exchange.getRequest().getURI().getPath() + + (exchange.getRequest().getURI().getQuery() != null ? "?" + exchange.getRequest().getURI().getQuery() : ""); + + logger.error("Exception occurred: {} {} {} ERROR {} {}", exchange.getRequest().getMethod(), fullPath, originIp, ex.getMessage(), userAgent); + + slackNotificationService.sendErrorNotification( + ex, + exchange.getRequest().getMethod().toString(), + exchange.getRequest().getURI().toString(), + HttpStatus.INTERNAL_SERVER_ERROR.toString(), + System.currentTimeMillis(), // or appropriate execution time + exchange.getRequest().getHeaders().getFirst("User-Agent") + ); + + return Mono.just(new ResponseEntity<>("Internal Server Error", HttpStatus.INTERNAL_SERVER_ERROR)); + } +} From 4d5a1c33418827a287dcc93c56ef6fbe37f2589a Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Sat, 3 Aug 2024 21:24:03 +0900 Subject: [PATCH 2/4] feat: add slack related info in application.yaml --- user-service/src/main/resources/application.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/user-service/src/main/resources/application.yaml b/user-service/src/main/resources/application.yaml index c627c588..d6128548 100644 --- a/user-service/src/main/resources/application.yaml +++ b/user-service/src/main/resources/application.yaml @@ -42,3 +42,9 @@ management: zipkin: tracing: endpoint: http://zipkin/api/v2/spans + +slack: + webhook: + token: ${SLACK_TOKEN} + channel: + error: ${SLACK_ERROR_CHANNEL} From 410a19376f5a0750730a5d0f303c056edbd2c570 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Thu, 15 Aug 2024 22:16:13 +0900 Subject: [PATCH 3/4] refactor: change sendErrorNotification to return Mono --- .../user/handler/GlobalExceptionHandler.java | 24 +-- .../kr/mafoo/user/service/SlackService.java | 196 +++++++++++------- 2 files changed, 128 insertions(+), 92 deletions(-) diff --git a/user-service/src/main/java/kr/mafoo/user/handler/GlobalExceptionHandler.java b/user-service/src/main/java/kr/mafoo/user/handler/GlobalExceptionHandler.java index de20ae45..b9de2b94 100644 --- a/user-service/src/main/java/kr/mafoo/user/handler/GlobalExceptionHandler.java +++ b/user-service/src/main/java/kr/mafoo/user/handler/GlobalExceptionHandler.java @@ -1,6 +1,6 @@ package kr.mafoo.user.handler; -import kr.mafoo.user.slack.SlackNotificationService; +import kr.mafoo.user.service.SlackService; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,10 +17,11 @@ @RequiredArgsConstructor public class GlobalExceptionHandler { private final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); - private final SlackNotificationService slackNotificationService; + private final SlackService slackService; @ExceptionHandler(Exception.class) public Mono> handleException(ServerWebExchange exchange, Exception ex) { + String method = exchange.getRequest().getMethod().toString(); String userAgent = exchange.getRequest().getHeaders().getFirst("User-Agent"); String proxyIp = exchange.getRequest().getHeaders().getFirst("X-Forwarded-For"); InetSocketAddress address = exchange.getRequest().getRemoteAddress(); @@ -28,17 +29,16 @@ public Mono> handleException(ServerWebExchange exchange, String fullPath = exchange.getRequest().getURI().getPath() + (exchange.getRequest().getURI().getQuery() != null ? "?" + exchange.getRequest().getURI().getQuery() : ""); - logger.error("Exception occurred: {} {} {} ERROR {} {}", exchange.getRequest().getMethod(), fullPath, originIp, ex.getMessage(), userAgent); + logger.error("Exception occurred: {} {} {} ERROR {} {}", method, fullPath, originIp, ex.getMessage(), userAgent); - slackNotificationService.sendErrorNotification( - ex, - exchange.getRequest().getMethod().toString(), - exchange.getRequest().getURI().toString(), - HttpStatus.INTERNAL_SERVER_ERROR.toString(), - System.currentTimeMillis(), // or appropriate execution time - exchange.getRequest().getHeaders().getFirst("User-Agent") + return slackService.sendErrorNotification( + method, + fullPath, + originIp, + userAgent, + ex.getMessage() + ).then( + Mono.just(new ResponseEntity<>("Internal Server Error", HttpStatus.INTERNAL_SERVER_ERROR)) ); - - return Mono.just(new ResponseEntity<>("Internal Server Error", HttpStatus.INTERNAL_SERVER_ERROR)); } } diff --git a/user-service/src/main/java/kr/mafoo/user/service/SlackService.java b/user-service/src/main/java/kr/mafoo/user/service/SlackService.java index 81f10d4b..4b533c8b 100644 --- a/user-service/src/main/java/kr/mafoo/user/service/SlackService.java +++ b/user-service/src/main/java/kr/mafoo/user/service/SlackService.java @@ -1,23 +1,19 @@ package kr.mafoo.user.service; 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.block.Blocks; import com.slack.api.model.block.LayoutBlock; import com.slack.api.model.block.composition.MarkdownTextObject; -import com.slack.api.model.block.composition.TextObject; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; -import java.io.IOException; import java.util.ArrayList; import java.util.List; import static com.slack.api.model.block.Blocks.*; -import static com.slack.api.model.block.composition.BlockCompositions.markdownText; import static com.slack.api.model.block.composition.BlockCompositions.plainText; @Service @@ -32,86 +28,126 @@ public class SlackService { private final MethodsClient methodsClient; - public void sendErrorNotification(Throwable throwable, String method, String uri, String statusCode, long executionTime, String userAgent) { - try { - List textObjects = new ArrayList<>(); - - textObjects.add(markdownText(">*예상하지 못한 에러가 발생했습니다!*\n")); - textObjects.add(markdownText("\n")); - - textObjects.add(markdownText("*메소드:* \n`" + method + "`\n")); - textObjects.add(markdownText("*URI:* \n`" + uri + "`\n")); - textObjects.add(markdownText("*상태코드:* \n`" + statusCode + "`\n")); - textObjects.add(markdownText("*메세지:* \n`" + throwable.getMessage() + "`\n")); - textObjects.add(markdownText("*소요시간:* \n`" + executionTime + " ms`\n")); - textObjects.add(markdownText("*사용자:* \n`" + userAgent + "`\n")); - - ChatPostMessageRequest request = ChatPostMessageRequest - .builder() - .channel(errorChannel) - .blocks( - asBlocks( - divider(), - section( - section -> section.fields(textObjects) - ) - )) - .build(); - - methodsClient.chatPostMessage(request); - } catch (SlackApiException | IOException e) { - throw new RuntimeException("Can't send Slack Message.", e); - } + public Mono sendErrorNotification(String method, String uri, String originIp, String userAgent, String message) { + return Mono.fromCallable(() -> { + List layoutBlocks = new ArrayList<>(); + + // Header 삽입 + layoutBlocks.add( + Blocks.header( + headerBlockBuilder -> + headerBlockBuilder.text(plainText("🚨 예상하지 못한 에러 발생")) + ) + ); + + layoutBlocks.add(divider()); + + // Content 삽입 + MarkdownTextObject errorMethodMarkdown = + MarkdownTextObject.builder().text("`METHOD`\n" + method).build(); + + MarkdownTextObject errorUriMarkdown = + MarkdownTextObject.builder().text("`URI`\n" + uri).build(); + + layoutBlocks.add( + section( + section -> section.fields(List.of(errorMethodMarkdown, errorUriMarkdown)) + ) + ); + + MarkdownTextObject errorOriginIpMarkdown = + MarkdownTextObject.builder().text("`에러 발생 IP`\n" + originIp).build(); + + MarkdownTextObject errorUserAgentMarkdown = + MarkdownTextObject.builder().text("`에러 발생 환경`\n" + userAgent).build(); + + layoutBlocks.add( + section( + section -> section.fields(List.of(errorOriginIpMarkdown, errorUserAgentMarkdown)) + ) + ); + + MarkdownTextObject errorMessageMarkdown = + MarkdownTextObject.builder().text("`메세지`\n" + message).build(); + + layoutBlocks.add( + section( + section -> section.fields(List.of(errorMessageMarkdown)) + ) + ); + + ChatPostMessageRequest chatPostMessageRequest = + ChatPostMessageRequest + .builder() + .text("예상하지 못한 에러 발생 알림") + .channel(errorChannel) + .blocks(layoutBlocks) + .build(); + + return methodsClient.chatPostMessage(chatPostMessageRequest); + + }).then(); } public Mono sendNewMemberNotification(String memberId, String memberName, String memberProfileImageUrl, String memberCreatedAt, String userAgent) { return Mono.fromCallable(() -> { - List layoutBlocks = new ArrayList<>(); - - layoutBlocks.add( - Blocks.header( - headerBlockBuilder -> - headerBlockBuilder.text(plainText("🎉 신규 사용자 가입")))); - layoutBlocks.add(divider()); - - MarkdownTextObject userIdMarkdown = - MarkdownTextObject.builder().text("`사용자 ID`\n" + memberId).build(); - - MarkdownTextObject userNameMarkdown = - MarkdownTextObject.builder().text("`사용자 닉네임`\n" + memberName).build(); - - layoutBlocks.add( - section( - section -> section.fields(List.of(userIdMarkdown, userNameMarkdown)))); - - MarkdownTextObject userProfileImageMarkdown = - MarkdownTextObject.builder().text("`프로필 이미지`\n" + memberProfileImageUrl).build(); - - MarkdownTextObject userCreatedAtMarkdown = - MarkdownTextObject.builder().text("`가입 일자`\n" + memberCreatedAt).build(); - - layoutBlocks.add( - section( - section -> section.fields(List.of(userProfileImageMarkdown, userCreatedAtMarkdown)))); - - MarkdownTextObject userUserAgentMarkdown = - MarkdownTextObject.builder().text("`가입 환경`\n" + userAgent).build(); - - layoutBlocks.add( - section( - section -> section.fields(List.of(userUserAgentMarkdown)))); - - ChatPostMessageRequest chatPostMessageRequest = - ChatPostMessageRequest - .builder() - .text("신규 사용자 가입 알림") - .channel(memberChannel) - .blocks(layoutBlocks) - .build(); - - return methodsClient.chatPostMessage(chatPostMessageRequest); - }) - .then(); + List layoutBlocks = new ArrayList<>(); + + // Header 삽입 + layoutBlocks.add( + Blocks.header( + headerBlockBuilder -> + headerBlockBuilder.text(plainText("🎉 신규 사용자 가입")) + ) + ); + + layoutBlocks.add(divider()); + + // Content 삽입 + MarkdownTextObject userIdMarkdown = + MarkdownTextObject.builder().text("`사용자 ID`\n" + memberId).build(); + + MarkdownTextObject userNameMarkdown = + MarkdownTextObject.builder().text("`사용자 닉네임`\n" + memberName).build(); + + layoutBlocks.add( + section( + section -> section.fields(List.of(userIdMarkdown, userNameMarkdown)) + ) + ); + + MarkdownTextObject userProfileImageMarkdown = + MarkdownTextObject.builder().text("`프로필 이미지`\n" + memberProfileImageUrl).build(); + + MarkdownTextObject userCreatedAtMarkdown = + MarkdownTextObject.builder().text("`가입 일자`\n" + memberCreatedAt).build(); + + layoutBlocks.add( + section( + section -> section.fields(List.of(userProfileImageMarkdown, userCreatedAtMarkdown)) + ) + ); + + MarkdownTextObject userUserAgentMarkdown = + MarkdownTextObject.builder().text("`가입 환경`\n" + userAgent).build(); + + layoutBlocks.add( + section( + section -> section.fields(List.of(userUserAgentMarkdown)) + ) + ); + + ChatPostMessageRequest chatPostMessageRequest = + ChatPostMessageRequest + .builder() + .text("신규 사용자 가입 알림") + .channel(memberChannel) + .blocks(layoutBlocks) + .build(); + + return methodsClient.chatPostMessage(chatPostMessageRequest); + + }).then(); } } From 5028aee53e5fdaef5041b9e4ee04b29c14fb4fa7 Mon Sep 17 00:00:00 2001 From: Gyoungmin Kim Date: Thu, 15 Aug 2024 23:08:25 +0900 Subject: [PATCH 4/4] refactor: fix GlobalExceptionHandler to send slack error for INTERNAL_SERVER_ERROR only --- .../user/handler/GlobalExceptionHandler.java | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/user-service/src/main/java/kr/mafoo/user/handler/GlobalExceptionHandler.java b/user-service/src/main/java/kr/mafoo/user/handler/GlobalExceptionHandler.java index b9de2b94..0d4e0bcc 100644 --- a/user-service/src/main/java/kr/mafoo/user/handler/GlobalExceptionHandler.java +++ b/user-service/src/main/java/kr/mafoo/user/handler/GlobalExceptionHandler.java @@ -8,6 +8,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @@ -19,8 +20,17 @@ public class GlobalExceptionHandler { private final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); private final SlackService slackService; + @ExceptionHandler(ResponseStatusException.class) + public Mono> handleResponseStatusException(ServerWebExchange exchange, ResponseStatusException ex) { + return handleExceptionInternal(exchange, ex, (HttpStatus) ex.getStatusCode()); + } + @ExceptionHandler(Exception.class) - public Mono> handleException(ServerWebExchange exchange, Exception ex) { + public Mono> handleGenericException(ServerWebExchange exchange, Exception ex) { + return handleExceptionInternal(exchange, ex, HttpStatus.INTERNAL_SERVER_ERROR); + } + + private Mono> handleExceptionInternal(ServerWebExchange exchange, Exception ex, HttpStatus status) { String method = exchange.getRequest().getMethod().toString(); String userAgent = exchange.getRequest().getHeaders().getFirst("User-Agent"); String proxyIp = exchange.getRequest().getHeaders().getFirst("X-Forwarded-For"); @@ -31,14 +41,16 @@ public Mono> handleException(ServerWebExchange exchange, logger.error("Exception occurred: {} {} {} ERROR {} {}", method, fullPath, originIp, ex.getMessage(), userAgent); - return slackService.sendErrorNotification( - method, - fullPath, - originIp, - userAgent, - ex.getMessage() - ).then( - Mono.just(new ResponseEntity<>("Internal Server Error", HttpStatus.INTERNAL_SERVER_ERROR)) - ); + if (status == HttpStatus.INTERNAL_SERVER_ERROR) { + return slackService.sendErrorNotification( + method, + fullPath, + originIp, + userAgent, + ex.getMessage() + ).then(Mono.just(new ResponseEntity<>("Internal Server Error", HttpStatus.INTERNAL_SERVER_ERROR))); + } + + return Mono.just(new ResponseEntity<>(status.getReasonPhrase(), status)); } }