Skip to content

Commit adebc7a

Browse files
authored
Merge pull request #57 from Central-MakeUs/feature/#53-aop-logging
모든 Request에 대해 로깅을 적용한다.
2 parents b56dd09 + 55631d8 commit adebc7a

File tree

10 files changed

+628
-13
lines changed

10 files changed

+628
-13
lines changed

build.gradle

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ plugins {
22
id 'java'
33
id 'org.springframework.boot' version '3.5.3'
44
id 'io.spring.dependency-management' version '1.1.7'
5+
id "io.sentry.jvm.gradle" version "5.9.0"
56
}
67

78
group = 'akuma'
@@ -13,6 +14,14 @@ java {
1314
}
1415
}
1516

17+
sentry {
18+
includeSourceContext = true
19+
20+
org = "akuma-ir"
21+
projectName = "nuntteo_dev"
22+
authToken = System.getenv("SENTRY_AUTH_TOKEN")
23+
}
24+
1625
configurations {
1726
compileOnly {
1827
extendsFrom annotationProcessor
@@ -30,6 +39,7 @@ dependencies {
3039
implementation 'org.springframework.boot:spring-boot-starter-security'
3140
implementation 'org.springframework.boot:spring-boot-starter-validation'
3241
implementation 'org.springframework.boot:spring-boot-starter-webflux' // 버전 명시 X (BOM 관리)
42+
implementation 'org.springframework.boot:spring-boot-starter-aop'
3343

3444
// --- Swagger/OpenAPI ---
3545
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
@@ -52,6 +62,10 @@ dependencies {
5262
implementation 'com.google.apis:google-api-services-sheets:v4-rev612-1.25.0'
5363
implementation 'com.google.firebase:firebase-admin:9.2.0'
5464

65+
// Sentry BOM 추가하여 버전 관리 일원화
66+
implementation platform("io.sentry:sentry-bom:8.19.0")
67+
implementation "io.sentry:sentry-spring-boot-starter-jakarta"
68+
5569
// --- Lombok ---
5670
compileOnly 'org.projectlombok:lombok'
5771
annotationProcessor 'org.projectlombok:lombok'
@@ -73,15 +87,5 @@ dependencies {
7387
testImplementation platform('org.testcontainers:testcontainers-bom:1.21.3')
7488
testImplementation 'org.testcontainers:junit-jupiter'
7589
testImplementation 'org.testcontainers:mysql'
76-
77-
// (선택) 일부 CI 환경에서 필요한 경우만 사용
78-
testRuntimeOnly 'net.bytebuddy:byte-buddy-agent:1.14.17'
7990
}
8091

81-
tasks.withType(Test).configureEach {
82-
useJUnitPlatform()
83-
// Mockito가 ByteBuddy 에이전트를 self-attach 할 수 있게 허용 (CI에서 static/final 모킹 이슈 방지)
84-
jvmArgs += ['-Djdk.attach.allowAttachSelf=true']
85-
// JDK 21+를 쓰는 CI라면 아래 한 줄도 고려
86-
// jvmArgs += ['-XX:+EnableDynamicAgentLoading']
87-
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package akuma.whiplash.global.config.sentry;
2+
3+
import io.sentry.SentryOptions;
4+
import org.springframework.context.annotation.Bean;
5+
import org.springframework.context.annotation.Configuration;
6+
7+
@Configuration
8+
public class SentryConfig {
9+
10+
@Bean
11+
public SentryOptions.BeforeSendCallback beforeSendCallback() {
12+
return (event, hint) -> {
13+
if (event.getExceptions() != null && !event.getExceptions().isEmpty()) {
14+
var ex = event.getExceptions().get(0);
15+
if (ex.getType() != null && ex.getType().endsWith("ApplicationException")) {
16+
// 전역 핸들러에서 미리 심어둔 태그
17+
String code = event.getTag("error.code");
18+
if (code != null) {
19+
String originalMessage = ex.getValue(); // 기존 사람이 읽는 메시지
20+
// 타이틀에 반영되는 'type'을 에러코드로 교체
21+
ex.setType(code);
22+
// 부제목(value)은 원래 메시지를 유지(또는 필요 시 축약)
23+
ex.setValue(originalMessage);
24+
// 참고용으로 원본도 extra에 보존
25+
event.setExtra("original.message", originalMessage);
26+
}
27+
}
28+
}
29+
return event;
30+
};
31+
}
32+
}

src/main/java/akuma/whiplash/global/exception/GlobalExceptionHandler.java

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
package akuma.whiplash.global.exception;
22

3+
import akuma.whiplash.global.log.LogUtils;
34
import akuma.whiplash.global.response.ApplicationResponse;
45
import akuma.whiplash.global.response.code.BaseErrorCode;
56
import akuma.whiplash.global.response.code.CommonErrorCode;
7+
import io.sentry.Sentry;
68
import jakarta.servlet.http.HttpServletRequest;
79
import jakarta.validation.ConstraintViolation;
810
import jakarta.validation.ConstraintViolationException;
911
import java.util.LinkedHashMap;
12+
import java.util.List;
1013
import java.util.Map;
1114
import java.util.Optional;
15+
import java.util.stream.Collectors;
1216
import lombok.extern.slf4j.Slf4j;
1317
import org.springframework.http.HttpHeaders;
1418
import org.springframework.http.HttpStatus;
@@ -45,6 +49,14 @@ public ResponseEntity<Object> handleMethodArgumentNotValid(
4549
(existingErrorMessage, newErrorMessage) -> existingErrorMessage + ", " + newErrorMessage);
4650
});
4751

52+
sendErrorToSentry(
53+
e,
54+
extractRequestUri(request),
55+
LogUtils.maskSensitiveQuery(extractQueryString(request)),
56+
CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.getCustomCode(),
57+
CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.getHttpStatus()
58+
);
59+
4860
return handleExceptionInternalArgs(
4961
e,
5062
request,
@@ -59,6 +71,18 @@ public ResponseEntity<Object> validation(ConstraintViolationException e, WebRequ
5971
.findFirst()
6072
.orElseThrow(() -> new RuntimeException("ConstraintViolationException Error"));
6173

74+
sendErrorToSentry(
75+
e,
76+
extractRequestUri(request),
77+
LogUtils.maskSensitiveQuery(
78+
e.getConstraintViolations().stream()
79+
.map(v -> v.getPropertyPath() + "=" + v.getInvalidValue())
80+
.collect(Collectors.joining(", "))
81+
),
82+
errorMessage,
83+
CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.getHttpStatus()
84+
);
85+
6286
return handleExceptionInternalConstraint(e, CommonErrorCode.valueOf(errorMessage), request);
6387
}
6488

@@ -74,13 +98,30 @@ protected ResponseEntity<Object> handleHttpMessageNotReadable(
7498
CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.getCustomCode(),
7599
CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.getMessage()
76100
);
101+
102+
sendErrorToSentry(
103+
ex,
104+
extractRequestUri(request),
105+
LogUtils.maskSensitiveQuery(extractQueryString(request)),
106+
CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.getCustomCode(),
107+
CommonErrorCode.METHOD_ARGUMENT_NOT_VALID.getHttpStatus()
108+
);
109+
77110
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
78111
}
79112

80113
@ExceptionHandler
81114
public ResponseEntity<Object> exception(Exception e, WebRequest request) {
82115
log.error("Unexpected error: ", e);
83116

117+
sendErrorToSentry(
118+
e,
119+
request.getDescription(false),
120+
request.getParameterMap().toString(),
121+
CommonErrorCode.INTERNAL_SERVER_ERROR.getCustomCode(),
122+
CommonErrorCode.INTERNAL_SERVER_ERROR.getHttpStatus()
123+
);
124+
84125
return handleExceptionInternalFalse(
85126
e,
86127
CommonErrorCode.INTERNAL_SERVER_ERROR.getHttpStatus(),
@@ -90,10 +131,18 @@ public ResponseEntity<Object> exception(Exception e, WebRequest request) {
90131
}
91132

92133
@ExceptionHandler(value = ApplicationException.class)
93-
public ResponseEntity<Object> onThrowException(ApplicationException applicationException, HttpServletRequest request) {
94-
BaseErrorCode baseErrorCode = applicationException.getCode();
134+
public ResponseEntity<Object> onThrowException(ApplicationException ex, HttpServletRequest request) {
135+
BaseErrorCode baseErrorCode = ex.getCode();
95136

96-
return handleExceptionInternal(applicationException, baseErrorCode, null, request);
137+
sendErrorToSentry(
138+
ex,
139+
request.getRequestURI(),
140+
LogUtils.maskSensitiveQuery(request.getQueryString()),
141+
baseErrorCode.getCustomCode(),
142+
baseErrorCode.getHttpStatus()
143+
);
144+
145+
return handleExceptionInternal(ex, baseErrorCode, null, request);
97146
}
98147

99148
private ResponseEntity<Object> handleExceptionInternal(
@@ -179,4 +228,40 @@ private ResponseEntity<Object> handleExceptionInternalConstraint(
179228
request
180229
);
181230
}
231+
232+
private static String extractRequestUri(WebRequest request) {
233+
if (request instanceof ServletWebRequest servletWebRequest) {
234+
return servletWebRequest.getRequest().getRequestURI();
235+
}
236+
String desc = request.getDescription(false); // ex: "uri=/api/alarms/100/checkin"
237+
if (desc != null && desc.startsWith("uri=")) return desc.substring(4);
238+
return desc;
239+
}
240+
241+
private static String extractQueryString(WebRequest request) {
242+
if (request instanceof ServletWebRequest servletWebRequest) {
243+
return servletWebRequest.getRequest().getQueryString();
244+
}
245+
return null;
246+
}
247+
248+
private static void sendErrorToSentry(Exception ex, String requestUri, String queryString, String errorCode, HttpStatus status) {
249+
if (status.is5xxServerError()) {
250+
Sentry.withScope(scope -> {
251+
scope.setTransaction(requestUri);
252+
scope.setTag("path", requestUri);
253+
254+
if (errorCode != null && !errorCode.isBlank()) {
255+
scope.setTag("error.code", errorCode);
256+
scope.setFingerprint(List.of(errorCode));
257+
}
258+
259+
if (queryString != null && !queryString.isBlank()) {
260+
scope.setExtra("query", queryString);
261+
}
262+
263+
Sentry.captureException(ex);
264+
});
265+
}
266+
}
182267
}

0 commit comments

Comments
 (0)