diff --git a/src/main/java/com/wooteco/wiki/global/exception/WikiExceptionHandler.java b/src/main/java/com/wooteco/wiki/global/exception/WikiExceptionHandler.java index 76ce0a6..49afcb6 100644 --- a/src/main/java/com/wooteco/wiki/global/exception/WikiExceptionHandler.java +++ b/src/main/java/com/wooteco/wiki/global/exception/WikiExceptionHandler.java @@ -2,8 +2,11 @@ import com.wooteco.wiki.global.common.ApiResponse; import com.wooteco.wiki.global.common.ApiResponseGenerator; +import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -14,21 +17,60 @@ public class WikiExceptionHandler { @ExceptionHandler(WikiException.class) public ApiResponse handle(WikiException exception) { - log.error(exception.getMessage(), exception); + logError( + exception, + exception.getErrorCode().name(), + exception.getMessage() + ); return ApiResponseGenerator.failure(exception.getErrorCode(), exception.getMessage(), exception.getHttpStatus()); } @ExceptionHandler(Exception.class) public ApiResponse handle(Exception exception) { - log.error(exception.getMessage(), exception); + logError( + exception, + ErrorCode.UNKNOWN_ERROR.name(), + "An unknown error occurred." + ); return ApiResponseGenerator.failure(ErrorCode.UNKNOWN_ERROR, "An unknown error occurred.", HttpStatus.INTERNAL_SERVER_ERROR); } @ExceptionHandler(MethodArgumentNotValidException.class) public ApiResponse handle(MethodArgumentNotValidException exception) { - log.error(exception.getMessage(), exception); + logError( + exception, + ErrorCode.VALIDATION_ERROR.name(), + exception.getMessage() + ); return ApiResponseGenerator.failure(ErrorCode.VALIDATION_ERROR); } + + private void logError(Exception exception, String errorCode, String message) { + RequestInfo requestInfo = getRequestInfo(); + log.error( + "api_exception requestId={} httpMethod={} uri={} errorCode={} message={}", + requestInfo.requestId, + requestInfo.httpMethod, + requestInfo.uri, + errorCode, + message, + exception + ); + } + + private RequestInfo getRequestInfo() { + try { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()) + .getRequest(); + String requestId = request.getAttribute("requestId") instanceof String id ? id : "N/A"; + return new RequestInfo(requestId, request.getMethod(), request.getRequestURI()); + } catch (IllegalStateException e) { + return new RequestInfo("N/A", "N/A", "N/A"); + } + } + + private record RequestInfo(String requestId, String httpMethod, String uri) { + } } diff --git a/src/main/java/com/wooteco/wiki/logging/BusinessLogicLogger.java b/src/main/java/com/wooteco/wiki/logging/BusinessLogicLogger.java index 5c77d00..353cb27 100755 --- a/src/main/java/com/wooteco/wiki/logging/BusinessLogicLogger.java +++ b/src/main/java/com/wooteco/wiki/logging/BusinessLogicLogger.java @@ -1,7 +1,13 @@ package com.wooteco.wiki.logging; -import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; +import java.lang.reflect.Array; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Locale; +import java.util.Map; +import java.util.StringJoiner; +import java.util.UUID; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; @@ -10,7 +16,6 @@ import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; -import org.springframework.web.util.ContentCachingRequestWrapper; @Slf4j @Aspect @@ -18,58 +23,198 @@ @Order(1) public class BusinessLogicLogger { - @Around("execution(* com.wooteco.wiki.controller.*Controller.*(..)) " - + "|| execution(* com.wooteco.wiki.service.*Service.*(..)) " - + "|| execution(* com.wooteco.wiki.domain.*.*(..)) " - + "|| execution(* com.wooteco.wiki.repository.*Repository.*(..)) ") - public Object printLog(ProceedingJoinPoint joinPoint) throws Throwable { + private static final String NOT_AVAILABLE = "N/A"; + private static final String DOCUMENT_SERVICE_PACKAGE = "com.wooteco.wiki.document.service."; + private static final String ORGANIZATION_DOCUMENT_SERVICE_PACKAGE = "com.wooteco.wiki.organizationdocument.service."; + + @Around("execution(* com.wooteco.wiki..service..*(..))") + public Object traceCrud(ProceedingJoinPoint joinPoint) throws Throwable { String className = joinPoint.getSignature().getDeclaringTypeName(); String methodName = joinPoint.getSignature().getName(); - String type = getBusinessType(className); - if (className.contains("Controller")) { - printRequestBody(); + + if (!isDocumentService(className)) { + return joinPoint.proceed(); + } + + CrudAction crudAction = resolveCrudAction(methodName); + if (crudAction == CrudAction.NONE) { + return joinPoint.proceed(); } + + RequestInfo requestInfo = getRequestInfo(); + String arguments = summarizeArguments(joinPoint.getArgs()); + long start = System.currentTimeMillis(); + try { - HttpServletRequest request = getRequest(); - String requestId = (String) request.getAttribute("requestId"); - log.info("{} = {} {} {}.{}()", "RequestId", requestId, type, className, methodName); - } catch (IllegalStateException e) { - log.info("{} = {} {} {}.{}()", "Scheduler", className, type, className, methodName); + Object result = joinPoint.proceed(); + long durationMs = System.currentTimeMillis() - start; + + log.info( + "crud_trace status=SUCCESS action={} class={} method={} requestId={} httpMethod={} uri={} durationMs={} args={}", + crudAction, + className, + methodName, + requestInfo.requestId(), + requestInfo.httpMethod(), + requestInfo.uri(), + durationMs, + arguments + ); + return result; + } catch (Throwable throwable) { + long durationMs = System.currentTimeMillis() - start; + + log.error( + "crud_trace status=FAILED action={} class={} method={} requestId={} httpMethod={} uri={} durationMs={} errorType={} errorMessage={} args={}", + crudAction, + className, + methodName, + requestInfo.requestId(), + requestInfo.httpMethod(), + requestInfo.uri(), + durationMs, + throwable.getClass().getSimpleName(), + sanitize(throwable.getMessage()), + arguments + ); + throw throwable; + } + } + + private boolean isDocumentService(String className) { + return className.startsWith(DOCUMENT_SERVICE_PACKAGE) + || className.startsWith(ORGANIZATION_DOCUMENT_SERVICE_PACKAGE); + } + + private CrudAction resolveCrudAction(String methodName) { + String normalized = methodName.toLowerCase(Locale.ROOT); + + if (startsWithAny(normalized, "post", "create", "add", "link")) { + return CrudAction.CREATE; + } + if (startsWithAny(normalized, "put", "update", "modify", "flush")) { + return CrudAction.UPDATE; + } + if (startsWithAny(normalized, "delete", "remove", "unlink")) { + return CrudAction.DELETE; + } + if (startsWithAny(normalized, "get", "find", "search", "read")) { + return CrudAction.READ; + } + + return CrudAction.NONE; + } + + private boolean startsWithAny(String value, String... prefixes) { + for (String prefix : prefixes) { + if (value.startsWith(prefix)) { + return true; + } + } + return false; + } + + private String summarizeArguments(Object[] args) { + if (args == null || args.length == 0) { + return "[]"; + } + + StringJoiner stringJoiner = new StringJoiner(", ", "[", "]"); + for (Object arg : args) { + stringJoiner.add(summarizeArgument(arg)); } - return joinPoint.proceed(); + return stringJoiner.toString(); } - private String getBusinessType(String className) { - if (className.contains("Controller")) { - return "<>"; + private String summarizeArgument(Object arg) { + if (arg == null) { + return "null"; + } + if (arg instanceof UUID || arg instanceof Number || arg instanceof Boolean) { + return arg.toString(); + } + if (arg instanceof CharSequence text) { + return "'" + truncate(text.toString(), 80) + "'"; + } + if (arg instanceof Collection collection) { + return arg.getClass().getSimpleName() + "(size=" + collection.size() + ")"; } - if (className.contains("Service")) { - return "<>"; + if (arg instanceof Map map) { + return arg.getClass().getSimpleName() + "(size=" + map.size() + ")"; } - if (className.contains("Repository")) { - return "<>"; + if (arg.getClass().isArray()) { + return arg.getClass().getSimpleName() + "(length=" + Array.getLength(arg) + ")"; } - if (className.contains("Scheduler")) { - return "<>"; + + String identifier = extractIdentifier(arg); + if (identifier.isBlank()) { + return arg.getClass().getSimpleName(); + } + return arg.getClass().getSimpleName() + "(" + identifier + ")"; + } + + private String extractIdentifier(Object target) { + StringJoiner joiner = new StringJoiner(", "); + appendIdentifier(joiner, target, "getUuid", "uuid"); + appendIdentifier(joiner, target, "getId", "id"); + appendIdentifier(joiner, target, "getTitle", "title"); + appendIdentifier(joiner, target, "getWriter", "writer"); + return joiner.toString(); + } + + private void appendIdentifier(StringJoiner joiner, Object target, String getterName, String label) { + try { + Method method = target.getClass().getMethod(getterName); + if (method.getParameterCount() != 0) { + return; + } + Object value = method.invoke(target); + if (value != null) { + joiner.add(label + "=" + sanitize(value.toString())); + } + } catch (Exception ignored) { } - return ""; } - private void printRequestBody() { - HttpServletRequest request = getRequest(); - final ContentCachingRequestWrapper cachingRequest = (ContentCachingRequestWrapper) request; - String requestId = (String) cachingRequest.getAttribute("requestId"); - String logType = "<>"; - ObjectMapper objectMapper = new ObjectMapper(); + private RequestInfo getRequestInfo() { try { - log.info("{} = {} {} {} = \n{}", "RequestId", requestId, logType, "Body", - objectMapper.readTree(cachingRequest.getContentAsByteArray()).toPrettyString()); - } catch (Exception e) { - log.info("{} = {} {} {} = \n{}", "RequestId", requestId, logType, "Body", " "); + HttpServletRequest request = getRequest(); + String requestId = request.getAttribute("requestId") instanceof String id + ? id + : NOT_AVAILABLE; + String queryString = request.getQueryString(); + String uri = request.getRequestURI(); + if (queryString != null && !queryString.isBlank()) { + uri = uri + "?" + queryString; + } + return new RequestInfo(requestId, request.getMethod(), uri); + } catch (IllegalStateException exception) { + return new RequestInfo(NOT_AVAILABLE, NOT_AVAILABLE, NOT_AVAILABLE); } } - private static HttpServletRequest getRequest() { + private HttpServletRequest getRequest() { return ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); } + + private String sanitize(String value) { + if (value == null || value.isBlank()) { + return NOT_AVAILABLE; + } + return truncate(value.replace('\n', ' ').replace('\r', ' '), 120); + } + + private String truncate(String value, int maxLength) { + if (value.length() <= maxLength) { + return value; + } + return value.substring(0, maxLength) + "..."; + } + + private enum CrudAction { + CREATE, READ, UPDATE, DELETE, NONE + } + + private record RequestInfo(String requestId, String httpMethod, String uri) { + } }