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 0000000..0d4e0bc --- /dev/null +++ b/user-service/src/main/java/kr/mafoo/user/handler/GlobalExceptionHandler.java @@ -0,0 +1,56 @@ +package kr.mafoo.user.handler; + +import kr.mafoo.user.service.SlackService; +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.ResponseStatusException; +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 SlackService slackService; + + @ExceptionHandler(ResponseStatusException.class) + public Mono> handleResponseStatusException(ServerWebExchange exchange, ResponseStatusException ex) { + return handleExceptionInternal(exchange, ex, (HttpStatus) ex.getStatusCode()); + } + + @ExceptionHandler(Exception.class) + 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"); + 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 {} {}", method, fullPath, originIp, ex.getMessage(), userAgent); + + 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)); + } +} 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 81f10d4..4b533c8 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(); } } diff --git a/user-service/src/main/resources/application.yaml b/user-service/src/main/resources/application.yaml index ce6440d..bfa2a3c 100644 --- a/user-service/src/main/resources/application.yaml +++ b/user-service/src/main/resources/application.yaml @@ -49,4 +49,4 @@ slack: token: ${SLACK_TOKEN} channel: error: ${SLACK_ERROR_CHANNEL} - member: ${SLACK_MEMBER_CHANNEL} + member: ${SLACK_MEMBER_CHANNEL} \ No newline at end of file