개발관련
SpringBoot - http request와 response 로깅하기
부발자
2020. 8. 5. 23:27
웹 어플리케이션을 운영하다 보면 http의 request와 response 로그가 필요한 경우가 있다.
기본적으로 SpringBoot 에서는 http의 request, response 를 로깅하지 않으므로 개발자는 추가로 설정을 해줘야 한다.
먼저, Spring MVC Request Lifecycle 을 이해해야 함으로 아래 그림을 참고 하자
본 글에서는 Filter와 HandlerInterceptor를 사용하여 로깅을 할 예정이다.
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
반응형