Skip to content

Commit 2ef3069

Browse files
committed
✨ [feat] 예외 발생 시 알림 전송 기능 추가
1 parent b5b1840 commit 2ef3069

File tree

5 files changed

+180
-4
lines changed

5 files changed

+180
-4
lines changed

Backend_Config

build.gradle

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,6 @@ dependencies {
3333
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
3434
//webflux
3535
implementation 'org.springframework.boot:spring-boot-starter-webflux'
36-
//jpa
37-
// implementation 'mysql:mysql-connector-java:8.0.33'
38-
// implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
3936
//neo4j
4037
implementation 'org.springframework.boot:spring-boot-starter-data-neo4j'
4138
// openAi
@@ -48,6 +45,8 @@ dependencies {
4845
implementation 'sh.platform:config:2.2.2'
4946
implementation 'org.springframework.boot:spring-boot-starter-validation'
5047
implementation 'com.fasterxml.jackson.core:jackson-databind'
48+
//AOP
49+
implementation 'org.springframework.boot:spring-boot-starter-aop'
5150
}
5251

5352
tasks.named('test') {
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package com.going.server.global.common;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.aspectj.lang.JoinPoint;
6+
import org.aspectj.lang.annotation.AfterThrowing;
7+
import org.aspectj.lang.annotation.Aspect;
8+
import org.springframework.boot.web.client.RestTemplateBuilder;
9+
import org.springframework.context.annotation.Profile;
10+
import org.springframework.core.env.Environment;
11+
import org.springframework.core.io.ByteArrayResource;
12+
import org.springframework.http.HttpEntity;
13+
import org.springframework.http.HttpHeaders;
14+
import org.springframework.http.MediaType;
15+
import org.springframework.http.ResponseEntity;
16+
import org.springframework.scheduling.annotation.Async;
17+
import org.springframework.stereotype.Component;
18+
import org.springframework.util.LinkedMultiValueMap;
19+
import org.springframework.util.MultiValueMap;
20+
import org.springframework.web.client.RestTemplate;
21+
22+
import java.nio.charset.StandardCharsets;
23+
import java.time.LocalDateTime;
24+
import java.time.format.DateTimeFormatter;
25+
import java.util.ArrayList;
26+
import java.util.List;
27+
import java.util.Map;
28+
29+
30+
@Slf4j
31+
@Aspect
32+
@Component
33+
@Profile("!dev & !test")
34+
public class ExceptionDiscordLoggingAspect {
35+
private String webhookUrl;
36+
37+
private final RestTemplate restTemplate;
38+
private final ObjectMapper objectMapper;
39+
private final Environment environment;
40+
41+
public ExceptionDiscordLoggingAspect(
42+
RestTemplateBuilder restTemplateBuilder, ObjectMapper objectMapper, Environment environment) {
43+
this.restTemplate = restTemplateBuilder.build();
44+
this.objectMapper = objectMapper;
45+
this.environment = environment;
46+
}
47+
48+
private boolean isNotificationDisabled() {
49+
return environment.acceptsProfiles("dev", "test");
50+
}
51+
52+
private boolean shouldSkipNotification(Throwable ex) {
53+
SkipNotification skipNotification = ex.getClass().getAnnotation(SkipNotification.class);
54+
if (skipNotification == null) {
55+
return false;
56+
}
57+
return skipNotification.value();
58+
}
59+
60+
@AfterThrowing(pointcut = "within(com..*)", throwing = "ex")
61+
public void handleException(JoinPoint joinPoint, Throwable ex) {
62+
if (isNotificationDisabled()) {
63+
return;
64+
}
65+
66+
try {
67+
// log.info("Webhook URL: {}", webhookUrl);
68+
if (shouldSkipNotification(ex)) {
69+
log.info("알림을 보내지 않음 : {}", ex.getClass().getName());
70+
return;
71+
}
72+
List<Map<String, Object>> fields = buildFields(joinPoint, ex);
73+
Map<String, Object> embed = Map.of("title", "서버 예외 발생", "color", 16711680, "fields", fields);
74+
Map<String, Object> payload = Map.of("embeds", List.of(embed));
75+
String fullStackTrace = buildStackTrace(ex);
76+
sendToDiscord(objectMapper.writeValueAsString(payload), fullStackTrace);
77+
} catch (Exception e) {
78+
log.warn("디스코드 예외 전송 실패: {}", e.getMessage());
79+
}
80+
}
81+
82+
private List<Map<String, Object>> buildFields(JoinPoint joinPoint, Throwable ex) {
83+
List<Map<String, Object>> fields = new ArrayList<>();
84+
85+
fields.add(Map.of("name", "Exception", "value", ex.getClass().getName(), "inline", false));
86+
fields.add(
87+
Map.of(
88+
"name",
89+
"Message",
90+
"value",
91+
ex.getMessage() != null ? ex.getMessage() : "No message",
92+
"inline",
93+
false));
94+
fields.add(
95+
Map.of(
96+
"name",
97+
"Location",
98+
"value",
99+
joinPoint.getSignature().toShortString(),
100+
"inline",
101+
false));
102+
fields.add(
103+
Map.of(
104+
"name",
105+
"Time",
106+
"value",
107+
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
108+
"inline",
109+
false));
110+
111+
return fields;
112+
}
113+
114+
private String buildStackTrace(Throwable ex) {
115+
StringBuilder sb = new StringBuilder();
116+
for (StackTraceElement element : ex.getStackTrace()) {
117+
sb.append(element.toString()).append("\n");
118+
}
119+
return sb.toString();
120+
}
121+
122+
@Async
123+
protected void sendToDiscord(String payloadJson, String fullStackTrace) {
124+
try {
125+
if (isNotificationDisabled()) {
126+
return;
127+
}
128+
byte[] traceBytes = fullStackTrace.getBytes(StandardCharsets.UTF_8);
129+
ByteArrayResource traceFile = new TraceFileResource(traceBytes, "stacktrace.txt");
130+
131+
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
132+
body.add("payload_json", payloadJson);
133+
body.add("file", traceFile);
134+
135+
HttpHeaders headers = new HttpHeaders();
136+
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
137+
138+
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
139+
140+
ResponseEntity<String> response =
141+
restTemplate.postForEntity(webhookUrl, entity, String.class);
142+
log.info("디스코드 응답: {}", response.getStatusCode());
143+
144+
} catch (Exception e) {
145+
log.error("디스코드 전송 실패: {}", e.getMessage(), e);
146+
}
147+
}
148+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.going.server.global.common;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
@Target(ElementType.TYPE)
9+
@Retention(RetentionPolicy.RUNTIME)
10+
public @interface SkipNotification {
11+
boolean value() default true;
12+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.going.server.global.common;
2+
3+
import org.springframework.core.io.ByteArrayResource;
4+
5+
public class TraceFileResource extends ByteArrayResource {
6+
private final String fileName;
7+
8+
public TraceFileResource(byte[] byteArray, String fileName) {
9+
super(byteArray);
10+
this.fileName = fileName;
11+
}
12+
13+
@Override
14+
public String getFilename() {
15+
return fileName;
16+
}
17+
}

0 commit comments

Comments
 (0)