Как правильно протоколировать HTTP-запросы с помощью Spring MVC

Здравствуйте, я пытаюсь выяснить общий способ протоколирования HTTP-запросов в моем приложении, до сих пор не повезло, вот как я обрабатываю ведение журнала прямо сейчас i.e:

@RequestMapping(value="register", method = RequestMethod.POST)
    @ResponseBody
    public String register(@RequestParam(value="param1",required=false) String param1, @RequestParam("param2") String param2, @RequestParam("param3") String param3, HttpServletRequest request){
        long start = System.currentTimeMillis();
        logger.info("!--REQUEST START--!");

        logger.info("Request URL: " + request.getRequestURL().toString());

        List<String> requestParameterNames = Collections.list((Enumeration<String>)request.getParameterNames());
        logger.info("Parameter number: " + requestParameterNames.size()); 

 for (String parameterName : requestParameterNames){
           logger.info("Parameter name: " + parameterName + " - Parameter value: " + request.getParameter(parameterName));
        }
                  //Some processing logic, call to the various services/methods with different parameters, response is always String(Json)
        String response = service.callSomeServiceMethods(param1,param2,param3);

logger.info("Response is: " + response);

        long end = System.currentTimeMillis();
        logger.info("Requested completed in: " + (end-start) + "ms");
        logger.info("!--REQUEST END--!");   

        return response;
    }

Итак, что я делаю прямо сейчас для разных контроллеров/методов, это копировать все из начала внутри метода до тех пор, пока логика обработки не будет отличаться от метода к методу, а затем скопирует все, что находится ниже, как показано в приведенном выше шаблоне.

Это довольно грязно, и есть много повторений кода (что мне не нравится). Но мне нужно все регистрировать.

Есть ли у кого-нибудь больше опыта в таких журналах, может ли кто-нибудь пролить свет на это?

Ответ 1

Используйте interceptor:

  • expand HandlerInterceptorAdapter и переопределить preHandle
  • определите его с помощью <mvc:interceptors> в dispatcher-servlet.xml

Он будет запускаться для каждого запроса.

Ответ 2

EDIT: Также см. комментарий @membersound на этот ответ, который улучшает этот ответ.

Spring поддерживает это. См. CommonsRequestLoggingFilter. Если вы используете Spring Boot, просто зарегистрируйте bean этого типа, и Boot применит его к цепочке фильтров. Как:

@Bean
public Filter logFilter() {
    CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter();
    filter.setIncludeQueryString(true);
    filter.setIncludePayload(true);
    filter.setMaxPayloadLength(5120);
    return filter;
}

Кроме того, для этого фильтра регистрации требуется, чтобы уровень журнала был установлен в DEBUG. Например. сделайте это в logback.xml с помощью:

<logger name="org.springframework.web.filter.CommonsRequestLoggingFilter" level="DEBUG"/>

Ответ 3

Основная проблема с запросом на чтение заключается в том, что, как только поток ввода потребляется, он исчезнет и не может быть снова прочитан. Поэтому входной поток должен быть кэширован. Вместо того, чтобы писать собственные классы для кеширования (которые можно найти в нескольких местах в Интернете), Spring предоставляет пару полезных классов, т.е. ContentCachingRequestWrapper и ContentCachingResponseWrapper. Эти классы могут быть использованы очень эффективно, например, в фильтрах для целей ведения журнала.

Определите фильтр в web.xml:

<filter>
    <filter-name>loggingFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>loggingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Так как фильтр объявлен как DelegatingFilterProxy, его можно объявить как bean с помощью @Component или @Bean аннотаций. В методе loggingFilter doFilter завершите запрос и ответ с Spring предоставленными классами, прежде чем передавать его в цепочку фильтров:

HttpServletRequest requestToCache = new ContentCachingRequestWrapper(request);
HttpServletResponse responseToCache = new ContentCachingResponseWrapper(response);
chain.doFilter(requestToCache, responseToCache);
String requestData = getRequestData(requestToCache);
String responseData = getResponseData(responseToCache);

Входной поток будет кэшироваться в обернутом запросе, как только поток ввода будет потреблен после chain.doFilter(). Затем его можно получить, как показано ниже:

public static String getRequestData(final HttpServletRequest request) throws UnsupportedEncodingException {
    String payload = null;
    ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
    if (wrapper != null) {
        byte[] buf = wrapper.getContentAsByteArray();
        if (buf.length > 0) {
            payload = new String(buf, 0, buf.length, wrapper.getCharacterEncoding());
        }
    }
    return payload;
}

Однако для ответа ситуация немного отличается. Поскольку ответ также был завернут перед передачей его в цепочку фильтров, он также будет кэшироваться в выходной поток, как только он будет записан на обратном пути. Но так как поток вывода также будет потреблен, поэтому вам нужно скопировать ответ обратно в выходной поток, используя wrapper.copyBodyToResponse(). См. Ниже:

public static String getResponseData(final HttpServletResponse response) throws IOException {
    String payload = null;
    ContentCachingResponseWrapper wrapper =
        WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
    if (wrapper != null) {
        byte[] buf = wrapper.getContentAsByteArray();
        if (buf.length > 0) {
            payload = new String(buf, 0, buf.length, wrapper.getCharacterEncoding());
            wrapper.copyBodyToResponse();
        }
    }
    return payload;
}

Надеюсь, что это поможет!

Ответ 4

Здесь небольшая библиотека, которую я написал, вы можете использовать: spring-mvc-logger

Я сделал это доступным через maven central:

<dependency>
    <groupId>com.github.isrsal</groupId>
    <artifactId>spring-mvc-logger</artifactId>
    <version>0.2</version>
</dependency>

Ответ 5

Как любой технический ответ... это зависит. на техническом стеке, который вы используете, и каковы ваши требования.

например, более общий, который вы хотите сделать для ведения журнала, дальнейший аванс, который вы хотели бы сделать. в вашем случае вы регистрируете только запросы, которые включены в журнал, и обрабатываются в контексте spring. Таким образом, вы можете "пропустить" другие запросы.

Я бы посмотрел на контейнер или веб-сервер, который вы используете для запуска вашего приложения. Это устранит эту зависимость от Spring. Плюс-контейнеры обеспечивают гибкость подключения провайдера протоколирования, а затем настраивают формат внешнего кода журнала. Например, если вы используете веб-сервер Apache, используйте ведение журнала веб-сервера Apache для регистрации всех HTTP-запросов на уровне регистрации доступа. Но будьте осторожны, у некоторых вариантов ведения журнала есть штрафы за производительность. Запишите только то, что вам действительно необходимо для перспективы контроля шаблонов доступа.

Если вы используете tomcat, то tomcat также позволит вам записывать материал. Найдите Access Valve в документации tomcat для Tomcat, которую вы используете. Это откроет мир возможностей.

Более обширное ведение журнала должно быть областью стратегии исключения, т.е. той деталью, которую вы хотите увидеть, когда проблема возникает в системе.

Ответ 6

Добавление к тому, о чем @B.Ali ответил. Если вы используете это в сценарии обработки асинхронного запроса spring (serlvet 3.0 или выше), тогда для меня работает следующий код.

public class OncePerRequestLoggingFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    boolean isFirstRequest = !isAsyncDispatch(request);
    HttpServletRequest requestToUse = request;
    HttpServletResponse responseToUse = response;

    // The below check is critical and if not there, then the request/response gets corrupted.
    // Probably because in async case the filter is invoked multiple times.
    if (isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
        requestToUse = new ContentCachingRequestWrapper(request);
    }

    if (isFirstRequest && !(response instanceof ContentCachingResponseWrapper)) {
        responseToUse = new ContentCachingResponseWrapper(response);
    }

    filterChain.doFilter(requestToUse, responseToUse);

    if (!isAsyncStarted(request)) {
        ContentCachingResponseWrapper responseWrapper =
                WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        responseWrapper.copyBodyToResponse(); // IMPORTANT to copy it back to response
    }
}

@Override
protected boolean shouldNotFilterAsyncDispatch() {
    return false; // IMPORTANT this is true by default and wont work in async scenario.
}

}