diff --git a/.gitignore b/.gitignore index 118c7228..5eb3ca7c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ build/ !**/src/main/**/build/ !**/src/test/**/build/ *.env +/logs ### IntelliJ IDEA ### .idea diff --git a/app/external-api/build.gradle b/app/external-api/build.gradle index ed483a5f..61ea2473 100644 --- a/app/external-api/build.gradle +++ b/app/external-api/build.gradle @@ -22,6 +22,9 @@ dependencies { // Token Bucket implementation 'com.bucket4j:bucket4j_jdk17-core:8.14.0' + // logging + implementation 'net.logstash.logback:logstash-logback-encoder:8.0' + testImplementation platform('org.junit:junit-bom:5.9.1') testImplementation 'org.junit.jupiter:junit-jupiter' testImplementation 'org.springframework.boot:spring-boot-starter-test' diff --git a/app/external-api/src/main/java/com/bombombom/devs/external/global/logging/ApiLoggingFilter.java b/app/external-api/src/main/java/com/bombombom/devs/external/global/logging/ApiLoggingFilter.java new file mode 100644 index 00000000..65691d42 --- /dev/null +++ b/app/external-api/src/main/java/com/bombombom/devs/external/global/logging/ApiLoggingFilter.java @@ -0,0 +1,70 @@ +package com.bombombom.devs.external.global.logging; + +import com.bombombom.devs.external.global.logging.dto.ApiLogInfo; +import com.bombombom.devs.security.AppUserDetails; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Optional; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; + +@Slf4j +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +@RequiredArgsConstructor +class ApiLoggingFilter extends OncePerRequestFilter { + + private final ObjectMapper objectMapper; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws IOException, ServletException { + String requestId = Optional.ofNullable(request.getHeader("request_id")) + .orElse(UUID.randomUUID().toString()); + MDC.put("request_id", requestId); + ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request); + ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); + Long userId = getRequestUserId(); + long start = System.currentTimeMillis(); + try { + filterChain.doFilter(requestWrapper, responseWrapper); + long elapsedTime = System.currentTimeMillis() - start; + ApiLogInfo apiLogInfo = ApiLogInfo.fromResult(requestWrapper, responseWrapper, userId, + elapsedTime); + log.info(objectMapper.writeValueAsString(apiLogInfo)); + } catch (Throwable e) { + ApiLogInfo apiLogInfo = ApiLogInfo.fromResult(requestWrapper, userId); + log.info(objectMapper.writeValueAsString(apiLogInfo)); + throw e; + } finally { + responseWrapper.copyBodyToResponse(); + } + MDC.clear(); + } + + private Long getRequestUserId() { + Long userId = null; + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null && authentication.isAuthenticated()) { + Object principal = authentication.getPrincipal(); + if (principal instanceof AppUserDetails) { + userId = ((AppUserDetails) principal).getId(); + } + } + return userId; + } +} diff --git a/app/external-api/src/main/java/com/bombombom/devs/external/global/logging/dto/ApiLogInfo.java b/app/external-api/src/main/java/com/bombombom/devs/external/global/logging/dto/ApiLogInfo.java new file mode 100644 index 00000000..32549330 --- /dev/null +++ b/app/external-api/src/main/java/com/bombombom/devs/external/global/logging/dto/ApiLogInfo.java @@ -0,0 +1,92 @@ +package com.bombombom.devs.external.global.logging.dto; + +import com.bombombom.devs.core.exception.ErrorCode; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import lombok.Builder; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; + +@Builder +public record ApiLogInfo( + @JsonProperty(value = "http_method") String httpMethod, + String uri, + @JsonProperty(value = "user_id") Long userId, + @JsonProperty(value = "request_header") Map requestHeader, + @JsonProperty(value = "response_header") Map responseHeader, + @JsonProperty(value = "request_body") String requestBody, + @JsonProperty(value = "response_body") String responseBody, + @JsonProperty(value = "client_ip") String clientIp, + @JsonProperty(value = "elapsed_time") long elapsedTime +) { + + public static ApiLogInfo fromResult(ContentCachingRequestWrapper request, + ContentCachingResponseWrapper response, Long userId, long elapsedTime) throws IOException { + String requestBody = new String(request.getContentAsByteArray(), StandardCharsets.UTF_8); + String responseBody = new String(response.getContentAsByteArray(), + StandardCharsets.UTF_8); + return ApiLogInfo.builder() + .httpMethod(request.getMethod()) + .uri(request.getRequestURI()) + .userId(userId) + .requestHeader(getRequestHeader(request)) + .responseHeader(getResponseHeader(response)) + .requestBody(requestBody) + .responseBody(responseBody) + .clientIp(getClientIp(request)) + .elapsedTime(elapsedTime) + .build(); + } + + public static ApiLogInfo fromResult(ContentCachingRequestWrapper request, Long userId) + throws IOException { + String requestBody = new String(request.getContentAsByteArray(), StandardCharsets.UTF_8); + return ApiLogInfo.builder() + .httpMethod(request.getMethod()) + .uri(request.getRequestURI()) + .userId(userId) + .requestHeader(getRequestHeader(request)) + .requestBody(requestBody) + .responseBody(String.valueOf(ErrorCode.UNEXPECTED_EXCEPTION)) + .clientIp(getClientIp(request)) + .build(); + } + + private static Map getRequestHeader(HttpServletRequest request) { + Map requestHeaders = new HashMap<>(); + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + requestHeaders.put(headerName, request.getHeader(headerName)); + } + return requestHeaders; + } + + private static Map getResponseHeader(HttpServletResponse response) { + Map responseHeaders = new HashMap<>(); + Collection headerNames = response.getHeaderNames(); + for (String headerName : headerNames) { + responseHeaders.put(headerName, response.getHeader(headerName)); + } + return responseHeaders; + } + + private static String getClientIp(HttpServletRequest request) { + String clientIp; + String xForwardedForHeader = request.getHeader("X-Forwarded-For"); + if (xForwardedForHeader != null && !xForwardedForHeader.isEmpty()) { + clientIp = xForwardedForHeader.split(",")[0]; + } else { + clientIp = request.getRemoteAddr(); + } + return clientIp; + } + +} diff --git a/app/external-api/src/main/resources/logback.xml b/app/external-api/src/main/resources/logback.xml new file mode 100644 index 00000000..0d2730aa --- /dev/null +++ b/app/external-api/src/main/resources/logback.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + ${LOG_PATTERN} + + + + + + + + + ${LOG_PATH}/data/${FILE_NAME}.log + + UTF-8 + + + + {"service_id":"devsService"} + + + + + + + + + + + + ${LOG_PATH}/prev/%d{yyyy-MM-dd}/${FILE_NAME}_%i.log + 90 + + 10MB + + + + + + + + \ No newline at end of file