Skip to content

Commit 4e7ff2d

Browse files
committed
Merge branch 'feat/log' into develop
2 parents 59b240a + d571572 commit 4e7ff2d

File tree

17 files changed

+1378
-10
lines changed

17 files changed

+1378
-10
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,4 @@ memory-infra/docker/**/data/*
4242
.env
4343
CLAUDE.md
4444
settings.local.json
45+
./logs/*

memory-api/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ dependencies {
1818
// Monitoring
1919
implementation 'org.springframework.boot:spring-boot-starter-actuator'
2020
implementation 'io.micrometer:micrometer-registry-prometheus'
21+
22+
// Logstash 연동을 위한 JSON 로깅
23+
implementation 'net.logstash.logback:logstash-logback-encoder:7.4'
2124

2225
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
2326
testImplementation 'org.testcontainers:junit-jupiter'

memory-api/src/main/java/com/memory/config/WebConfig.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.memory.config.interceptor.AdminInterceptor;
44
import com.memory.config.interceptor.AuthInterceptor;
55
import com.memory.config.resolver.MemberIdResolver;
6+
import com.memory.interceptor.ApiLoggingInterceptor;
67
import lombok.RequiredArgsConstructor;
78
import org.springframework.context.annotation.Configuration;
89
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
@@ -19,6 +20,7 @@ public class WebConfig implements WebMvcConfigurer {
1920
private final MemberIdResolver memberIdResolver;
2021
private final AuthInterceptor authInterceptor;
2122
private final AdminInterceptor adminInterceptor;
23+
private final ApiLoggingInterceptor apiLoggingInterceptor;
2224

2325
@Override
2426
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
@@ -27,10 +29,14 @@ public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers)
2729

2830
@Override
2931
public void addInterceptors(InterceptorRegistry registry) {
32+
registry.addInterceptor(apiLoggingInterceptor)
33+
.addPathPatterns("/api/**")
34+
.excludePathPatterns("/swagger-ui/**", "/v3/api-docs/**");
35+
3036
registry.addInterceptor(authInterceptor)
3137
.addPathPatterns("/api/**")
3238
.excludePathPatterns("/swagger-ui/**", "/v3/api-docs/**");
33-
39+
3440
registry.addInterceptor(adminInterceptor)
3541
.addPathPatterns("/api/**")
3642
.excludePathPatterns("/swagger-ui/**", "/v3/api-docs/**");
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package com.memory.interceptor;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import jakarta.servlet.http.HttpServletRequest;
5+
import jakarta.servlet.http.HttpServletResponse;
6+
import lombok.RequiredArgsConstructor;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.slf4j.MDC;
9+
import org.springframework.stereotype.Component;
10+
import org.springframework.web.method.HandlerMethod;
11+
import org.springframework.web.servlet.HandlerInterceptor;
12+
import org.springframework.web.servlet.ModelAndView;
13+
14+
import java.time.LocalDateTime;
15+
import java.time.format.DateTimeFormatter;
16+
import java.util.Enumeration;
17+
import java.util.HashMap;
18+
import java.util.Map;
19+
import java.util.UUID;
20+
21+
@Slf4j
22+
@Component
23+
@RequiredArgsConstructor
24+
public class ApiLoggingInterceptor implements HandlerInterceptor {
25+
26+
private final ObjectMapper objectMapper;
27+
private static final String REQUEST_TIME_ATTRIBUTE = "requestTime";
28+
private static final String TRACE_ID_ATTRIBUTE = "traceId";
29+
30+
@Override
31+
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
32+
long startTime = System.currentTimeMillis();
33+
String traceId = UUID.randomUUID().toString().substring(0, 8);
34+
35+
request.setAttribute(REQUEST_TIME_ATTRIBUTE, startTime);
36+
request.setAttribute(TRACE_ID_ATTRIBUTE, traceId);
37+
38+
MDC.put("traceId", traceId);
39+
MDC.put("method", request.getMethod());
40+
MDC.put("uri", request.getRequestURI());
41+
MDC.put("userAgent", request.getHeader("User-Agent"));
42+
MDC.put("remoteAddr", getClientIpAddress(request));
43+
44+
if (handler instanceof HandlerMethod handlerMethod) {
45+
String controllerName = handlerMethod.getBeanType().getSimpleName();
46+
String methodName = handlerMethod.getMethod().getName();
47+
MDC.put("controller", controllerName);
48+
MDC.put("action", methodName);
49+
}
50+
51+
Map<String, Object> logData = new HashMap<>();
52+
logData.put("type", "API_REQUEST");
53+
logData.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
54+
logData.put("traceId", traceId);
55+
logData.put("method", request.getMethod());
56+
logData.put("uri", request.getRequestURI());
57+
logData.put("queryString", request.getQueryString());
58+
logData.put("userAgent", request.getHeader("User-Agent"));
59+
logData.put("remoteAddr", getClientIpAddress(request));
60+
logData.put("headers", getHeaders(request));
61+
62+
if (handler instanceof HandlerMethod handlerMethod) {
63+
logData.put("controller", handlerMethod.getBeanType().getSimpleName());
64+
logData.put("method", handlerMethod.getMethod().getName());
65+
}
66+
67+
log.info("API Request: {}", objectMapper.writeValueAsString(logData));
68+
69+
return true;
70+
}
71+
72+
@Override
73+
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
74+
ModelAndView modelAndView) throws Exception {
75+
// 특별한 처리가 필요한 경우 여기에 구현
76+
}
77+
78+
@Override
79+
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
80+
Exception ex) throws Exception {
81+
try {
82+
long startTime = (Long) request.getAttribute(REQUEST_TIME_ATTRIBUTE);
83+
String traceId = (String) request.getAttribute(TRACE_ID_ATTRIBUTE);
84+
long endTime = System.currentTimeMillis();
85+
long processingTime = endTime - startTime;
86+
87+
Map<String, Object> logData = new HashMap<>();
88+
logData.put("type", "API_RESPONSE");
89+
logData.put("timestamp", LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
90+
logData.put("traceId", traceId);
91+
logData.put("method", request.getMethod());
92+
logData.put("uri", request.getRequestURI());
93+
logData.put("statusCode", response.getStatus());
94+
logData.put("processingTime", processingTime);
95+
logData.put("success", response.getStatus() < 400);
96+
97+
if (ex != null) {
98+
logData.put("exception", ex.getClass().getSimpleName());
99+
logData.put("errorMessage", ex.getMessage());
100+
log.error("API Response with Exception: {}", objectMapper.writeValueAsString(logData), ex);
101+
} else {
102+
if (response.getStatus() >= 400) {
103+
log.warn("API Response: {}", objectMapper.writeValueAsString(logData));
104+
} else {
105+
log.info("API Response: {}", objectMapper.writeValueAsString(logData));
106+
}
107+
}
108+
} finally {
109+
MDC.clear();
110+
}
111+
}
112+
113+
private String getClientIpAddress(HttpServletRequest request) {
114+
String xForwardedFor = request.getHeader("X-Forwarded-For");
115+
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
116+
return xForwardedFor.split(",")[0].trim();
117+
}
118+
119+
String xRealIp = request.getHeader("X-Real-IP");
120+
if (xRealIp != null && !xRealIp.isEmpty()) {
121+
return xRealIp;
122+
}
123+
124+
return request.getRemoteAddr();
125+
}
126+
127+
private Map<String, String> getHeaders(HttpServletRequest request) {
128+
Map<String, String> headers = new HashMap<>();
129+
Enumeration<String> headerNames = request.getHeaderNames();
130+
131+
while (headerNames.hasMoreElements()) {
132+
String headerName = headerNames.nextElement();
133+
// 민감한 정보는 로그에서 제외
134+
if (!headerName.toLowerCase().contains("authorization") &&
135+
!headerName.toLowerCase().contains("cookie")) {
136+
headers.put(headerName, request.getHeader(headerName));
137+
}
138+
}
139+
140+
return headers;
141+
}
142+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<configuration>
3+
<!-- 공통 변수 정의 -->
4+
<springProfile name="local">
5+
<property name="LOG_PATH" value="./logs"/>
6+
</springProfile>
7+
<springProfile name="prod, dev">
8+
<property name="LOG_PATH" value="/app/logs"/>
9+
</springProfile>
10+
11+
<!-- Console Appender (개발환경) -->
12+
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
13+
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
14+
<providers>
15+
<timestamp/>
16+
<logLevel/>
17+
<loggerName/>
18+
<mdc/>
19+
<arguments/>
20+
<message/>
21+
<stackTrace/>
22+
</providers>
23+
<jsonGeneratorDecorator class="net.logstash.logback.decorate.PrettyPrintingJsonGeneratorDecorator"/>
24+
</encoder>
25+
</appender>
26+
27+
<!-- File Appender for All Logs -->
28+
<appender name="FILE_ALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
29+
<file>${LOG_PATH}/application.log</file>
30+
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
31+
<fileNamePattern>${LOG_PATH}/application-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
32+
<maxFileSize>100MB</maxFileSize>
33+
<maxHistory>30</maxHistory>
34+
<totalSizeCap>3GB</totalSizeCap>
35+
</rollingPolicy>
36+
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
37+
<providers>
38+
<timestamp>
39+
<pattern>yyyy-MM-dd HH:mm:ss.SSS</pattern>
40+
</timestamp>
41+
<logLevel/>
42+
<loggerName/>
43+
<mdc/>
44+
<arguments/>
45+
<message/>
46+
<stackTrace/>
47+
</providers>
48+
</encoder>
49+
</appender>
50+
51+
<!-- File Appender for API Logs -->
52+
<appender name="FILE_API" class="ch.qos.logback.core.rolling.RollingFileAppender">
53+
<file>${LOG_PATH}/api.log</file>
54+
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
55+
<fileNamePattern>${LOG_PATH}/api-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
56+
<maxFileSize>100MB</maxFileSize>
57+
<maxHistory>30</maxHistory>
58+
<totalSizeCap>2GB</totalSizeCap>
59+
</rollingPolicy>
60+
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
61+
<providers>
62+
<timestamp>
63+
<pattern>yyyy-MM-dd HH:mm:ss.SSS</pattern>
64+
</timestamp>
65+
<logLevel/>
66+
<loggerName/>
67+
<mdc/>
68+
<arguments/>
69+
<message/>
70+
<stackTrace/>
71+
</providers>
72+
</encoder>
73+
</appender>
74+
75+
<!-- File Appender for Error Logs -->
76+
<appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
77+
<file>${LOG_PATH}/error.log</file>
78+
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
79+
<fileNamePattern>${LOG_PATH}/error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
80+
<maxFileSize>100MB</maxFileSize>
81+
<maxHistory>90</maxHistory>
82+
<totalSizeCap>5GB</totalSizeCap>
83+
</rollingPolicy>
84+
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
85+
<providers>
86+
<timestamp>
87+
<pattern>yyyy-MM-dd HH:mm:ss.SSS</pattern>
88+
</timestamp>
89+
<logLevel/>
90+
<loggerName/>
91+
<mdc/>
92+
<arguments/>
93+
<message/>
94+
<stackTrace/>
95+
</providers>
96+
</encoder>
97+
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
98+
<level>ERROR</level>
99+
</filter>
100+
</appender>
101+
102+
<!-- File Appender for Business Logs -->
103+
<appender name="FILE_BUSINESS" class="ch.qos.logback.core.rolling.RollingFileAppender">
104+
<file>${LOG_PATH}/business.log</file>
105+
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
106+
<fileNamePattern>${LOG_PATH}/business-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
107+
<maxFileSize>100MB</maxFileSize>
108+
<maxHistory>30</maxHistory>
109+
<totalSizeCap>2GB</totalSizeCap>
110+
</rollingPolicy>
111+
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
112+
<providers>
113+
<timestamp>
114+
<pattern>yyyy-MM-dd HH:mm:ss.SSS</pattern>
115+
</timestamp>
116+
<logLevel/>
117+
<loggerName/>
118+
<mdc/>
119+
<arguments/>
120+
<message/>
121+
<stackTrace/>
122+
</providers>
123+
</encoder>
124+
</appender>
125+
126+
<!-- Logger 설정 -->
127+
<logger name="com.memory" level="INFO"/>
128+
129+
<!-- API 로깅 인터셉터 전용 로거 -->
130+
<logger name="com.memory.interceptor.ApiLoggingInterceptor" level="DEBUG" additivity="false">
131+
<appender-ref ref="FILE_API"/>
132+
<appender-ref ref="CONSOLE"/>
133+
</logger>
134+
135+
<!-- 서비스 레이어 로거 -->
136+
<logger name="com.memory.service" level="INFO" additivity="false">
137+
<appender-ref ref="FILE_BUSINESS"/>
138+
<appender-ref ref="CONSOLE"/>
139+
</logger>
140+
141+
<!-- 비즈니스 이벤트 전용 로거 -->
142+
<logger name="BUSINESS" level="INFO" additivity="false">
143+
<appender-ref ref="FILE_BUSINESS"/>
144+
<appender-ref ref="CONSOLE"/>
145+
</logger>
146+
147+
<!-- SQL 로그 (개발환경만) -->
148+
<springProfile name="local,dev">
149+
<logger name="org.springframework.jdbc.core" level="DEBUG"/>
150+
<logger name="org.hibernate.SQL" level="DEBUG"/>
151+
<logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE"/>
152+
</springProfile>
153+
154+
<!-- Root Logger -->
155+
<springProfile name="local,dev">
156+
<root level="INFO">
157+
<appender-ref ref="CONSOLE"/>
158+
<appender-ref ref="FILE_ALL"/>
159+
<appender-ref ref="FILE_ERROR"/>
160+
</root>
161+
</springProfile>
162+
163+
<springProfile name="prod">
164+
<root level="WARN">
165+
<appender-ref ref="FILE_ALL"/>
166+
<appender-ref ref="FILE_ERROR"/>
167+
</root>
168+
</springProfile>
169+
</configuration>

0 commit comments

Comments
 (0)