개발관련

SpringBoot - http request와 response 로깅하기

부발자 2020. 8. 5. 23:27

웹 어플리케이션을 운영하다 보면 http의 request와 response 로그가 필요한 경우가 있다.

기본적으로 SpringBoot 에서는 http의 request, response 를 로깅하지 않으므로 개발자는 추가로 설정을 해줘야 한다.

 

먼저, Spring MVC Request Lifecycle 을 이해해야 함으로 아래 그림을 참고 하자

본 글에서는 Filter와 HandlerInterceptor를 사용하여 로깅을 할 예정이다.

 

 

 

Spring MVC Request Lifecycle

 

 

Http logging 방법은 2가지가 있다.

 

Custom Request, Response 로깅


1. RequestWrapper와 ResponseWrapper 클래스를 만든다.

Wrapper를 만드는 이유는 HttpServletRequest 의 InputStream 은 오직 한번만 읽을 수 있기 때문이다.

Wrapper 클래스를 만들어서 요청했던 데이터를 캐싱하여 여러번 읽을 수 있도록 하기 위함이다.

 

public class CachingRequestWrapper extends HttpServletRequestWrapper {

  private final Charset encoding;
  private byte[] rawData;

  public CachingRequestWrapper(HttpServletRequest request) throws IOException {
    super(request);

    String characterEncoding = request.getCharacterEncoding();
    if (StringUtils.isEmpty(characterEncoding)) {
      characterEncoding = StandardCharsets.UTF_8.name();
    }
    this.encoding = Charset.forName(characterEncoding);

    try (InputStream inputStream = request.getInputStream()) {
      this.rawData = IOUtils.toByteArray(inputStream);
    }
  }

  @Override
  public ServletInputStream getInputStream() {
    return new CachedServletInputStream(this.rawData);
  }

  @Override
  public BufferedReader getReader() {
    return new BufferedReader(new InputStreamReader(this.getInputStream(), this.encoding));
  }


  private static class CachedServletInputStream extends ServletInputStream {

    private final ByteArrayInputStream buffer;

    public CachedServletInputStream(byte[] contents) {
      this.buffer = new ByteArrayInputStream(contents);
    }

    @Override
    public int read() throws IOException {
      return buffer.read();
    }

    @Override
    public boolean isFinished() {
      return buffer.available() == 0;
    }

    @Override
    public boolean isReady() {
      return true;
    }

    @Override
    public void setReadListener(ReadListener listener) {
      throw new UnsupportedOperationException("not support");
    }
  }
}
public class CachingResponseWrapper extends HttpServletResponseWrapper {

  private final FastByteArrayOutputStream content = new FastByteArrayOutputStream(1024);
  private ServletOutputStream outputStream;
  private PrintWriter writer;

  public CachingResponseWrapper(HttpServletResponse response) {
    super(response);
  }

  @Override
  public ServletOutputStream getOutputStream() throws IOException {
    if (this.outputStream == null) {
      this.outputStream = new CachedServletOutputStream(getResponse().getOutputStream(), this.content);
    }
    return this.outputStream;
  }

  @Override
  public PrintWriter getWriter() throws IOException {
    if (writer == null) {
      writer = new PrintWriter(new OutputStreamWriter(content, this.getCharacterEncoding()), true);
    }
    return writer;
  }

  public InputStream getContentInputStream() {
    return this.content.getInputStream();
  }

  private class CachedServletOutputStream extends ServletOutputStream {

    private final TeeOutputStream targetStream;

    public CachedServletOutputStream(OutputStream one, OutputStream two) {
      targetStream = new TeeOutputStream(one, two);
    }

    @Override
    public void write(int arg) throws IOException {
      this.targetStream.write(arg);
    }

    @Override
    public void write(byte[] buf, int off, int len) throws IOException {
      this.targetStream.write(buf, off, len);
    }

    @Override
    public void flush() throws IOException {
      super.flush();
      this.targetStream.flush();
    }

    @Override
    public void close() throws IOException {
      super.close();
      this.targetStream.close();
    }

    @Override
    public boolean isReady() {
      return false;
    }

    @Override
    public void setWriteListener(WriteListener writeListener) {
      throw new UnsupportedOperationException("not support");
    }
  }
}

 

2. Filter 를 만들어서 Request 와 Response 를 Wrapper 클래스로 만든다.

Wrapper 클래스를 만들어서 필터 이후의 뒷단에서 여러번 InputStream 을 읽을 수 있도록 한다.

 

OncePerRequestFilter
단 한번만 처리가 수행되도록 보장되는 필터이다.
GenericFilterBean를 상속하고 있으며, 스프링 제공 서블릿 필터를 만들수 있다.

 

@Slf4j
@Component
public class RequestResponseWrapperFilter extends OncePerRequestFilter {

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
      throws ServletException, IOException {
    if (isAsyncDispatch(request)) {
      filterChain.doFilter(request, response);
    } else {
      filterChain.doFilter(
          new CachingRequestWrapper(request),
          new CachingResponseWrapper(response)
      );
    }
  }
}

 

3. HandlerInterceptor 를 만들어서 로깅한다.

 

Interceptor 는 3개의 메소드가 있으므로 원하는 곳에서 로깅 처리를 하면 된다.

 

Interceptor 에는 preHandle, postHandle, afterCompletion 의 3개의 메소드가 존재한다.

preHandle
 - 컨트롤러의 핸들러 메서드를 실행하기전에 호출
 - 핸들러 메서드가 호출되지 않게 하고 싶을 때 메서드 반환값으로 false

postHandle
 - 컨트롤러의 핸들러 메서드가 정상적으로 종료된 후에 호출
 - 핸들러 메서드에서 예외가 발생하면 호출 안됨

afterHandle
 - 컨트롤러의 핸들러 메서드의 처리가 종료된 후에 호출
 - 예외가 발생해도 호출
@Slf4j
@Component
public class HttpLogInterceptor extends HandlerInterceptorAdapter {

  private final ObjectMapper objectMapper;

  public HttpLogInterceptor(ObjectMapper objectMapper) {
    this.objectMapper = objectMapper;
  }

  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

    if (request instanceof CachingRequestWrapper) {
      String req = IOUtils.toString(request.getInputStream(), request.getCharacterEncoding());
      log.info("request - {}", req);
    }
    return true;
  }
  
  @Override
  public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
      throws Exception {

    if (response instanceof CachingResponseWrapper) {
      String res = IOUtils.toString(((CachingResponseWrapper) response).getContentInputStream(), response.getCharacterEncoding());
      log.info("response - {}", res);
    }
  }
}

 

4. Interceptor 의 PathPattern 을 설정한다.

여기서는 모든 요청과 응답에 로깅할수 있도록 설정하도록 하였다.

@RequiredArgsConstructor
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

  private final HttpLogInterceptor httpLogInterceptor;

  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(httpLogInterceptor)
        .addPathPatterns("/**");
  }
}

 

 

SpringBoot Built-In Request 로깅


SpringBoot에서는 Request log 를 남기는 것을 지원하고 있다.

설정이 간단해서 쉽게 적용할 수 있지만, 역시나 Spring Commons~ 클래스들은 뭔가 2% 부족한 느낌일 들것이다.

또한 Response 를 로깅하는 것도 지원하지 않고 있다.

 

@Configuration
public class RequestLoggingFilterConfig {
 
    @Bean
    public CommonsRequestLoggingFilter logFilter() {
        CommonsRequestLoggingFilter filter
          = new CommonsRequestLoggingFilter();
        filter.setIncludeQueryString(true);
        filter.setIncludePayload(true);
        filter.setMaxPayloadLength(10000);
        filter.setIncludeHeaders(false);
        filter.setAfterMessagePrefix("REQUEST DATA : ");
        return filter;
    }
}

로그 레벨도 같이 설정해 줘야 한다.

logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG
반응형