로그 피드백 반영

박재성·2025년 3월 7일
0

파이널 프로젝트

목록 보기
2/3

피드백

  • 오딧 로그 적용
  • 로그에 IP, 브라우저 추가

🌊 think flow

LogFilter에 남기기와 Interceptor 두가지 방법이 있다고 한다. 어느 한 블로그의 말에 의하면 많은 사람들이 Filter를 사용한다고 한다. 난 찾아보지는 않았으나 그저 AOP를 이용한 InterceptorService와 더 가까우니 좋다고 생각해서 사용했다.
하지만 이런 저런 글을 찾아보니 'Filter 를 사용하면 컨트롤러에 도달하기 전에 요청을 조작할 수 있고 Spring MVC 외부에서 조작할 수 있다.' 라고 하는데, 내가 이해하기로는 Spring MVC 내부에서 세세하게 로그를 찍지는 못하지만 바로 request를 받고, 내보내기 직전 response를 찍는다는 점에서 장점인거 같다.

Filter를 사용해보겠습니다~

public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException

여기 requestresponse를 그냥 . 찍고 이것저것 쓰고싶은데 그러면 안된다고 한다. getInputStream()이라는 것이 작동하는데 이러면 내부 내용이 사라진다. 난 사용하는게 급하니 궁금하다면 자세한 로직은 직접 찾아보길 바란다. 그래서 Spring에서 제공하는 ContentCachingRequestWrapper로 캐싱해야한다.

Logging V1

@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;
   }
}

예.. RequestResponseMap으로 반환할 수 있을 줄 알았던 나의 실..수.... 사실 블로그들을 보니 String으로 반환했는데 난 Headers처럼 Map으로 가능 할 줄 알았다.. 하지만 local에서 돌려보니 그냥 아무것도 안나온다. 그래서 수정했다.
GPT를 이용해 String 으로 바꾸고 코드 뜯어서 이해해보고, Client의 IP와 브라우저도 받을 수 있도록 추가했다.

Logging V2

*수정된 내용은 코드 내 주석을 참고

@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";
   }
}

Logging V3

이번에는 마스킹 처리를 추가했다. passwordcontact 같은 민감한 정보는 로그에 남기지 않도록 하는것이다. 둘 다 ****이 뜨도록 했다.

@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;
   }
}

Logging V4

이번에는 .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>

0개의 댓글