Log
는 Filter
에 남기기와 Interceptor
두가지 방법이 있다고 한다. 어느 한 블로그의 말에 의하면 많은 사람들이 Filter
를 사용한다고 한다. 난 찾아보지는 않았으나 그저 AOP
를 이용한 Interceptor
가 Service
와 더 가까우니 좋다고 생각해서 사용했다.
하지만 이런 저런 글을 찾아보니 'Filter
를 사용하면 컨트롤러에 도달하기 전에 요청을 조작할 수 있고 Spring MVC
외부에서 조작할 수 있다.' 라고 하는데, 내가 이해하기로는 Spring MVC
내부에서 세세하게 로그를 찍지는 못하지만 바로 request
를 받고, 내보내기 직전 response
를 찍는다는 점에서 장점인거 같다.
public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException
여기 request
와 response
를 그냥 . 찍고 이것저것 쓰고싶은데 그러면 안된다고 한다. getInputStream()
이라는 것이 작동하는데 이러면 내부 내용이 사라진다. 난 사용하는게 급하니 궁금하다면 자세한 로직은 직접 찾아보길 바란다. 그래서 Spring
에서 제공하는 ContentCachingRequestWrapper
로 캐싱해야한다.
@Slf4j
@Component
public class Logging implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper((HttpServletRequest) request);
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper((HttpServletResponse) response);
filterChain.doFilter(wrappedRequest, wrappedResponse);
log.info("\n" +
"[Request] {} - {}\n" +
"Headers : {}\n" +
"Request : {}\n" +
"Response : {}\n" +
((HttpServletRequest) request).getMethod(),
((HttpServletRequest) request).getRequestURI(),
getHeaders((HttpServletRequest) request),
getRequest(wrappedRequest),
getResponse(wrappedResponse)
);
}
private Map getHeaders(HttpServletRequest request) {
Map headerMap = new HashMap();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = headerNames.nextElement();
String value = request.getHeader(key);
headerMap.put(key, value);
}
return headerMap;
}
private Map getRequest(ContentCachingRequestWrapper wrappedRequest) {
Map requestMap = new HashMap();
int i = 0;
Enumeration<String> headerNames = wrappedRequest.getHeaderNames();
while (wrappedRequest.getContentLength() < i) {
String key = headerNames.nextElement();
String value = wrappedRequest.getHeader(key);
requestMap.put(key, value);
i++;
}
return requestMap;
}
private Map getResponse(ContentCachingResponseWrapper wrappedResponse) {
Map responseMap = new HashMap();
int i = 0;
Collection<String> headerNames = wrappedResponse.getHeaderNames();
while (wrappedResponse.getContentSize() < i) {
String key = headerNames.iterator().next();
String value = wrappedResponse.getHeader(key);
responseMap.put(key, value);
i++;
}
return responseMap;
}
}
예.. Request
와 Response
도 Map
으로 반환할 수 있을 줄 알았던 나의 실..수.... 사실 블로그들을 보니 String
으로 반환했는데 난 Headers
처럼 Map
으로 가능 할 줄 알았다.. 하지만 local
에서 돌려보니 그냥 아무것도 안나온다. 그래서 수정했다.
GPT를 이용해 String 으로 바꾸고 코드 뜯어서 이해해보고, Client의 IP와 브라우저도 받을 수 있도록 추가했다.
@Slf4j
@Component
public class Logging implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper((HttpServletRequest) request);
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper((HttpServletResponse) response);
filterChain.doFilter(wrappedRequest, wrappedResponse);
log.info("\n" +
"[Request] {} - {}\n" +
"Client : {}\n" + // Client 추가
"Headers : {}\n" +
"Request : {}\n" +
"Response : {}\n",
((HttpServletRequest) request).getMethod(),
((HttpServletRequest) request).getRequestURI(),
getClient((HttpServletRequest) request),
getHeaders((HttpServletRequest) request),
getRequest(wrappedRequest),
getResponse(wrappedResponse)
);
wrappedResponse.copyBodyToResponse();
}
private Map<String, String> getHeaders(HttpServletRequest request) {
Map<String, String> headerMap = new HashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = headerNames.nextElement();
String value = request.getHeader(key);
headerMap.put(key, value);
}
return headerMap;
}
// Client 추가
private String getClient(HttpServletRequest request) {
String ip = request.getRemoteAddr();
String userAgent = request.getHeader("User-Agent");
return "{IP: " + ip + ", Browser: " + (userAgent != null ? userAgent : "Unknown") + "}";
}
// String으로 바꿨다
private String getRequest(ContentCachingRequestWrapper wrappedRequest) {
// Content 내용은 Byte 형식으로 꺼낸다..!
byte[] content = wrappedRequest.getContentAsByteArray();
// 비어있으면 "Empty Body", 내용이 있으면 new String으로 UTF_8 방식으로 바이트 배열을 문자열로 변환한다..!!
return (content.length > 0) ? new String(content, StandardCharsets.UTF_8) : "Empty Body";
}
// String으로 바꿨다
private String getResponse(ContentCachingResponseWrapper wrappedResponse) {
byte[] content = wrappedResponse.getContentAsByteArray();
return (content.length > 0) ? new String(content, StandardCharsets.UTF_8) : "Empty Body";
}
}
이번에는 마스킹 처리를 추가했다. password
나 contact
같은 민감한 정보는 로그에 남기지 않도록 하는것이다. 둘 다 ****
이 뜨도록 했다.
@Slf4j
@Component
public class Logging implements Filter {
// 편한 마스킹을 위한 장치..랄까?
private static final String MASK = "****";
private static final String[] SENSITIVE_KEYS = {"password", "contact"};
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper((HttpServletRequest) request);
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper((HttpServletResponse) response);
filterChain.doFilter(wrappedRequest, wrappedResponse);
log.info("\n" +
"[Request] {} - {}\n" +
"Client : {}\n" +
"Headers : {}\n" +
"Request : {}\n" +
"Response : {}\n",
((HttpServletRequest) request).getMethod(),
((HttpServletRequest) request).getRequestURI(),
getClient((HttpServletRequest) request),
getHeaders((HttpServletRequest) request),
getRequest(wrappedRequest),
getResponse(wrappedResponse)
);
wrappedResponse.copyBodyToResponse();
}
private Map<String, String> getHeaders(HttpServletRequest request) {
Map<String, String> headerMap = new HashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = headerNames.nextElement();
String value = request.getHeader(key);
headerMap.put(key, value);
}
return headerMap;
}
private String getClient(HttpServletRequest request) {
String ip = request.getRemoteAddr();
String userAgent = request.getHeader("User-Agent");
return "{IP: " + ip + ", Browser: " + (userAgent != null ? userAgent : "Unknown") + "}";
}
// 조금씩 바뀌었다!
private String getRequest(ContentCachingRequestWrapper wrappedRequest) {
byte[] content = wrappedRequest.getContentAsByteArray();
String requestBody = (content.length > 0) ? new String(content, StandardCharsets.UTF_8) : "Empty Body";
return maskSensitiveData(requestBody);
}
// 얘도 마찬가지로 조금씩 바뀌었다!
private String getResponse(ContentCachingResponseWrapper wrappedResponse) {
byte[] content = wrappedResponse.getContentAsByteArray();
String responseBody = (content.length > 0) ? new String(content, StandardCharsets.UTF_8) : "Empty Body";
return maskSensitiveData(responseBody);
}
// 마스킹 처리
private String maskSensitiveData(String data) {
if (data == null || data.isEmpty()) {
return data;
}
for (String key : SENSITIVE_KEYS) {
data = data.replaceAll("(?i)" + key + "\\s*:\\s*\"[^\"]+\"", key + ": \"" + MASK + "\""); // JSON 형식
data = data.replaceAll("(?i)" + key + "\\s*=\\s*[^&\\s]+", key + "=" + MASK); // Query 파라미터 형식
}
return data;
}
}
이번에는 .log
파일로 저장되게 만들었다.
<configuration>
<!-- 콘솔 로깅 설정 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 파일 로깅 설정 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/application-%d{yyyy-MM-dd}.log.zip</fileNamePattern>
<maxHistory>365</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 기본 로거 설정 (콘솔 & 파일 모두 출력) -->
<root level="info">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>