diff --git a/build.gradle b/build.gradle index 082ed8e..af06554 100644 --- a/build.gradle +++ b/build.gradle @@ -75,6 +75,11 @@ dependencies { // prometheus implementation("org.springframework.boot:spring-boot-starter-actuator") runtimeOnly("io.micrometer:micrometer-registry-prometheus") + + // slack + implementation("com.slack.api:bolt:1.44.2") + implementation("com.slack.api:bolt-servlet:1.44.2") + implementation("com.slack.api:bolt-jetty:1.44.2") } def querydslSrcDir = 'src/main/generated' diff --git a/src/main/java/com/avab/avab/apiPayload/exception/ExceptionAdvice.java b/src/main/java/com/avab/avab/apiPayload/exception/ExceptionAdvice.java index 15bff59..f4f2ba9 100644 --- a/src/main/java/com/avab/avab/apiPayload/exception/ExceptionAdvice.java +++ b/src/main/java/com/avab/avab/apiPayload/exception/ExceptionAdvice.java @@ -1,10 +1,6 @@ package com.avab.avab.apiPayload.exception; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.time.LocalDateTime; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import java.util.Optional; @@ -28,8 +24,6 @@ import com.avab.avab.apiPayload.BaseResponse; import com.avab.avab.apiPayload.code.ErrorReasonDTO; import com.avab.avab.apiPayload.code.status.ErrorStatus; -import com.avab.avab.feign.discord.dto.DiscordMessage; -import com.avab.avab.feign.discord.dto.DiscordMessage.Embed; import com.avab.avab.feign.discord.service.DiscordClient; import com.avab.avab.utils.EnvironmentHelper; @@ -43,6 +37,7 @@ public class ExceptionAdvice extends ResponseEntityExceptionHandler { private final DiscordClient discordClient; private final EnvironmentHelper environmentHelper; + private final ExceptionNotifier exceptionNotifier; @Override protected ResponseEntity handleTypeMismatch( @@ -109,10 +104,11 @@ public ResponseEntity handleMethodArgumentNotValid( @ExceptionHandler public ResponseEntity exception(Exception e, WebRequest request) { - e.printStackTrace(); + log.error("알 수 없는 예외 발생", e); - if (environmentHelper.isLocal()) { - sendDiscordAlarm(e, request); + if (!environmentHelper.isLocal()) { + // sendDiscordAlarm(e, request); + exceptionNotifier.notify(e, request); } return handleExceptionInternalFalse( @@ -187,48 +183,48 @@ private ResponseEntity handleExceptionInternalMessage( return super.handleExceptionInternal( e, body, headers, errorStatus.getHttpStatus(), request); } - - private void sendDiscordAlarm(Exception e, WebRequest request) { - discordClient.sendAlarm(createMessage(e, request)); - } - - private DiscordMessage createMessage(Exception e, WebRequest request) { - return DiscordMessage.builder() - .content("# 🚨 에러 발생 비이이이이사아아아앙") - .embeds( - List.of( - Embed.builder() - .title("ℹ️ 에러 정보") - .description( - "### 🕖 발생 시간\n" - + LocalDateTime.now() - + "\n" - + "### 🔗 요청 URL\n" - + createRequestFullPath(request) - + "\n" - + "### 📄 Stack Trace\n" - + "```\n" - + getStackTrace(e).substring(0, 1000) - + "\n```") - .build())) - .build(); - } - - private String createRequestFullPath(WebRequest webRequest) { - HttpServletRequest request = ((ServletWebRequest) webRequest).getRequest(); - String fullPath = request.getMethod() + " " + request.getRequestURL(); - - String queryString = request.getQueryString(); - if (queryString != null) { - fullPath += "?" + queryString; - } - - return fullPath; - } - - private String getStackTrace(Exception e) { - StringWriter stringWriter = new StringWriter(); - e.printStackTrace(new PrintWriter(stringWriter)); - return stringWriter.toString(); - } + // + // private void sendDiscordAlarm(Exception e, WebRequest request) { + // discordClient.sendAlarm(createMessage(e, request)); + // } + // + // private DiscordMessage createMessage(Exception e, WebRequest request) { + // return DiscordMessage.builder() + // .content("# 🚨 에러 발생 비이이이이사아아아앙") + // .embeds( + // List.of( + // Embed.builder() + // .title("ℹ️ 에러 정보") + // .description( + // "### 🕖 발생 시간\n" + // + LocalDateTime.now() + // + "\n" + // + "### 🔗 요청 URL\n" + // + createRequestFullPath(request) + // + "\n" + // + "### 📄 Stack Trace\n" + // + "```\n" + // + getStackTrace(e).substring(0, 1000) + // + "\n```") + // .build())) + // .build(); + // } + // + // private String createRequestFullPath(WebRequest webRequest) { + // HttpServletRequest request = ((ServletWebRequest) webRequest).getRequest(); + // String fullPath = request.getMethod() + " " + request.getRequestURL(); + // + // String queryString = request.getQueryString(); + // if (queryString != null) { + // fullPath += "?" + queryString; + // } + // + // return fullPath; + // } + // + // private String getStackTrace(Exception e) { + // StringWriter stringWriter = new StringWriter(); + // e.printStackTrace(new PrintWriter(stringWriter)); + // return stringWriter.toString(); + // } } diff --git a/src/main/java/com/avab/avab/apiPayload/exception/ExceptionNotifier.java b/src/main/java/com/avab/avab/apiPayload/exception/ExceptionNotifier.java new file mode 100644 index 0000000..73320b1 --- /dev/null +++ b/src/main/java/com/avab/avab/apiPayload/exception/ExceptionNotifier.java @@ -0,0 +1,86 @@ +package com.avab.avab.apiPayload.exception; + +import static com.slack.api.model.block.Blocks.*; +import static com.slack.api.model.block.composition.BlockCompositions.*; +import static com.slack.api.model.block.element.BlockElements.*; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.request.WebRequest; + +import com.avab.avab.slack.SlackChannel; +import com.avab.avab.slack.SlackService; +import com.slack.api.model.block.LayoutBlock; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class ExceptionNotifier { + private final SlackService slackService; + + public void notify(Exception e, WebRequest request) { + slackService.sendMessage(SlackChannel.SERVER_ERROR, createMessage(e, request)); + } + + private List createMessage(Exception e, WebRequest request) { + return asBlocks( + // 타이틀 + header(h -> h.text(plainText(pt -> pt.text("🚨 에러 발생")))), + + // 구분선 + divider(), + + // 에러 정보 + section(section -> section.text(markdownText("*ℹ️ 에러 정보*"))), + // 발생 시간 + section(section -> section.text(markdownText("*🕖 발생 시간*"))), + section( + section -> + section.text( + markdownText( + DateTimeFormatter.ofPattern( + "yyyy-MM-dd HH:mm:ss:SSS") + .format(LocalDateTime.now())))), + + // 요청 URL + section(section -> section.text(markdownText("*🔗 요청 URL*"))), + section(section -> section.text(markdownText(createRequestFullPath(request)))), + + // Stack Trace + section(section -> section.text(markdownText("*📄 Stack Trace*"))), + section( + section -> + section.text( + markdownText( + "```\n" + + getStackTrace(e).substring(0, 1000) + + "\n```")))); + } + + private String createRequestFullPath(WebRequest webRequest) { + HttpServletRequest request = ((ServletWebRequest) webRequest).getRequest(); + String fullPath = request.getMethod() + " " + request.getRequestURL(); + + String queryString = request.getQueryString(); + if (queryString != null) { + fullPath += "?" + queryString; + } + + return fullPath; + } + + private String getStackTrace(Exception e) { + StringWriter stringWriter = new StringWriter(); + e.printStackTrace(new PrintWriter(stringWriter)); + return stringWriter.toString(); + } +} diff --git a/src/main/java/com/avab/avab/slack/SlackChannel.java b/src/main/java/com/avab/avab/slack/SlackChannel.java new file mode 100644 index 0000000..12b25b8 --- /dev/null +++ b/src/main/java/com/avab/avab/slack/SlackChannel.java @@ -0,0 +1,12 @@ +package com.avab.avab.slack; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public enum SlackChannel { + SERVER_ERROR("C086U3W5KPU"); + + private final String channelId; +} diff --git a/src/main/java/com/avab/avab/slack/SlackService.java b/src/main/java/com/avab/avab/slack/SlackService.java new file mode 100644 index 0000000..cdb9e91 --- /dev/null +++ b/src/main/java/com/avab/avab/slack/SlackService.java @@ -0,0 +1,36 @@ +package com.avab.avab.slack; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import com.slack.api.Slack; +import com.slack.api.methods.request.chat.ChatPostMessageRequest; +import com.slack.api.methods.response.chat.ChatPostMessageResponse; +import com.slack.api.model.block.LayoutBlock; + +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class SlackService { + @Value("${slack.token}") + private String token; + + public void sendMessage(SlackChannel channel, List message) { + try { + ChatPostMessageRequest request = + ChatPostMessageRequest.builder() + .channel(channel.getChannelId()) + .blocks(message) + .build(); + + ChatPostMessageResponse response = + Slack.getInstance().methods(token).chatPostMessage(request); + System.out.println(response.getMessage()); + } catch (Exception e) { + log.error("슬랙 메시지 전송 실패: {}", e.getMessage()); + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b56d183..c65becb 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -30,6 +30,10 @@ management: http-basic: username: ${ACTUATOR_USERNAME} # 액츄에이터 http basic 인증 사용자 이름 password: ${ACTUATOR_PASSWORD} # 액츄에이터 http basic 인증 비밀번호 + +# slack +slack: + token: ${SLACK_TOKEN} --- # Local Profile spring: