diff --git a/src/main/java/gg/agit/konect/global/exception/GlobalExceptionHandler.java b/src/main/java/gg/agit/konect/global/exception/GlobalExceptionHandler.java index 1a5c8d2d..dcd9d8a6 100644 --- a/src/main/java/gg/agit/konect/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/gg/agit/konect/global/exception/GlobalExceptionHandler.java @@ -13,6 +13,7 @@ import org.apache.catalina.connector.ClientAbortException; import org.slf4j.Logger; +import org.slf4j.MDC; import org.springframework.web.multipart.MaxUploadSizeExceededException; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; @@ -44,6 +45,7 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { private static final Logger RUNTIME_ERROR_LOGGER = LoggerFactory.getLogger("runtime.error"); + private static final String REQUEST_ID_MDC_KEY = "requestId"; private static final String MASKED_HEADER_VALUE = "***"; private static final List SENSITIVE_HEADER_NAMES = List.of( "authorization", @@ -209,19 +211,21 @@ public ResponseEntity handleException(HttpServletRequest request, Except String exception = e.getClass().getSimpleName(); String location = String.format("%s:%d", origin.getFileName(), origin.getLineNumber()); String message = e.getMessage(); + String requestId = getRequestId(); String slackMessage = String.format( """ + Request ID: `%s` URI: `%s` Location: `%s` Exception: `%s` ```%s``` """, - uri, location, exception, message + requestId, uri, location, exception, message ); RUNTIME_ERROR_LOGGER.error(slackMessage); - requestDebugLogging(request); + requestDebugLogging(request, requestId); return buildErrorResponse(ApiResponseCode.UNEXPECTED_SERVER_ERROR); } @@ -272,17 +276,25 @@ private void requestLogging( String errorTraceId ) { log.warn("[{}] {} | errorTraceId={}", httpStatus, errorMessage, errorTraceId); - requestDebugLogging(request); + requestDebugLogging(request, getRequestId()); } - private void requestDebugLogging(HttpServletRequest request) { + private void requestDebugLogging(HttpServletRequest request, String requestId) { if (!log.isDebugEnabled()) { return; } - log.debug("Request: {} {}", request.getMethod(), request.getRequestURI()); - log.debug("Headers: {}", getLoggableHeaders(request)); - log.debug("Query String: {}", getQueryString(request)); - log.debug("Body: {}", getRequestBody(request)); + log.debug("Request [requestId: {}]: {} {}", requestId, request.getMethod(), request.getRequestURI()); + log.debug("Headers [requestId: {}]: {}", requestId, getLoggableHeaders(request)); + log.debug("Query String [requestId: {}]: {}", requestId, getQueryString(request)); + log.debug("Body [requestId: {}]: {}", requestId, getRequestBody(request)); + } + + private String getRequestId() { + String requestId = MDC.get(REQUEST_ID_MDC_KEY); + if (requestId == null) { + return " - "; + } + return requestId; } private Map getLoggableHeaders(HttpServletRequest request) { diff --git a/src/test/java/gg/agit/konect/unit/global/exception/GlobalExceptionHandlerTest.java b/src/test/java/gg/agit/konect/unit/global/exception/GlobalExceptionHandlerTest.java index 5b39ec07..89a8c4f0 100644 --- a/src/test/java/gg/agit/konect/unit/global/exception/GlobalExceptionHandlerTest.java +++ b/src/test/java/gg/agit/konect/unit/global/exception/GlobalExceptionHandlerTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.slf4j.LoggerFactory; +import org.slf4j.MDC; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.http.HttpStatus; @@ -34,12 +35,14 @@ void setUp() { @AfterEach void tearDown() { exceptionHandlerLogger.setLevel(originalLevel); + MDC.clear(); } @Test @DisplayName("예상하지 못한 예외도 디버그 로그에서 요청 본문을 확인할 수 있다") void logsRequestBodyAtDebugLevelForUnexpectedException(CapturedOutput output) { // given + MDC.put("requestId", "request-123"); GlobalExceptionHandler handler = new GlobalExceptionHandler(); MockHttpServletRequest request = new MockHttpServletRequest("POST", "/clubs"); request.setContentType("application/json"); @@ -61,21 +64,23 @@ void logsRequestBodyAtDebugLevelForUnexpectedException(CapturedOutput output) { // then assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); assertThat(output) - .contains("Request: POST /clubs") + .contains("Request ID: `request-123`") + .contains("Request [requestId: request-123]: POST /clubs") .contains("Authorization=***") .contains("Cookie=***") .contains("X-Request-ID=request-1") .doesNotContain("secret-token") .doesNotContain("secret-cookie") .contains( - "Query String: access%5Ftoken=***&access_token=***&code=***&name=KONECT&token=***&token=***" + "Query String [requestId: request-123]: " + + "access%5Ftoken=***&access_token=***&code=***&name=KONECT&token=***&token=***" ) .doesNotContain("encoded-query-secret") .doesNotContain("query-secret") .doesNotContain("oauth-code") .doesNotContain("repeat-secret-one") .doesNotContain("repeat-secret-two") - .contains("Body: {\"name\":\"KONECT\"}"); + .contains("Body [requestId: request-123]: {\"name\":\"KONECT\"}"); } @Test @@ -97,7 +102,7 @@ void skipsRequestDetailLoggingWhenDebugIsDisabled(CapturedOutput output) { // then assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); assertThat(output) - .doesNotContain("Request: POST /clubs") + .doesNotContain("Request [requestId:") .doesNotContain("Body: {\"name\":\"KONECT\"}"); } }