Skip to content

Commit

Permalink
Merge pull request #5 from KUSITMS-MOAMOA/feat/#4
Browse files Browse the repository at this point in the history
[Feat/#4] 에러 발생 시 Discord 알림 전송 기능 구현
  • Loading branch information
daeun084 authored Jan 24, 2025
2 parents c1a6190 + 5492ee0 commit 32aab41
Show file tree
Hide file tree
Showing 16 changed files with 240 additions and 102 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
// Open Ai
implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
// AOP
implementation 'org.springframework.boot:spring-boot-starter-aop'
}

dependencyManagement {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package corecord.dev.common.exception;

import corecord.dev.common.status.ErrorStatus;
import corecord.dev.common.base.BaseErrorStatus;
import lombok.Getter;

@Getter
public class GeneralException extends RuntimeException{
private final ErrorStatus errorStatus;
protected final BaseErrorStatus errorStatus;

public GeneralException(ErrorStatus errorStatus) {
public GeneralException(BaseErrorStatus errorStatus) {
super(errorStatus.getMessage());
this.errorStatus = errorStatus;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@

import corecord.dev.common.response.ApiResponse;
import corecord.dev.common.status.ErrorStatus;
import corecord.dev.domain.ability.exception.AbilityException;
import corecord.dev.domain.analysis.exception.AnalysisException;
import corecord.dev.domain.auth.exception.TokenException;
import corecord.dev.domain.chat.exception.ChatException;
import corecord.dev.domain.folder.exception.FolderException;
import corecord.dev.domain.record.exception.RecordException;
import corecord.dev.domain.user.exception.UserException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpHeaders;
Expand All @@ -29,57 +23,9 @@

@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class GeneralExceptionAdvice extends ResponseEntityExceptionHandler {

// UserException 처리
@ExceptionHandler(UserException.class)
public ResponseEntity<ApiResponse<Void>> handleUserException(UserException e) {
log.warn(">>>>>>>>UserException: {}", e.getUserErrorStatus().getMessage());
return ApiResponse.error(e.getUserErrorStatus());
}

// TokenException 처리
@ExceptionHandler(TokenException.class)
public ResponseEntity<ApiResponse<Void>> handleTokenException(TokenException e) {
log.warn(">>>>>>>>TokenException: {}", e.getTokenErrorStatus().getMessage());
return ApiResponse.error(e.getTokenErrorStatus());
}

// FolderException 처리
@ExceptionHandler(FolderException.class)
public ResponseEntity<ApiResponse<Void>> handleFolderException(FolderException e) {
log.warn(">>>>>>>>FolderException: {}", e.getFolderErrorStatus().getMessage());
return ApiResponse.error(e.getFolderErrorStatus());
}

// RecordException 처리
@ExceptionHandler(RecordException.class)
public ResponseEntity<ApiResponse<Void>> handleRecordException(RecordException e) {
log.warn(">>>>>>>>RecordException: {}", e.getRecordErrorStatus().getMessage());
return ApiResponse.error(e.getRecordErrorStatus());
}

// AnalysisException 처리
@ExceptionHandler(AnalysisException.class)
public ResponseEntity<ApiResponse<Void>> handleAnalysisException(AnalysisException e) {
log.warn(">>>>>>>>AnalysisException: {}", e.getAnalysisErrorStatus().getMessage());
return ApiResponse.error(e.getAnalysisErrorStatus());
}

// AbilityException 처리
@ExceptionHandler(AbilityException.class)
public ResponseEntity<ApiResponse<Void>> handleAbilityException(AbilityException e) {
log.warn(">>>>>>>>AbilityException: {}", e.getAbilityErrorStatus().getMessage());
return ApiResponse.error(e.getAbilityErrorStatus());
}

// ChatException 처리
@ExceptionHandler(ChatException.class)
public ResponseEntity<ApiResponse<Void>> handleChatException(ChatException e) {
log.warn(">>>>>>>>ChatException: {}", e.getChatErrorStatus().getMessage());
return ApiResponse.error(e.getChatErrorStatus());
}

// GeneralException 처리
@ExceptionHandler(GeneralException.class)
public ResponseEntity<ApiResponse<Void>> handleGeneralException(GeneralException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package corecord.dev.common.log.discord;

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

import java.util.Arrays;

@Slf4j
@Component
@RequiredArgsConstructor
public class DiscordAlarmSender {

private final Environment environment;
@Value("${logging.discord.web-hook-url}")
private String webHookUrl;

private final DiscordUtil discordUtil;
private final WebClient webClient = WebClient.create();

public Void sendDiscordAlarm(Exception exception, HttpServletRequest httpServletRequest) {
if (Arrays.asList(environment.getActiveProfiles()).contains("dev")) {
return webClient.post()
.uri(webHookUrl)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(discordUtil.createMessage(exception, httpServletRequest))
.retrieve()
.bodyToMono(Void.class)
.block();
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package corecord.dev.common.log.discord;

import corecord.dev.common.exception.GeneralException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class DiscordLoggerAop {

private final DiscordAlarmSender discordAlarmSender;

@Pointcut("execution(* corecord.dev.common.exception.GeneralExceptionAdvice..*(..))")
public void generalExceptionErrorLoggerExecute() {}

@Before("generalExceptionErrorLoggerExecute()")
public void serverErrorLogging(JoinPoint joinpoint) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
Object[] args = joinpoint.getArgs();

if (args[0] instanceof GeneralException exception) {
if (exception.getErrorStatus().getHttpStatus() == HttpStatus.INTERNAL_SERVER_ERROR)
discordAlarmSender.sendDiscordAlarm(exception, request);
} else {
Exception exception = (Exception) args[0];
discordAlarmSender.sendDiscordAlarm(exception, request);
}
}
}
62 changes: 62 additions & 0 deletions src/main/java/corecord/dev/common/log/discord/DiscordUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package corecord.dev.common.log.discord;

import corecord.dev.common.log.discord.dto.DiscordDto;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.security.Principal;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;


@Component
public class DiscordUtil {
public DiscordDto.MessageDto createMessage(Exception exception, HttpServletRequest httpServletRequest) {
return DiscordDto.MessageDto.builder()
.content("# 🚨 서버 에러 발생 🚨")
.embeds(List.of(DiscordDto.EmbedDto.builder()
.title("에러 정보")
.description("### 에러 발생 시간\n"
+ ZonedDateTime.now(ZoneId.of("Asia/Seoul")).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH시 mm분 ss초"))
+ "\n"
+ "### 요청 엔드포인트\n"
+ getEndPoint(httpServletRequest)
+ "\n"
+ "### 요청 클라이언트\n"
+ getClient(httpServletRequest)
+"\n"
+ "### 에러 스택 트레이스\n"
+ "```\n"
+ getStackTrace(exception).substring(0, 1000)
+ "\n```")
.build()
)
).build();
}

private String getClient(HttpServletRequest httpServletRequest) {
String ip = httpServletRequest.getRemoteAddr();

Principal principal = httpServletRequest.getUserPrincipal();
if (principal != null) {
return "[IP] : " + ip + " / [Id] : " + principal.getName();
}
return "[IP] : " + ip;
}

private String getEndPoint(HttpServletRequest httpServletRequest) {
String method = httpServletRequest.getMethod();
String url = httpServletRequest.getRequestURI();
return method + " " + url;
}

private String getStackTrace(Exception exception) {
StringWriter stringWriter = new StringWriter();
exception.printStackTrace(new PrintWriter(stringWriter));
return stringWriter.toString();
}
}
35 changes: 35 additions & 0 deletions src/main/java/corecord/dev/common/log/discord/dto/DiscordDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package corecord.dev.common.log.discord.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;

public class DiscordDto {
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public static class MessageDto {
@JsonProperty("content")
private String content;

@JsonProperty("embeds")
private List<EmbedDto> embeds;
}

@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public static class EmbedDto {
@JsonProperty("title")
private String title;

@JsonProperty("description")
private String description;
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
package corecord.dev.domain.ability.exception;

import corecord.dev.domain.ability.status.AbilityErrorStatus;
import lombok.AllArgsConstructor;
import corecord.dev.common.base.BaseErrorStatus;
import corecord.dev.common.exception.GeneralException;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class AbilityException extends RuntimeException {
public class AbilityException extends GeneralException {

private final AbilityErrorStatus abilityErrorStatus;
public AbilityException(BaseErrorStatus errorStatus) {
super(errorStatus);
}

@Override
public String getMessage() {
return abilityErrorStatus.getMessage();
return errorStatus.getMessage();
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package corecord.dev.domain.analysis.exception;

import corecord.dev.domain.analysis.status.AnalysisErrorStatus;
import lombok.AllArgsConstructor;
import corecord.dev.common.base.BaseErrorStatus;
import corecord.dev.common.exception.GeneralException;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class AnalysisException extends RuntimeException {
private final AnalysisErrorStatus analysisErrorStatus;
public class AnalysisException extends GeneralException {

public AnalysisException(BaseErrorStatus errorStatus) {
super(errorStatus);
}

@Override
public String getMessage() {
return analysisErrorStatus.getMessage();
return errorStatus.getMessage();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
@AllArgsConstructor
public enum AnalysisErrorStatus implements BaseErrorStatus {
OVERFLOW_ANALYSIS_CONTENT(HttpStatus.BAD_REQUEST, "E0400_OVERFLOW_CONTENT", "경험 기록 내용은 500자 이내여야 합니다."),
OVERFLOW_ANALYSIS_COMMENT(HttpStatus.INTERNAL_SERVER_ERROR, "E0500_OVERFLOW_COMMENT", "경험 기록 코멘트는 200자 이내여야 합니다."),
OVERFLOW_ANALYSIS_KEYWORD_CONTENT(HttpStatus.INTERNAL_SERVER_ERROR, "E0500_OVERFLOW_KEYWORD_CONTENT", "경험 기록 키워드별 내용은 200자 이내여야 합니다."),
OVERFLOW_ANALYSIS_COMMENT(HttpStatus.BAD_REQUEST, "E0400_OVERFLOW_COMMENT", "경험 기록 코멘트는 200자 이내여야 합니다."),
OVERFLOW_ANALYSIS_KEYWORD_CONTENT(HttpStatus.BAD_REQUEST, "E0400_OVERFLOW_KEYWORD_CONTENT", "경험 기록 키워드별 내용은 200자 이내여야 합니다."),
USER_ANALYSIS_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "E401_ANALYSIS_UNAUTHORIZED", "유저가 역량 분석에 대한 권한이 없습니다."),
ANALYSIS_NOT_FOUND(HttpStatus.NOT_FOUND, "E0404_ANALYSIS", "존재하지 않는 역량 분석입니다."),
INVALID_ABILITY_ANALYSIS(HttpStatus.INTERNAL_SERVER_ERROR, "E500_INVALID_ANALYSIS", "역량 분석 데이터 파싱 중 오류가 발생했습니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package corecord.dev.domain.auth.exception;

import corecord.dev.domain.auth.status.TokenErrorStatus;
import corecord.dev.common.base.BaseErrorStatus;
import corecord.dev.common.exception.GeneralException;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class TokenException extends RuntimeException {
private final TokenErrorStatus tokenErrorStatus;
public class TokenException extends GeneralException {

public TokenException(BaseErrorStatus errorStatus) {
super(errorStatus);
}

@Override
public String getMessage() {
return tokenErrorStatus.getMessage();
return errorStatus.getMessage();
}
}
2 changes: 1 addition & 1 deletion src/main/java/corecord/dev/domain/auth/jwt/JwtFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ private void handleTokenException(HttpServletResponse response, TokenException e
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
String jsonResponse = String.format("{\"isSuccess\": \"false\", \"code\": \"%s\", \"message\": \"%s\"}", e.getTokenErrorStatus().getCode(), e.getMessage());
String jsonResponse = String.format("{\"isSuccess\": \"false\", \"code\": \"%s\", \"message\": \"%s\"}", e.getErrorStatus().getCode(), e.getMessage());
response.getWriter().write(jsonResponse);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package corecord.dev.domain.chat.exception;

import corecord.dev.common.base.BaseErrorStatus;
import corecord.dev.common.exception.GeneralException;
import corecord.dev.domain.chat.status.ChatErrorStatus;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class ChatException extends RuntimeException {
private final ChatErrorStatus chatErrorStatus;
public class ChatException extends GeneralException {

public ChatException(BaseErrorStatus errorStatus) {
super(errorStatus);
}

@Override
public String getMessage() {
return chatErrorStatus.getMessage();
return errorStatus.getMessage();
}
}
Loading

0 comments on commit 32aab41

Please sign in to comment.