diff --git a/src/main/java/org/runimo/runimo/common/log/HttpRequestLogAspect.java b/src/main/java/org/runimo/runimo/common/log/HttpRequestLogAspect.java new file mode 100644 index 00000000..7202f6f1 --- /dev/null +++ b/src/main/java/org/runimo/runimo/common/log/HttpRequestLogAspect.java @@ -0,0 +1,46 @@ +package org.runimo.runimo.common.log; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.runimo.runimo.common.log.model.HttpRequestLogInfo; +import org.runimo.runimo.common.log.model.MethodEndLogInfo; +import org.runimo.runimo.common.log.model.MethodStartLogInfo; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +@Aspect +@Slf4j +@RequiredArgsConstructor +@Component +public class HttpRequestLogAspect { + + private final LogMessageFormatter logMessageFormatter; + + @Pointcut("execution(* org.runimo.runimo..controller.*Controller.*(..))") + private void controller() { + } + + @Before("controller()") + public void apiRequestLogger() { + ServletRequestAttributes attributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()); + if (attributes == null) { + log.info("ServletRequestAttributes is null"); + return; + } + + HttpServletRequest request = attributes.getRequest(); + HttpRequestLogInfo logInfo = HttpRequestLogInfo.of(request); + + log.info(logMessageFormatter.toHttpRequestLogMessage(logInfo)); + } + +} diff --git a/src/main/java/org/runimo/runimo/common/log/LogMessageFormatter.java b/src/main/java/org/runimo/runimo/common/log/LogMessageFormatter.java new file mode 100644 index 00000000..c8ab24bb --- /dev/null +++ b/src/main/java/org/runimo/runimo/common/log/LogMessageFormatter.java @@ -0,0 +1,108 @@ +package org.runimo.runimo.common.log; + +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import org.runimo.runimo.common.log.model.HttpRequestLogInfo; +import org.runimo.runimo.common.log.model.MethodEndLogInfo; +import org.runimo.runimo.common.log.model.MethodStartLogInfo; +import org.springframework.stereotype.Component; + +@Component +public class LogMessageFormatter { // TODO : 중복코드 리팩토링 + + public String toHttpRequestLogMessage(HttpRequestLogInfo logInfo) { + String queryParamString = convertMapToLogFormatString(logInfo.queryParams(), + new StringBuilder()).toString(); + + Map logs = new LinkedHashMap<>(); + logs.put("method", logInfo.requestMethod()); + logs.put("uri", logInfo.uri()); + logs.put("query_params", queryParamString); + logs.put("time", getCurrentTime()); + + StringBuilder sb = new StringBuilder(); + sb.append("HTTP_REQUEST "); + + convertMapToLogFormatString(logs, sb); + + return sb.toString(); + } + + + public String toMethodStartLogMessage(MethodStartLogInfo logInfo) { + String paramString = convertMapToLogFormatString(logInfo.params(), + new StringBuilder()).toString(); + + Map logs = new LinkedHashMap<>(); + logs.put("name", logInfo.className() + "." + logInfo.methodName()); + logs.put("authenticated", String.valueOf(logInfo.authenticated())); + logs.put("user_id", logInfo.userId()); + logs.put("params", paramString); + logs.put("time", getCurrentTime()); + + StringBuilder sb = new StringBuilder(); + sb.append("METHOD_CALL "); + + convertMapToLogFormatString(logs, sb); + + return sb.toString(); + } + + public String toMethodEndLogMessage(MethodEndLogInfo logInfo) { + Map logs = new LinkedHashMap<>(); + logs.put("name", logInfo.className() + "." + logInfo.methodName()); + logs.put("elapsed_time", logInfo.elapsedTimeMillis() + "ms"); + logs.put("return", logInfo.returnData()); + logs.put("time", getCurrentTime()); + + StringBuilder sb = new StringBuilder(); + sb.append("METHOD_END "); + + convertMapToLogFormatString(logs, sb); + + return sb.toString(); + } + + public String toMethodErrorLogMessage(Throwable ex) { + StringBuilder sb = new StringBuilder(); + sb.append("METHOD_EXCEPTION "); + sb.append(ex.getMessage()); + + return sb.toString(); + } + + private StringBuilder convertMapToLogFormatString(Map infoMap, + StringBuilder sb) { + sb.append("["); + + Iterator> it = infoMap.entrySet().iterator(); + while (it.hasNext()) { + Entry entry = it.next(); + sb.append(convertCamelCaseToSnakeCase(entry.getKey())); + sb.append("="); + sb.append(entry.getValue()); + + if (it.hasNext()) { + sb.append(", "); + } + } + + sb.append("]"); + return sb; + } + + private String getCurrentTime() { + return ZonedDateTime.now().toString(); + } + + private String convertCamelCaseToSnakeCase(String camelCase) { + return camelCase + .replaceAll("([A-Z])(?=[A-Z])", "$1_") + .replaceAll("([a-z])([A-Z])", "$1_$2") + .toLowerCase(); + } +} diff --git a/src/main/java/org/runimo/runimo/common/log/MethodLogAspect.java b/src/main/java/org/runimo/runimo/common/log/MethodLogAspect.java new file mode 100644 index 00000000..75da9bc8 --- /dev/null +++ b/src/main/java/org/runimo/runimo/common/log/MethodLogAspect.java @@ -0,0 +1,66 @@ +package org.runimo.runimo.common.log; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.runimo.runimo.common.log.model.MethodEndLogInfo; +import org.runimo.runimo.common.log.model.MethodStartLogInfo; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +@Aspect +@Slf4j +@RequiredArgsConstructor +@Component +public class MethodLogAspect { + + private final LogMessageFormatter logMessageFormatter; + + @Pointcut("@within(org.runimo.runimo.common.log.ServiceLog) || @annotation(org.runimo.runimo.common.log.ServiceLog)") + private void annotatedClassAndMethod() { + } + + @Around("annotatedClassAndMethod()") + public Object calledMethodLogger(ProceedingJoinPoint pjp) throws Throwable { + MethodStartLogInfo methodStartLogInfo = getMethodStartLogInfo(pjp); + log.info(logMessageFormatter.toMethodStartLogMessage(methodStartLogInfo)); + + long startTime = getCurrentTimeMillis(); + long endTime; + Object proceedReturn = null; + try { + proceedReturn = pjp.proceed(); + } catch (Throwable ex) { + log.error(logMessageFormatter.toMethodErrorLogMessage(ex)); + } + endTime = getCurrentTimeMillis(); + + MethodEndLogInfo methodEndLogInfo = MethodEndLogInfo.of(pjp, endTime - startTime, + proceedReturn); + log.info(logMessageFormatter.toMethodEndLogMessage(methodEndLogInfo)); + + return proceedReturn; + } + + private static MethodStartLogInfo getMethodStartLogInfo(ProceedingJoinPoint pjp) { + MethodStartLogInfo methodStartLogInfo; + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + methodStartLogInfo = MethodStartLogInfo.of(pjp, true, authentication.getName()); + } else { + methodStartLogInfo = MethodStartLogInfo.of(pjp, false, null); + } + + return methodStartLogInfo; + } + + private long getCurrentTimeMillis() { + return System.currentTimeMillis(); + } + +} diff --git a/src/main/java/org/runimo/runimo/common/log/ServiceLog.java b/src/main/java/org/runimo/runimo/common/log/ServiceLog.java new file mode 100644 index 00000000..075ef71c --- /dev/null +++ b/src/main/java/org/runimo/runimo/common/log/ServiceLog.java @@ -0,0 +1,15 @@ +package org.runimo.runimo.common.log; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 비즈니스 로그 기록 어노테이션 - 메서드 실행 시 메서드명, 인자 목록, 반환값, 소요시간 등의 정보를 로깅 + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ServiceLog { + +} diff --git a/src/main/java/org/runimo/runimo/common/log/model/HttpRequestLogInfo.java b/src/main/java/org/runimo/runimo/common/log/model/HttpRequestLogInfo.java new file mode 100644 index 00000000..ef8ebf40 --- /dev/null +++ b/src/main/java/org/runimo/runimo/common/log/model/HttpRequestLogInfo.java @@ -0,0 +1,48 @@ +package org.runimo.runimo.common.log.model; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.StringTokenizer; +import lombok.AccessLevel; +import lombok.Builder; + +@Builder(access = AccessLevel.PRIVATE) +public record HttpRequestLogInfo( + String requestMethod, + String uri, + Map queryParams +) { + + public static HttpRequestLogInfo of(HttpServletRequest request) { + String queryString = request.getQueryString(); + + Map queryParams = getQueryParamMap(queryString); + + return HttpRequestLogInfo.builder() + .requestMethod(request.getMethod()) + .uri(request.getRequestURI()) + .queryParams(queryParams) + .build(); + } + + private static Map getQueryParamMap(String queryString) { + if (queryString == null || queryString.isBlank()) { + return Collections.emptyMap(); + } + + Map queryParams = new HashMap<>(); + + String[] paramPairs = queryString.split("&"); + for (String paramPair : paramPairs) { + String[] keyVal = paramPair.split("=", 2); + + String val = keyVal.length < 2 ? "" : keyVal[1]; + queryParams.put(keyVal[0], val); + } + + return queryParams; + } + +} diff --git a/src/main/java/org/runimo/runimo/common/log/model/MethodEndLogInfo.java b/src/main/java/org/runimo/runimo/common/log/model/MethodEndLogInfo.java new file mode 100644 index 00000000..bae58ce7 --- /dev/null +++ b/src/main/java/org/runimo/runimo/common/log/model/MethodEndLogInfo.java @@ -0,0 +1,29 @@ +package org.runimo.runimo.common.log.model; + +import lombok.AccessLevel; +import lombok.Builder; +import org.aspectj.lang.JoinPoint; + +@Builder(access = AccessLevel.PRIVATE) +public record MethodEndLogInfo( + String className, + String methodName, + long elapsedTimeMillis, + String returnData +) { + + public static MethodEndLogInfo of(JoinPoint joinPoint, long elapsedTimeMillis, + Object returnData) { + return MethodEndLogInfo.builder() + .className(getClassName(joinPoint)) + .methodName(joinPoint.getSignature().getName()) + .elapsedTimeMillis(elapsedTimeMillis) + .returnData(String.valueOf(returnData)) + .build(); + } + + private static String getClassName(JoinPoint joinPoint) { + String classPath = joinPoint.getSignature().getDeclaringTypeName(); + return classPath.substring(classPath.lastIndexOf(".") + 1); + } +} diff --git a/src/main/java/org/runimo/runimo/common/log/model/MethodStartLogInfo.java b/src/main/java/org/runimo/runimo/common/log/model/MethodStartLogInfo.java new file mode 100644 index 00000000..4d24a1ab --- /dev/null +++ b/src/main/java/org/runimo/runimo/common/log/model/MethodStartLogInfo.java @@ -0,0 +1,49 @@ +package org.runimo.runimo.common.log.model; + +import java.util.HashMap; +import java.util.Map; +import lombok.AccessLevel; +import lombok.Builder; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.reflect.MethodSignature; + +@Builder(access = AccessLevel.PRIVATE) +public record MethodStartLogInfo( + String className, + String methodName, + boolean authenticated, + String userId, + Map params +) { + + public static MethodStartLogInfo of(JoinPoint joinPoint, boolean authenticated, String userId) { + String className = getClassName(joinPoint); + + Map params = getParamMap(joinPoint); + + return MethodStartLogInfo.builder() + .className(className) + .methodName(joinPoint.getSignature().getName()) + .authenticated(authenticated) + .userId(userId) + .params(params) + .build(); + } + + private static Map getParamMap(JoinPoint joinPoint) { + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + String[] parameterNames = methodSignature.getParameterNames(); + Object[] parameterValues = joinPoint.getArgs(); + + Map params = new HashMap<>(); + for (int i = 0; i < parameterNames.length; i++) { + params.put(parameterNames[i], String.valueOf(parameterValues[i])); + } + return params; + } + + private static String getClassName(JoinPoint joinPoint) { + String classPath = joinPoint.getSignature().getDeclaringTypeName(); + return classPath.substring(classPath.lastIndexOf(".") + 1); + } +} diff --git a/src/main/java/org/runimo/runimo/common/response/ErrorResponse.java b/src/main/java/org/runimo/runimo/common/response/ErrorResponse.java index 2b069934..379f6557 100644 --- a/src/main/java/org/runimo/runimo/common/response/ErrorResponse.java +++ b/src/main/java/org/runimo/runimo/common/response/ErrorResponse.java @@ -1,7 +1,9 @@ package org.runimo.runimo.common.response; +import lombok.ToString; import org.runimo.runimo.exceptions.code.CustomResponseCode; +@ToString public class ErrorResponse extends Response { private ErrorResponse(final String errorMessage, final String errorCode) { diff --git a/src/main/java/org/runimo/runimo/common/response/Response.java b/src/main/java/org/runimo/runimo/common/response/Response.java index 4e151c2c..fb63ae60 100644 --- a/src/main/java/org/runimo/runimo/common/response/Response.java +++ b/src/main/java/org/runimo/runimo/common/response/Response.java @@ -2,8 +2,10 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; +import lombok.ToString; import org.runimo.runimo.exceptions.code.CustomResponseCode; +@ToString @Getter public class Response { diff --git a/src/main/java/org/runimo/runimo/common/response/SuccessResponse.java b/src/main/java/org/runimo/runimo/common/response/SuccessResponse.java index 42f6e941..e5a7c1bc 100644 --- a/src/main/java/org/runimo/runimo/common/response/SuccessResponse.java +++ b/src/main/java/org/runimo/runimo/common/response/SuccessResponse.java @@ -1,8 +1,10 @@ package org.runimo.runimo.common.response; import lombok.Getter; +import lombok.ToString; import org.runimo.runimo.exceptions.code.CustomResponseCode; +@ToString @Getter public class SuccessResponse extends Response { diff --git a/src/main/java/org/runimo/runimo/hatch/controller/HatchController.java b/src/main/java/org/runimo/runimo/hatch/controller/HatchController.java index 573c45e4..aa28db76 100644 --- a/src/main/java/org/runimo/runimo/hatch/controller/HatchController.java +++ b/src/main/java/org/runimo/runimo/hatch/controller/HatchController.java @@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.runimo.runimo.common.log.ServiceLog; import org.runimo.runimo.common.response.SuccessResponse; import org.runimo.runimo.hatch.service.dto.HatchEggResponse; import org.runimo.runimo.hatch.exception.HatchHttpResponseCode; @@ -23,6 +24,7 @@ public class HatchController { private final HatchUsecase hatchUsecase; + @ServiceLog @Operation(summary = "알 부화", description = "알을 부화합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "[HSH2011] 알 부화 성공"),