Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: #BBB-153 로깅 시스템 구축 #74

Merged
merged 2 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ build/
!**/src/main/**/build/
!**/src/test/**/build/
*.env
/logs

### IntelliJ IDEA ###
.idea
Expand Down
3 changes: 3 additions & 0 deletions app/external-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> requestHeader,
@JsonProperty(value = "response_header") Map<String, String> 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<String, String> getRequestHeader(HttpServletRequest request) {
Map<String, String> requestHeaders = new HashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
requestHeaders.put(headerName, request.getHeader(headerName));
}
return requestHeaders;
}

private static Map<String, String> getResponseHeader(HttpServletResponse response) {
Map<String, String> responseHeaders = new HashMap<>();
Collection<String> 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;
}

}
51 changes: 51 additions & 0 deletions app/external-api/src/main/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<timestamp key="BY_DATE" datePattern="yyyy-MM-dd"/>
<property name="LOG_PATH" value="./logs"/>
<property name="FILE_NAME" value="api-server-logs"/>
<property name="LOG_PATTERN"
value="[%d{yyyy-MM-dd HH:mm:ss}:%-4relative] [%X{request_id:-startup}] %green([%thread]) %highlight(%-5level) %boldWhite([%C.%M:%yellow(%L)]) - %msg%n"/>

<springProfile name="!prod">
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>

<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/data/${FILE_NAME}.log</file>
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<charset>UTF-8</charset>
<providers>
<timestamp/>
<pattern>
<pattern>{"service_id":"devsService"}</pattern>
</pattern>
<mdc/>
<threadName/>
<logLevel/>
<message/>
<loggerName/>
<stackTrace/>
<callerData/>
</providers>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/prev/%d{yyyy-MM-dd}/${FILE_NAME}_%i.log</fileNamePattern>
<maxHistory>90</maxHistory>
<timeBasedFileNamingAndTriggeringPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
</appender>

<root level="INFO">
<appender-ref ref="FILE"/>
</root>
</configuration>