로그 요청 및 응답을 위한 Spring MVC Logger 작성

안녕하세요! 이 글에서는 로깅 요청 및 응답을 위한 로거 작성 방법에 대해 설명합니다. 이것에 대해 복잡한 것은 없어 보이지만 설명서를 보면 다음을 볼 수 있습니다.



getReader 메서드 또는 getInputStream 메서드를 호출하여 데이터를 한 번만 읽을 수 있음을 알려줍니다. 이 제한을 해결하기 위해 두 가지 특수 클래스ContentCachingRequetWrapper.java와 ContentCachingResponseWrapper.java가 발명되었습니다. 이 클래스는 요청 데이터를 기록하는 데 필요합니다. 요청 및 응답에 대한 데이터를 다듬기 위해 OncePerRequestFilter.java 클래스를 확장하는 필터를 구현할 것입니다. 이를 영화 LoggingFilter라고 부르고 작성을 시작하겠습니다. 먼저 로거의 인스턴스를 선언하겠습니다.

private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class);


다음으로 기록하려는 모든 MIME 유형을 선언합니다.

private static final List<MediaType> VISIBLE_TYPES = Arrays.asList(
      MediaType.valueOf("text/*"),
      MediaType.APPLICATION_FORM_URLENCODED,
      MediaType.APPLICATION_JSON,
      MediaType.APPLICATION_XML,
      MediaType.valueOf("application/*+json"),
      MediaType.valueOf("application/*+xml"),
      MediaType.MULTIPART_FORM_DATA
);


그런 다음 부모 클래스의 메서드를 재정의합니다.

@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest,
                                HttpServletResponse httpServletResponse,
                                FilterChain filterChain) 
                                throws ServletException, IOException {
  if (isAsyncDispatch(httpServletRequest)) {
      filterChain.doFilter(httpServletRequest, httpServletResponse);
  } else {
     doFilterWrapped(wrapRequest(httpServletRequest),
                     wrapResponse(httpServletResponse,
                     filterChain);
 }



이 경우 호출이 비동기인지 확인하고, 이 경우에는 이를 연마하려고 하지 않고, 호출이 동기이면 로깅을 랩핑합니다. doFilterWrapped 메소드 구현

protected void doFilterWrapped(ContentCachingRequestWrapper contentCachingRequestWrapper, ContentCachingResponseWrapper contentCachingResponseWrapper, FilterChain filterChain) throws IOException, ServletException {
        try {
            beforeRequest(contentCachingRequestWrapper);
            filterChain.doFilter(contentCachingRequestWrapper, contentCachingResponseWrapper);
        } finally {
            afterRequest(contentCachingRequestWrapper, contentCachingResponseWrapper);
            contentCachingResponseWrapper.copyBodyToResponse();
        }
    }


이 방법에서는 이미 캐시된 요청 및 응답을 수신하고 요청에 대한 처리 및 후처리를 수행하고 응답 데이터도 복사합니다. 요청 처리 코드를 작성해 보겠습니다.

protected void beforeRequest(ContentCachingRequestWrapper request) {
    if (logger.isInfoEnabled()) {
        logRequestHeader(request, request.getRemoteAddr() + "|>");
    }
}


로깅 수준을 확인하고 INFO 수준과 같으면 요청 헤더도 기록합니다.

사후 처리 방법을 작성해 봅시다.

protected void afterRequest(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response) {
        if (logger.isInfoEnabled()) {
            logRequestBody(request, request.getRemoteAddr() + "|>");
            logResponse(response, request.getRemoteAddr() + "|>");
        }
    }


이 경우 요청 및 응답을 다듬기 전에 로깅 수준이 INFO로 설정되어 있는지도 확인합니다. 마지막으로 가장 흥미로운 점은 요청 및 응답뿐만 아니라 헤더의 로깅을 작성한다는 것입니다.

private static void logRequestHeader(ContentCachingRequestWrapper request, String prefix) {
        String queryString = request.getQueryString();
        if (queryString == null) {
            logger.info("{} {} {}", prefix, request.getMethod(), request.getRequestURI());
        } else {
            logger.info("{} {} {}?{}", prefix, request.getMethod(), request.getRequestURI(), queryString);
        }
        Collections.list(request.getHeaderNames()).forEach(headerName ->
                Collections.list(request.getHeaders(headerName)).forEach(headerValue -> logger.info("{} {} {}", prefix, headerName, headerValue)));
        logger.info("{}", prefix);
    }


logRequestHeader 메서드에서 다음과 같은 일이 발생합니다. 요청에서 문자열을 얻고 null인지 확인하고 null이면 HTTP 메서드와 해당 요청이 온 URL을 코딩합니다. 그렇지 않으면 HTTP 메서드도 기록됩니다. 요청이 도착한 URL 및 모든 요청 헤더 다음으로 요청 본문을 기록하는 코드를 작성해야 합니다.

private static void logRequestBody(ContentCachingRequestWrapper request, String prefix) {
    byte[] content = request.getContentAsByteArray();
    if (content.length > 0) {
        logContent(content, request.getContentType(), request.getCharacterEncoding(), prefix);
    }
}


우리는 요청으로부터 데이터를 바이트 배열로 받고 배열의 크기가 0보다 큰 경우 데이터를 조금 나중에 작성할 logContent 메서드에 전달합니다.

이제 애플리케이션의 응답을 로깅하기 위한 코드를 작성할 때입니다.

private static void logResponse(ContentCachingResponseWrapper response, String prefix) {
    int status = response.getStatus();
    logger.info("{} {} {}", prefix, status, HttpStatus.valueOf(status).getReasonPhrase());
    response.getHeaderNames().forEach(header -> response.getHeaders(header).forEach(headerValue -> logger.info("{} {} {}", prefix, header, headerValue)));
    logger.info("{}", prefix);
    byte[] content = response.getContentAsByteArray();
    if (content.length > 0) {
        logContent(content, response.getContentType(), response.getCharacterEncoding(), prefix);
    }
}


여기서 정확히 무슨 일이 일어나고 있는지 알아봅시다. 첫 번째 줄에서 애플리케이션의 HTTP 응답 코드를 얻은 다음 이에 대한 데이터를 기록합니다. 다음으로 응답 헤더를 살펴보고 동일한 방식으로 기록합니다. 또한 논리는 이전에 본 것과 매우 유사합니다. 응답에서 데이터를 바이트 배열로 받은 다음 logContent 메서드에 전달합니다. 이는 다음과 같습니다.

private static void logContent(byte[] content, String contentType, String contentEncoding, String prefix) {
        MediaType mediaType = MediaType.valueOf(contentType);
        boolean visible = VISIBLE_TYPES.stream().anyMatch(visibleType -> visibleType.includes(mediaType));
        if (visible) {
            try {
                String contentString = new String(content, contentEncoding);
                Stream.of(contentString.split("\r\n|\r\n")).forEach(line -> logger.info("{} {}", prefix, line));
            } catch (UnsupportedEncodingException e) {
                logger.info("{}, [{} bytes content]", prefix, content.length);
            }
        } else {
            logger.info("{}, [{} bytes content]", prefix, content.length);
        }
    }


무슨 일이야? 먼저 전송된 데이터의 유형이 도핑을 지원하는지 확인하고 지원하지 않는 경우 데이터 배열의 크기를 표시합니다. 그렇다면 요청에 지정된 인코딩을 사용하여 데이터에서 한 줄을 만들고 캐리지 리턴 및 줄 바꿈 문자를 사용하여 각 줄을 구분합니다. 인코딩이 지원되지 않으면 지원되지 않는 데이터 유형의 경우와 마찬가지로 요청 또는 응답에서 받은 데이터의 크기를 간단히 기록합니다.

그리고 코드의 마지막 터치는 ContentCachingRequestWrapper 및 ContentCachingResponseWrapper에서 HttpServletRequest 및 HttpServletResponse를 래핑하는 데 실제로 필요한 두 가지 메서드입니다.

private static ContentCachingRequestWrapper wrapRequest(HttpServletRequest httpServletRequest) {
        if (httpServletRequest instanceof ContentCachingRequestWrapper) {
            return (ContentCachingRequestWrapper) httpServletRequest;
        } else {
            return new ContentCachingRequestWrapper(httpServletRequest);
        }
    }

    private static ContentCachingResponseWrapper wrapResponse(HttpServletResponse httpServletResponse) {
        if (httpServletResponse instanceof ContentCachingResponseWrapper) {
            return (ContentCachingResponseWrapper) httpServletResponse;
        } else {
            return new ContentCachingResponseWrapper(httpServletResponse);
        }
    }


다음으로 배포 설명자에 필터를 등록해야 합니다.

<filter>
    <filter-name>Filter</filter-name>
    <filter-class>ru.skillfactory.filter.LoggingFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>Filter</filter-name>
    <url-pattern>*</url-pattern>
</filter-mapping>


마지막으로 필터가 제대로 작동하는지 확인합니다.

2021-01-18 12:25:24 DEBUG RequestResponseBodyMethodProcessor:91 - Read "application/json;charset=ISO-8859-1" to [StudentData(firstName=John, lastName=Doe, grade=5)]
2021-01-18 12:25:24 DEBUG RequestResponseBodyMethodProcessor:265 - Using 'text/plain', given [*/*] and supported [text/plain, */*, application/json, application/*+json]
2021-01-18 12:25:24 DEBUG RequestResponseBodyMethodProcessor:91 - Writing ["You are great with your grade 5"]
2021-01-18 12:25:24 DEBUG DispatcherServlet:1131 - Completed 200 OK
2021-01-18 12:25:24 INFO  LoggingFilter:104 - 0:0:0:0:0:0:0:1|> {
    "firstName": "John",
    "lastName": "Doe",
    "grade": 5
}
2021-01-18 12:25:24 INFO  LoggingFilter:89 - 0:0:0:0:0:0:0:1|> 200 OK
2021-01-18 12:25:24 INFO  LoggingFilter:91 - 0:0:0:0:0:0:0:1|>
2021-01-18 12:25:24 INFO  LoggingFilter:104 - 0:0:0:0:0:0:0:1|> You are great with your grade 5


LoggingFilter.java의 전체 코드

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;

public class LoggingFilter extends OncePerRequestFilter {

    private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class);
    private static final List<MediaType> VISIBLE_TYPES = Arrays.asList(
            MediaType.valueOf("text/*"),
            MediaType.APPLICATION_FORM_URLENCODED,
            MediaType.APPLICATION_JSON,
            MediaType.APPLICATION_XML,
            MediaType.valueOf("application/*+json"),
            MediaType.valueOf("application/*+xml"),
            MediaType.MULTIPART_FORM_DATA
    );

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        if (isAsyncDispatch(httpServletRequest)) {
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        } else {
            doFilterWrapped(wrapRequest(httpServletRequest), wrapResponse(httpServletResponse), filterChain);
        }
    }

    protected void doFilterWrapped(ContentCachingRequestWrapper contentCachingRequestWrapper, ContentCachingResponseWrapper contentCachingResponseWrapper, FilterChain filterChain) throws IOException, ServletException {
        try {
            beforeRequest(contentCachingRequestWrapper);
            filterChain.doFilter(contentCachingRequestWrapper, contentCachingResponseWrapper);
        } finally {
            afterRequest(contentCachingRequestWrapper, contentCachingResponseWrapper);
            contentCachingResponseWrapper.copyBodyToResponse();
        }
    }

    protected void beforeRequest(ContentCachingRequestWrapper request) {
        if (logger.isInfoEnabled()) {
            logRequestHeader(request, request.getRemoteAddr() + "|>");
        }
    }

    protected void afterRequest(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response) {
        if (logger.isInfoEnabled()) {
            logRequestBody(request, request.getRemoteAddr() + "|>");
            logResponse(response, request.getRemoteAddr() + "|>");
        }
    }

    private static void logRequestHeader(ContentCachingRequestWrapper request, String prefix) {
        String queryString = request.getQueryString();
        if (queryString == null) {
            logger.info("{} {} {}", prefix, request.getMethod(), request.getRequestURI());
        } else {
            logger.info("{} {} {}?{}", prefix, request.getMethod(), request.getRequestURI(), queryString);
        }
        Collections.list(request.getHeaderNames()).forEach(headerName ->
                Collections.list(request.getHeaders(headerName)).forEach(headerValue -> logger.info("{} {} {}", prefix, headerName, headerValue)));
        logger.info("{}", prefix);
    }

    private static void logRequestBody(ContentCachingRequestWrapper request, String prefix) {
        byte[] content = request.getContentAsByteArray();
        if (content.length > 0) {
            logContent(content, request.getContentType(), request.getCharacterEncoding(), prefix);
        }
    }

    private static void logResponse(ContentCachingResponseWrapper response, String prefix) {
        int status = response.getStatus();
        logger.info("{} {} {}", prefix, status, HttpStatus.valueOf(status).getReasonPhrase());
        response.getHeaderNames().forEach(header -> response.getHeaders(header).forEach(headerValue -> logger.info("{} {} {}", prefix, header, headerValue)));
        logger.info("{}", prefix);
        byte[] content = response.getContentAsByteArray();
        if (content.length > 0) {
            logContent(content, response.getContentType(), response.getCharacterEncoding(), prefix);
        }
    }

    private static void logContent(byte[] content, String contentType, String contentEncoding, String prefix) {
        MediaType mediaType = MediaType.valueOf(contentType);
        boolean visible = VISIBLE_TYPES.stream().anyMatch(visibleType -> visibleType.includes(mediaType));
        if (visible) {
            try {
                String contentString = new String(content, contentEncoding);
                Stream.of(contentString.split("\r\n|\r\n")).forEach(line -> logger.info("{} {}", prefix, line));
            } catch (UnsupportedEncodingException e) {
                logger.info("{}, [{} bytes content]", prefix, content.length);
            }
        } else {
            logger.info("{}, [{} bytes content]", prefix, content.length);
        }
    }

    private static ContentCachingRequestWrapper wrapRequest(HttpServletRequest httpServletRequest) {
        if (httpServletRequest instanceof ContentCachingRequestWrapper) {
            return (ContentCachingRequestWrapper) httpServletRequest;
        } else {
            return new ContentCachingRequestWrapper(httpServletRequest);
        }
    }

    private static ContentCachingResponseWrapper wrapResponse(HttpServletResponse httpServletResponse) {
        if (httpServletResponse instanceof ContentCachingResponseWrapper) {
            return (ContentCachingResponseWrapper) httpServletResponse;
        } else {
            return new ContentCachingResponseWrapper(httpServletResponse);
        }
    }
}


기사가 마음에 들면 구독하고 친구들과 공유하십시오!
관심을 가져 주셔서 감사합니다

좋은 웹페이지 즐겨찾기