Spring Boot - Как регистрировать все запросы и ответы с исключениями в одном месте?

220

Я работаю над отдыхом API с весенней загрузки. Мне нужно регистрировать все запросы с входными параметрами (с помощью методов, например, GET, POST и т. Д.), Пути запроса, строки запроса, соответствующего метода класса этого запроса, а также ответа на это действие, как успеха, так и ошибок.

Для примера:

успешный запрос:

http://example.com/api/users/1

Лог должен выглядеть примерно так:

{
   HttpStatus: 200,
   path: "api/users/1",
   method: "GET",
   clientIp: "0.0.0.0",
   accessToken: "XHGu6as5dajshdgau6i6asdjhgjhg",
   method: "UsersController.getUser",
   arguments: {
     id: 1 
   },
   response: {
      user: {
        id: 1,
        username: "user123",
        email: "user123@example.com"   
      }
   },
   exceptions: []       
}

Или запрос с ошибкой:

http://example.com/api/users/9999

Журнал должен быть примерно таким:

    {
       HttpStatus: 404,
       errorCode: 101,                 
       path: "api/users/9999",
       method: "GET",
       clientIp: "0.0.0.0",
       accessToken: "XHGu6as5dajshdgau6i6asdjhgjhg",
       method: "UsersController.getUser",
       arguments: {
         id: 9999 
       },
       returns: {            
       },
       exceptions: [
         {
           exception: "UserNotFoundException",
           message: "User with id 9999 not found",
           exceptionId: "adhaskldjaso98d7324kjh989",
           stacktrace: ...................    
       ]       
    }

Я хочу, чтобы Запрос / Ответ был единым объектом, с пользовательской информацией, связанной с этим объектом, как в успешных, так и в случаях ошибок.

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

(Я играл с @ControllerAdvice и @ExceptionHandler, но, как я уже говорил, мне нужно обрабатывать все запросы об успехе и ошибках в одном месте (и в одном журнале)).

Теймураз
источник
Возможно, через входящий в систему ServletFilter (например, stackoverflow.com/a/2171633/995891 ), в качестве альтернативы, HandlerInterceptorно это может не сработать при регистрации ответа, как указано в ответе: concretepage.com/spring/spring-mvc/… - HandlerInterceptor имеет доступ к методу (метод: «UsersController.getUser»), хотя. Это не известно в фильтре сервлетов.
Запл
1
тем не менее, даже если вы добавите фильтр или какое-либо решение на уровне приложения, вы не будете регистрировать весь запрос, например, ошибка сервера HTTP 500 не будет записана в журнал, потому что в то время как необработанное исключение будет выдано на уровне приложения, встроенная по умолчанию ошибка на странице с сообщениями будет отображаться после проглатывания исключения, и, конечно, журнал не будет сохраняться. Также, если вы проверяете ответ пользователя 1817243, в случае какого-либо исключения он снова не регистрирует запрос, а регистрирует исключение (!!).
AntJavaDev
Должен ли этот формат журнала соответствовать каждому написанному вами символу? Похоже, что перевод JSON будет оптимальным в вашем случае: LogClass{ getRequestAndSaveIt()} Gson.toJson(LogClass)как псевдокод
Vale
1
Будущий читатель может извлечь пользу из моего ответа (URL, чтобы следовать в этом комментарии). По сути, мне удалось объединить разные посты по этому вопросу. ПОЖАЛУЙСТА, рассмотрите ответ привода (в ответах ниже) перед тем, как попробовать его вручную. Но ответ, который я публикую, позволяет регистрировать «400, 404, 500» (любой / все), но устанавливая приоритет порядка на самый низкий приоритет (или в пределах «8», если вы посмотрите на код). stackoverflow.com/questions/10210645/…
granadaCoder
Я следовал весенним документам по регистрации здесь: docs.spring.io/spring-boot/docs/current/reference/html/…
T04435

Ответы:

150

Не пишите никаких перехватчиков, фильтров, компонентов, аспектов и т. Д., Это очень распространенная проблема, которая решалась много раз.

Spring Boot имеет модули под названием Actuator , которые обеспечивают вход HTTP-запросов из коробки. Есть конечная точка, сопоставленная с /trace(SB1.x) или /actuator/httptrace(SB2.0 +), которая покажет вам последние 100 HTTP-запросов. Вы можете настроить его для регистрации каждого запроса или записи в БД.

Чтобы получить нужные конечные точки, вам понадобится зависимость spring-boot-starter-actator, а также «белый список» конечных точек, которые вы ищете, и, возможно, настройка или отключение защиты для них.

Кроме того, где будет работать это приложение? Будете ли вы использовать PaaS? Хостинг-провайдеры, например Heroku, обеспечивают ведение журнала запросов как часть своей услуги, и вам не нужно вообще ничего кодировать.

SergeyB
источник
4
какие-нибудь подробности? Я нашел github.com/spring-projects/spring-boot/tree/master/… , но не намного дальше.
Том Ховард
16
Это не может быть использовано для отладки: неаутентифицированные запросы (например, с использованием Spring Security) не регистрируются.
Bekce
11
На самом деле Actuator не имеет каких-либо конкретных компонентов для отключения http-регистрации. / trace - показывать только последние N запросов.
Владимир Филипченко
18
@ike_love, как настроить привод так, чтобы он регистрировал запрос (также тело POST) в файл?
11
Трассировка не будет регистрировать запрос и тело ответа для вас .... все остальное (заголовок и т. Д.), Кроме тех.
Лекки
95

Spring уже предоставляет фильтр, который выполняет эту работу. Добавьте следующий bean в ваш конфиг

@Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
    CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter();
    loggingFilter.setIncludeClientInfo(true);
    loggingFilter.setIncludeQueryString(true);
    loggingFilter.setIncludePayload(true);
    loggingFilter.setMaxPayloadLength(64000);
    return loggingFilter;
}

Не забудьте изменить уровень журнала org.springframework.web.filter.CommonsRequestLoggingFilterна DEBUG.

Йогеш Бадке
источник
75
Обратите внимание, что он не регистрирует ответы, а только запросы.
Вим Deblauwe
1
Есть только запросы. Как регистрировать тела ответов, используя CommonsRequestLoggingFilter?
user2602807
3
Также это не
входит
Что ж, это ожидаемо, так как это фильтр регистрации запросов. Подробнее об этом здесь: docs.spring.io/spring/docs/current/javadoc-api/org/…
Йогеш Бадке
4
Если у вас большое тело JSON, задайте для длины полезной нагрузки большое число, чтобы регистрировать все тело запроса. loggingFilter.setMaxPayloadLength (100000);
Венкатеш Наннан
58

Вы можете использовать, javax.servlet.Filterесли нет необходимости регистрировать выполненный Java-метод.

Но с этим требованием , вы должны доступ к информации , хранящейся в handlerMappingо DispatcherServlet. Тем не менее, вы можете переопределить, DispatcherServletчтобы выполнить регистрацию пары запрос / ответ.

Ниже приведен пример идеи, которая может быть усовершенствована и адаптирована к вашим потребностям.

public class LoggableDispatcherServlet extends DispatcherServlet {

    private final Log logger = LogFactory.getLog(getClass());

    @Override
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        if (!(request instanceof ContentCachingRequestWrapper)) {
            request = new ContentCachingRequestWrapper(request);
        }
        if (!(response instanceof ContentCachingResponseWrapper)) {
            response = new ContentCachingResponseWrapper(response);
        }
        HandlerExecutionChain handler = getHandler(request);

        try {
            super.doDispatch(request, response);
        } finally {
            log(request, response, handler);
            updateResponse(response);
        }
    }

    private void log(HttpServletRequest requestToCache, HttpServletResponse responseToCache, HandlerExecutionChain handler) {
        LogMessage log = new LogMessage();
        log.setHttpStatus(responseToCache.getStatus());
        log.setHttpMethod(requestToCache.getMethod());
        log.setPath(requestToCache.getRequestURI());
        log.setClientIp(requestToCache.getRemoteAddr());
        log.setJavaMethod(handler.toString());
        log.setResponse(getResponsePayload(responseToCache));
        logger.info(log);
    }

    private String getResponsePayload(HttpServletResponse response) {
        ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        if (wrapper != null) {

            byte[] buf = wrapper.getContentAsByteArray();
            if (buf.length > 0) {
                int length = Math.min(buf.length, 5120);
                try {
                    return new String(buf, 0, length, wrapper.getCharacterEncoding());
                }
                catch (UnsupportedEncodingException ex) {
                    // NOOP
                }
            }
        }
        return "[unknown]";
    }

    private void updateResponse(HttpServletResponse response) throws IOException {
        ContentCachingResponseWrapper responseWrapper =
            WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        responseWrapper.copyBodyToResponse();
    }

}

HandlerExecutionChain - содержит информацию об обработчике запроса.

Затем вы можете зарегистрировать этого диспетчера следующим образом:

    @Bean
    public ServletRegistrationBean dispatcherRegistration() {
        return new ServletRegistrationBean(dispatcherServlet());
    }

    @Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
    public DispatcherServlet dispatcherServlet() {
        return new LoggableDispatcherServlet();
    }

А вот образец логов:

http http://localhost:8090/settings/test
i.g.m.s.s.LoggableDispatcherServlet      : LogMessage{httpStatus=500, path='/error', httpMethod='GET', clientIp='127.0.0.1', javaMethod='HandlerExecutionChain with handler [public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)] and 3 interceptors', arguments=null, response='{"timestamp":1472475814077,"status":500,"error":"Internal Server Error","exception":"java.lang.RuntimeException","message":"org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.RuntimeException","path":"/settings/test"}'}

http http://localhost:8090/settings/params
i.g.m.s.s.LoggableDispatcherServlet      : LogMessage{httpStatus=200, path='/settings/httpParams', httpMethod='GET', clientIp='127.0.0.1', javaMethod='HandlerExecutionChain with handler [public x.y.z.DTO x.y.z.Controller.params()] and 3 interceptors', arguments=null, response='{}'}

http http://localhost:8090/123
i.g.m.s.s.LoggableDispatcherServlet      : LogMessage{httpStatus=404, path='/error', httpMethod='GET', clientIp='127.0.0.1', javaMethod='HandlerExecutionChain with handler [public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)] and 3 interceptors', arguments=null, response='{"timestamp":1472475840592,"status":404,"error":"Not Found","message":"Not Found","path":"/123"}'}

ОБНОВИТЬ

В случае ошибок Spring выполняет автоматическую обработку ошибок. Следовательно, BasicErrorController#errorотображается как обработчик запросов. Если вы хотите сохранить оригинальный обработчик запросов, то вы можете изменить это поведение на spring-webmvc-4.2.5.RELEASE-sources.jar!/org/springframework/web/servlet/DispatcherServlet.java:971перед тем #processDispatchResultназывается, кэшировать оригинальный обработчик.

Hahn
источник
2
что происходит, когда ответом является поток, а поток не поддерживает поиск? Будет ли выше все еще работать?
Том Ховард
Меня не волнует вызванный метод, только данные, полученные и отправленные. Фильтр , кажется, мне точку в правильном направлении , и @ ike_love в ответ направил меня github.com/spring-projects/spring-boot/blob/master/...
Том Ховард
@ TomHoward AFAIK, весной нет готовых «журналов ответов». Поэтому вы можете расширить WebRequestTraceFilter или AbstractRequestLoggingFilter, добавив логику регистрации ответов.
Хан
Работает просто отлично!
Павел Власов
@hahn, почему ты использовал для этого сервлет Dispatcher? нельзя ли добавить тот же логин с фильтром в doFilter?
BhendiGawaar
39

Библиотека Журнала специально создана для регистрации HTTP-запросов и ответов. Он поддерживает Spring Boot с использованием специальной стартовой библиотеки.

Чтобы включить ведение журнала в Spring Boot, все, что вам нужно сделать, это добавить библиотеку в зависимости вашего проекта. Например, если вы используете Maven:

<dependency>
    <groupId>org.zalando</groupId>
    <artifactId>logbook-spring-boot-starter</artifactId>
    <version>1.5.0</version>
</dependency>

По умолчанию вывод журнала выглядит так:

{
  "origin" : "local",
  "correlation" : "52e19498-890c-4f75-a06c-06ddcf20836e",
  "status" : 200,
  "headers" : {
    "X-Application-Context" : [
      "application:8088"
    ],
    "Content-Type" : [
      "application/json;charset=UTF-8"
    ],
    "Transfer-Encoding" : [
      "chunked"
    ],
    "Date" : [
      "Sun, 24 Dec 2017 13:10:45 GMT"
    ]
  },
  "body" : {
    "thekey" : "some_example"
  },
  "duration" : 105,
  "protocol" : "HTTP/1.1",
  "type" : "response"
}

Однако он не выводит имя класса, который обрабатывает запрос. Библиотека имеет некоторые интерфейсы для написания пользовательских регистраторов.

sihaya
источник
4
добавлен в качестве зависимости к минимальному приложению с весенней загрузкой и попытался запустить - без изменений, вообще не выводится логирование в моем приложении. Я думаю, что есть дополнительные зависимости или классы, которые нужны? Регистрация его в качестве фильтра, похоже, тоже ничего не делает.
эйс
1
@eis Вам необходимо зарегистрировать его в качестве фильтра, как описано в документации здесь. github.com/zalando/logbook
Пратик Сингхал
2
В журнале говорится: «Журнал поставляется с удобной автоматической настройкой для пользователей Spring Boot. Он автоматически настраивает все следующие части с разумными настройками по умолчанию». Но это не работает.
Леос Литерак
5
@LeosLiterak Я полагаю, вам нужно добавить logging.level.org.zalando.logbook=TRACE в свой application.properties(как указано в Readme)
TolkienWASP
2
Автоконфигурация журнала, похоже, не работает для spring-boot v2.0.5
Yashveer Rana
26

Я определил уровень входа в систему application.propertiesдля печати запросов / ответов, метод URL в файле журнала

logging.level.org.springframework.web=DEBUG
logging.level.org.hibernate.SQL=INFO
logging.file=D:/log/myapp.log

Я использовал Spring Boot.

Анкит
источник
2
Да, вы правы - это верный ответ для получения запросов на вход в тот же файл журнала со всеми другими результатами. Однако @moreo попросил войти в GET, POST и т. Д. И в отдельный файл (как я понимаю)
Манушин Игорь
4
Мне нравится этот. нулевая драма
Квирино Джервасио
1
Если вы хотите, чтобы заголовки были включены в журнал, то вы должны добавить: «spring.http.log-request-details = true» в ваш файл application.properties.
jfajunior
20

Вот как я делаю это весной, используя данные org.springframework.web.util.ContentCachingRequestWrapper и org.springframework.web.util.ContentCachingResponseWrapper

/**
 * Doogies very cool HTTP request logging
 *
 * There is also {@link org.springframework.web.filter.CommonsRequestLoggingFilter}  but it cannot log request method
 * And it cannot easily be extended.
 *
 * https://mdeinum.wordpress.com/2015/07/01/spring-framework-hidden-gems/
 * http://stackoverflow.com/questions/8933054/how-to-read-and-copy-the-http-servlet-response-output-stream-content-for-logging
 */
public class DoogiesRequestLogger extends OncePerRequestFilter {

  private boolean includeResponsePayload = true;
  private int maxPayloadLength = 1000;

  private String getContentAsString(byte[] buf, int maxLength, String charsetName) {
    if (buf == null || buf.length == 0) return "";
    int length = Math.min(buf.length, this.maxPayloadLength);
    try {
      return new String(buf, 0, length, charsetName);
    } catch (UnsupportedEncodingException ex) {
      return "Unsupported Encoding";
    }
  }

  /**
   * Log each request and respponse with full Request URI, content payload and duration of the request in ms.
   * @param request the request
   * @param response the response
   * @param filterChain chain of filters
   * @throws ServletException
   * @throws IOException
   */
  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

    long startTime = System.currentTimeMillis();
    StringBuffer reqInfo = new StringBuffer()
     .append("[")
     .append(startTime % 10000)  // request ID
     .append("] ")
     .append(request.getMethod())
     .append(" ")
     .append(request.getRequestURL());

    String queryString = request.getQueryString();
    if (queryString != null) {
      reqInfo.append("?").append(queryString);
    }

    if (request.getAuthType() != null) {
      reqInfo.append(", authType=")
        .append(request.getAuthType());
    }
    if (request.getUserPrincipal() != null) {
      reqInfo.append(", principalName=")
        .append(request.getUserPrincipal().getName());
    }

    this.logger.debug("=> " + reqInfo);

    // ========= Log request and response payload ("body") ========
    // We CANNOT simply read the request payload here, because then the InputStream would be consumed and cannot be read again by the actual processing/server.
    //    String reqBody = DoogiesUtil._stream2String(request.getInputStream());   // THIS WOULD NOT WORK!
    // So we need to apply some stronger magic here :-)
    ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
    ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);

    filterChain.doFilter(wrappedRequest, wrappedResponse);     // ======== This performs the actual request!
    long duration = System.currentTimeMillis() - startTime;

    // I can only log the request's body AFTER the request has been made and ContentCachingRequestWrapper did its work.
    String requestBody = this.getContentAsString(wrappedRequest.getContentAsByteArray(), this.maxPayloadLength, request.getCharacterEncoding());
    if (requestBody.length() > 0) {
      this.logger.debug("   Request body:\n" +requestBody);
    }

    this.logger.debug("<= " + reqInfo + ": returned status=" + response.getStatus() + " in "+duration + "ms");
    if (includeResponsePayload) {
      byte[] buf = wrappedResponse.getContentAsByteArray();
      this.logger.debug("   Response body:\n"+getContentAsString(buf, this.maxPayloadLength, response.getCharacterEncoding()));
    }

    wrappedResponse.copyBodyToResponse();  // IMPORTANT: copy content of response back into original response

  }


}
Роберт
источник
18

Если вы не возражаете попробовать Spring AOP, это то, что я исследовал для целей ведения журналов, и это работает довольно хорошо для меня. Это не будет регистрировать запросы, которые не были определены, и неудачные попытки запроса, хотя.

Добавьте эти три зависимости

spring-aop, aspectjrt, aspectjweaver

Добавьте это в ваш конфигурационный файл xml <aop:aspectj-autoproxy/>

Создайте аннотацию, которую можно использовать как pointcut

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface EnableLogging {
ActionType actionType();
}

Теперь аннотируйте все остальные методы API, которые вы хотите зарегистрировать

@EnableLogging(actionType = ActionType.SOME_EMPLOYEE_ACTION)
@Override
public Response getEmployees(RequestDto req, final String param) {
...
}

Теперь перейдем к Аспекту. компонентное сканирование пакета, в котором находится этот класс.

@Aspect
@Component
public class Aspects {

@AfterReturning(pointcut = "execution(@co.xyz.aspect.EnableLogging * *(..)) && @annotation(enableLogging) && args(reqArg, reqArg1,..)", returning = "result")
public void auditInfo(JoinPoint joinPoint, Object result, EnableLogging enableLogging, Object reqArg, String reqArg1) {

    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
            .getRequest();

    if (result instanceof Response) {
        Response responseObj = (Response) result;

    String requestUrl = request.getScheme() + "://" + request.getServerName()
                + ":" + request.getServerPort() + request.getContextPath() + request.getRequestURI()
                + "?" + request.getQueryString();

String clientIp = request.getRemoteAddr();
String clientRequest = reqArg.toString();
int httpResponseStatus = responseObj.getStatus();
responseObj.getEntity();
// Can log whatever stuff from here in a single spot.
}


@AfterThrowing(pointcut = "execution(@co.xyz.aspect.EnableLogging * *(..)) && @annotation(enableLogging) && args(reqArg, reqArg1,..)", throwing="exception")
public void auditExceptionInfo(JoinPoint joinPoint, Throwable exception, EnableLogging enableLogging, Object reqArg, String reqArg1) {

    HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
            .getRequest();

    String requestUrl = request.getScheme() + "://" + request.getServerName()
    + ":" + request.getServerPort() + request.getContextPath() + request.getRequestURI()
    + "?" + request.getQueryString();

    exception.getMessage();
    exception.getCause();
    exception.printStackTrace();
    exception.getLocalizedMessage();
    // Can log whatever exceptions, requests, etc from here in a single spot.
    }
}

Совет @AfterReturning запускается, когда выполнение соответствующего метода возвращается нормально.

@AfterThrowing advice запускается, когда выполнение соответствующего метода завершается с помощью исключения.

Если вы хотите прочитать подробно прочитайте это. http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html

Серый гусь
источник
1
Это регистрирует вызов метода, а не то, что было фактически получено и отправлено на уровне HTTP.
Том Ховард
1
Как написать запрос BODY? В моем случае это ПОЧТА ТЕЛА. на request.getReader или getInputStream я получаю сообщение об ошибке, что поток закрыт.
13

После добавления Actuators в основанное на весенней загрузке приложение, вам /traceдоступна конечная точка с информацией о последних запросах. Эта конечная точка работает на основе TraceRepository, а реализацией по умолчанию является InMemoryTraceRepository, которая сохраняет последние 100 вызовов. Вы можете изменить это, реализовав этот интерфейс самостоятельно и сделав его доступным в виде bean-компонента Spring. Например, чтобы регистрировать все запросы на запись (и при этом использовать реализацию по умолчанию в качестве основного хранилища для предоставления информации о /traceконечной точке), я использую этот вид реализации:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.actuate.trace.InMemoryTraceRepository;
import org.springframework.boot.actuate.trace.Trace;
import org.springframework.boot.actuate.trace.TraceRepository;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;


@Component
public class LoggingTraceRepository implements TraceRepository {

  private static final Logger LOG = LoggerFactory.getLogger(LoggingTraceRepository.class);
  private final TraceRepository delegate = new InMemoryTraceRepository();

  @Override
  public List<Trace> findAll() {
    return delegate.findAll();
  }

  @Override
  public void add(Map<String, Object> traceInfo) {
    LOG.info(traceInfo.toString());
    this.delegate.add(traceInfo);
  }
}

Эта traceInfoкарта содержит основную информацию о запросе и ответ в таком роде виде: {method=GET, path=/api/hello/John, headers={request={host=localhost:8080, user-agent=curl/7.51.0, accept=*/*}, response={X-Application-Context=application, Content-Type=text/plain;charset=UTF-8, Content-Length=10, Date=Wed, 29 Mar 2017 20:41:21 GMT, status=200}}}. Здесь нет НИКАКОГО содержания ответа.

РЕДАКТИРОВАТЬ! Регистрация данных POST

Вы можете получить доступ к данным POST, переопределив WebRequestTraceFilter , но не думайте, что это хорошая идея (например, все загруженное содержимое файла попадет в журналы). Вот пример кода, но не используйте его:

package info.fingo.nuntius.acuate.trace;

import org.apache.commons.io.IOUtils;
import org.springframework.boot.actuate.trace.TraceProperties;
import org.springframework.boot.actuate.trace.TraceRepository;
import org.springframework.boot.actuate.trace.WebRequestTraceFilter;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.LinkedHashMap;
import java.util.Map;

@Component
public class CustomWebTraceFilter extends WebRequestTraceFilter {

  public CustomWebTraceFilter(TraceRepository repository, TraceProperties properties) {
    super(repository, properties);
}

  @Override
  protected Map<String, Object> getTrace(HttpServletRequest request) {
    Map<String, Object> trace = super.getTrace(request);
    String multipartHeader = request.getHeader("content-type");
    if (multipartHeader != null && multipartHeader.startsWith("multipart/form-data")) {
        Map<String, Object> parts = new LinkedHashMap<>();
        try {
            request.getParts().forEach(
                    part -> {
                        try {
                            parts.put(part.getName(), IOUtils.toString(part.getInputStream(), Charset.forName("UTF-8")));
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
            );
        } catch (IOException | ServletException e) {
            e.printStackTrace();
        }
        if (!parts.isEmpty()) {
            trace.put("multipart-content-map", parts);
        }
    }
    return trace;
  }
}
Петр Чованец
источник
1
А как насчет тела POST?
Павел Вязанкин
@dart Я добавил для тебя пример
Петр Чованец
1
Я делал что-то подобное, но проблема в том, что тело ответа недоступно TraceRepository, как мы можем получить к нему доступ?
Амир Пашазаде
@AmirPashazadeh вы должны переопределить, protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)но я не уверен, когда этот фильтр выполняется - может быть на этапе запроса, поэтому тело ответа там не будет готово.
Петр Чованец
1
@Kekar ​​Начиная с 2.0 существует HttpTraceRepository (вместо TraceRepository)
Петр Чованец,
12

Этот код работает для меня в приложении Spring Boot - просто зарегистрируйте его как фильтр

    import java.io.BufferedReader;
    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.io.PrintWriter;
    import java.util.Collection;
    import java.util.Enumeration;
    import java.util.HashMap;
    import java.util.Locale;
    import java.util.Map;
    import javax.servlet.*;
    import javax.servlet.http.Cookie;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletRequestWrapper;
    import javax.servlet.http.HttpServletResponse;
    import org.apache.commons.io.output.TeeOutputStream;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Component;

    @Component
    public class HttpLoggingFilter implements Filter {

        private static final Logger log = LoggerFactory.getLogger(HttpLoggingFilter.class);

        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
        }

        @Override
        public void doFilter(ServletRequest request, ServletResponse response,
                             FilterChain chain) throws IOException, ServletException {
            try {
                HttpServletRequest httpServletRequest = (HttpServletRequest) request;
                HttpServletResponse httpServletResponse = (HttpServletResponse) response;

                Map<String, String> requestMap = this
                        .getTypesafeRequestMap(httpServletRequest);
                BufferedRequestWrapper bufferedRequest = new BufferedRequestWrapper(
                        httpServletRequest);
                BufferedResponseWrapper bufferedResponse = new BufferedResponseWrapper(
                        httpServletResponse);

                final StringBuilder logMessage = new StringBuilder(
                        "REST Request - ").append("[HTTP METHOD:")
                        .append(httpServletRequest.getMethod())
                        .append("] [PATH INFO:")
                        .append(httpServletRequest.getServletPath())
                        .append("] [REQUEST PARAMETERS:").append(requestMap)
                        .append("] [REQUEST BODY:")
                        .append(bufferedRequest.getRequestBody())
                        .append("] [REMOTE ADDRESS:")
                        .append(httpServletRequest.getRemoteAddr()).append("]");

                chain.doFilter(bufferedRequest, bufferedResponse);
                logMessage.append(" [RESPONSE:")
                        .append(bufferedResponse.getContent()).append("]");
                log.debug(logMessage.toString());
            } catch (Throwable a) {
                log.error(a.getMessage());
            }
        }

        private Map<String, String> getTypesafeRequestMap(HttpServletRequest request) {
            Map<String, String> typesafeRequestMap = new HashMap<String, String>();
            Enumeration<?> requestParamNames = request.getParameterNames();
            while (requestParamNames.hasMoreElements()) {
                String requestParamName = (String) requestParamNames.nextElement();
                String requestParamValue;
                if (requestParamName.equalsIgnoreCase("password")) {
                    requestParamValue = "********";
                } else {
                    requestParamValue = request.getParameter(requestParamName);
                }
                typesafeRequestMap.put(requestParamName, requestParamValue);
            }
            return typesafeRequestMap;
        }

        @Override
        public void destroy() {
        }

        private static final class BufferedRequestWrapper extends
                HttpServletRequestWrapper {

            private ByteArrayInputStream bais = null;
            private ByteArrayOutputStream baos = null;
            private BufferedServletInputStream bsis = null;
            private byte[] buffer = null;

            public BufferedRequestWrapper(HttpServletRequest req)
                    throws IOException {
                super(req);
                // Read InputStream and store its content in a buffer.
                InputStream is = req.getInputStream();
                this.baos = new ByteArrayOutputStream();
                byte buf[] = new byte[1024];
                int read;
                while ((read = is.read(buf)) > 0) {
                    this.baos.write(buf, 0, read);
                }
                this.buffer = this.baos.toByteArray();
            }

            @Override
            public ServletInputStream getInputStream() {
                this.bais = new ByteArrayInputStream(this.buffer);
                this.bsis = new BufferedServletInputStream(this.bais);
                return this.bsis;
            }

            String getRequestBody() throws IOException {
                BufferedReader reader = new BufferedReader(new InputStreamReader(
                        this.getInputStream()));
                String line = null;
                StringBuilder inputBuffer = new StringBuilder();
                do {
                    line = reader.readLine();
                    if (null != line) {
                        inputBuffer.append(line.trim());
                    }
                } while (line != null);
                reader.close();
                return inputBuffer.toString().trim();
            }

        }

        private static final class BufferedServletInputStream extends
                ServletInputStream {

            private ByteArrayInputStream bais;

            public BufferedServletInputStream(ByteArrayInputStream bais) {
                this.bais = bais;
            }

            @Override
            public int available() {
                return this.bais.available();
            }

            @Override
            public int read() {
                return this.bais.read();
            }

            @Override
            public int read(byte[] buf, int off, int len) {
                return this.bais.read(buf, off, len);
            }

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

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

            @Override
            public void setReadListener(ReadListener readListener) {

            }
        }

        public class TeeServletOutputStream extends ServletOutputStream {

            private final TeeOutputStream targetStream;

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

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

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

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

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

            @Override
            public void setWriteListener(WriteListener writeListener) {

            }
        }

        public class BufferedResponseWrapper implements HttpServletResponse {

            HttpServletResponse original;
            TeeServletOutputStream tee;
            ByteArrayOutputStream bos;

            public BufferedResponseWrapper(HttpServletResponse response) {
                original = response;
            }

            public String getContent() {
                return bos.toString();
            }

            public PrintWriter getWriter() throws IOException {
                return original.getWriter();
            }

            public ServletOutputStream getOutputStream() throws IOException {
                if (tee == null) {
                    bos = new ByteArrayOutputStream();
                    tee = new TeeServletOutputStream(original.getOutputStream(),
                            bos);
                }
                return tee;

            }

            @Override
            public String getCharacterEncoding() {
                return original.getCharacterEncoding();
            }

            @Override
            public String getContentType() {
                return original.getContentType();
            }

            @Override
            public void setCharacterEncoding(String charset) {
                original.setCharacterEncoding(charset);
            }

            @Override
            public void setContentLength(int len) {
                original.setContentLength(len);
            }

            @Override
            public void setContentLengthLong(long l) {
                original.setContentLengthLong(l);
            }

            @Override
            public void setContentType(String type) {
                original.setContentType(type);
            }

            @Override
            public void setBufferSize(int size) {
                original.setBufferSize(size);
            }

            @Override
            public int getBufferSize() {
                return original.getBufferSize();
            }

            @Override
            public void flushBuffer() throws IOException {
                tee.flush();
            }

            @Override
            public void resetBuffer() {
                original.resetBuffer();
            }

            @Override
            public boolean isCommitted() {
                return original.isCommitted();
            }

            @Override
            public void reset() {
                original.reset();
            }

            @Override
            public void setLocale(Locale loc) {
                original.setLocale(loc);
            }

            @Override
            public Locale getLocale() {
                return original.getLocale();
            }

            @Override
            public void addCookie(Cookie cookie) {
                original.addCookie(cookie);
            }

            @Override
            public boolean containsHeader(String name) {
                return original.containsHeader(name);
            }

            @Override
            public String encodeURL(String url) {
                return original.encodeURL(url);
            }

            @Override
            public String encodeRedirectURL(String url) {
                return original.encodeRedirectURL(url);
            }

            @SuppressWarnings("deprecation")
            @Override
            public String encodeUrl(String url) {
                return original.encodeUrl(url);
            }

            @SuppressWarnings("deprecation")
            @Override
            public String encodeRedirectUrl(String url) {
                return original.encodeRedirectUrl(url);
            }

            @Override
            public void sendError(int sc, String msg) throws IOException {
                original.sendError(sc, msg);
            }

            @Override
            public void sendError(int sc) throws IOException {
                original.sendError(sc);
            }

            @Override
            public void sendRedirect(String location) throws IOException {
                original.sendRedirect(location);
            }

            @Override
            public void setDateHeader(String name, long date) {
                original.setDateHeader(name, date);
            }

            @Override
            public void addDateHeader(String name, long date) {
                original.addDateHeader(name, date);
            }

            @Override
            public void setHeader(String name, String value) {
                original.setHeader(name, value);
            }

            @Override
            public void addHeader(String name, String value) {
                original.addHeader(name, value);
            }

            @Override
            public void setIntHeader(String name, int value) {
                original.setIntHeader(name, value);
            }

            @Override
            public void addIntHeader(String name, int value) {
                original.addIntHeader(name, value);
            }

            @Override
            public void setStatus(int sc) {
                original.setStatus(sc);
            }

            @SuppressWarnings("deprecation")
            @Override
            public void setStatus(int sc, String sm) {
                original.setStatus(sc, sm);
            }

            @Override
            public String getHeader(String arg0) {
                return original.getHeader(arg0);
            }

            @Override
            public Collection<String> getHeaderNames() {
                return original.getHeaderNames();
            }

            @Override
            public Collection<String> getHeaders(String arg0) {
                return original.getHeaders(arg0);
            }

            @Override
            public int getStatus() {
                return original.getStatus();
            }

        }
    }
user1817243
источник
Это хорошо работает для регистрации ответов - хотя мне пришлось установить ограничение на количество записываемых байтов, в противном случае он удаляет выходные данные консоли ведения журнала Intellij.
Адам
String getContent () {if (bos == null) {return String.format ("вызвано% s слишком рано", BufferedResponseWrapper.class.getCanonicalName ()); } byte [] bytes = bos.toByteArray (); вернуть новую строку (Arrays.copyOf (bytes, 5000)) + "...."; }
Адам
Также стоит включить переключатель log.isTraceEnabled () для ведения журнала.
Адам
6
Было бы здорово, если бы Java добавила некоторые методы по умолчанию в HttpServletResponse, поэтому нам не нужно писать такую ​​огромную реализацию.
Адам
1
плюс один за включение операторов импорта
granadaCoder
8

В настоящее время Spring Boot имеет функцию Actuator для получения журналов запросов и ответов.

Но вы также можете получить журналы, используя Aspect (AOP).

Аспект дает вам аннотацию , как: @Before, @AfterReturning, и @AfterThrowingт.д.

@Beforeрегистрирует запрос, @AfterReturningрегистрирует ответ и@AfterThrowing регистрирует сообщение об ошибке. Вам может не понадобиться журнал всех конечных точек, поэтому вы можете применить некоторые фильтры к пакетам.

Вот несколько примеров :

Для запроса:

@Before("within(your.package.where.endpoints.are..*)")
    public void endpointBefore(JoinPoint p) {
        if (log.isTraceEnabled()) {
            log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " START");
            Object[] signatureArgs = p.getArgs();


            ObjectMapper mapper = new ObjectMapper();
            mapper.enable(SerializationFeature.INDENT_OUTPUT);
            try {

                if (signatureArgs[0] != null) {
                    log.trace("\nRequest object: \n" + mapper.writeValueAsString(signatureArgs[0]));
                }
            } catch (JsonProcessingException e) {
            }
        }
    }

Здесь @Before("within(your.package.where.endpoints.are..*)")есть путь к пакету. Все конечные точки в этом пакете будут генерировать журнал.

Для ответа:

@AfterReturning(value = ("within(your.package.where.endpoints.are..*)"),
            returning = "returnValue")
    public void endpointAfterReturning(JoinPoint p, Object returnValue) {
        if (log.isTraceEnabled()) {
            ObjectMapper mapper = new ObjectMapper();
            mapper.enable(SerializationFeature.INDENT_OUTPUT);
            try {
                log.trace("\nResponse object: \n" + mapper.writeValueAsString(returnValue));
            } catch (JsonProcessingException e) {
                System.out.println(e.getMessage());
            }
            log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " END");
        }
    }

Здесь @AfterReturning("within(your.package.where.endpoints.are..*)")есть путь к пакету. Все конечные точки в этом пакете будут генерировать журнал. Также Object returnValueсодержит ответ.

Для исключения:

@AfterThrowing(pointcut = ("within(your.package.where.endpoints.are..*)"), throwing = "e")
public void endpointAfterThrowing(JoinPoint p, Exception e) throws DmoneyException {
    if (log.isTraceEnabled()) {
        System.out.println(e.getMessage());

        e.printStackTrace();


        log.error(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " " + e.getMessage());
    }
}

Здесь @AfterThrowing(pointcut = ("within(your.package.where.endpoints.are..*)"), throwing = "e") есть путь к пакету. Все конечные точки в этом пакете будут генерировать журнал. Также Exception eсодержит ответ об ошибке.

Вот полный код:

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Order(1)
@Component
@ConditionalOnExpression("${endpoint.aspect.enabled:true}")
public class EndpointAspect {
    static Logger log = Logger.getLogger(EndpointAspect.class);

    @Before("within(your.package.where.is.endpoint..*)")
    public void endpointBefore(JoinPoint p) {
        if (log.isTraceEnabled()) {
            log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " START");
            Object[] signatureArgs = p.getArgs();


            ObjectMapper mapper = new ObjectMapper();
            mapper.enable(SerializationFeature.INDENT_OUTPUT);
            try {

                if (signatureArgs[0] != null) {
                    log.trace("\nRequest object: \n" + mapper.writeValueAsString(signatureArgs[0]));
                }
            } catch (JsonProcessingException e) {
            }
        }
    }

    @AfterReturning(value = ("within(your.package.where.is.endpoint..*)"),
            returning = "returnValue")
    public void endpointAfterReturning(JoinPoint p, Object returnValue) {
        if (log.isTraceEnabled()) {
            ObjectMapper mapper = new ObjectMapper();
            mapper.enable(SerializationFeature.INDENT_OUTPUT);
            try {
                log.trace("\nResponse object: \n" + mapper.writeValueAsString(returnValue));
            } catch (JsonProcessingException e) {
                System.out.println(e.getMessage());
            }
            log.trace(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " END");
        }
    }


    @AfterThrowing(pointcut = ("within(your.package.where.is.endpoint..*)"), throwing = "e")
    public void endpointAfterThrowing(JoinPoint p, Exception e) throws Exception {
        if (log.isTraceEnabled()) {
            System.out.println(e.getMessage());

            e.printStackTrace();


            log.error(p.getTarget().getClass().getSimpleName() + " " + p.getSignature().getName() + " " + e.getMessage());
        }
    }
}

Здесь с помощью @ConditionalOnExpression("${endpoint.aspect.enabled:true}")вы можете включить / отключить журнал. просто добавить endpoint.aspect.enabled:trueв application.propertyи контролировать журнал

Больше информации о посещении АОП здесь:

Весенние доки про АОП

Пример статьи об АОП

Г-н Саджедул Карим
источник
1
new ObjectMapper()дорого, лучше поделитесь одним картографом на всех
Сэм
Да, конечно. Это демонстрационный код. В производстве мы должны следовать лучшим практикам.
Г-н Саджедул Карим
7

Вот мое решение (Spring 2.0.x)

Добавьте зависимость maven:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Отредактируйте файл application.properties и добавьте следующую строку:

management.endpoints.web.exposure.include=* 

Как только ваше весеннее загрузочное приложение запущено, вы можете отследить последние 100 http-запросов, вызвав этот URL: http: // localhost: 8070 / activator / httptrace

Энрико Джурин
источник
5

Вы также можете настроить пользовательский перехватчик Spring HandlerInterceptorAdapterдля упрощенной реализации перехватчиков только до / после:

@Component
public class CustomHttpInterceptor extends HandlerInterceptorAdapter {

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

        // Logs here

        return super.preHandle(request, response, handler);
    }

    @Override
    public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response,
            final Object handler, final Exception ex) {
        // Logs here
    }
}

Затем вы регистрируете столько перехватчиков, сколько хотите:

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    CustomHttpInterceptor customHttpInterceptor;

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

}

Примечание: так же, как заявлено @Robert , вам нужно обратить внимание на конкретные реализации и ваше приложение использует. HttpServletRequestHttpServletResponse

Например, для приложений, использующих ShallowEtagHeaderFilter, реализация ответа будет выглядеть ContentCachingResponseWrapperтак:

@Component
public class CustomHttpInterceptor extends HandlerInterceptorAdapter {

    private static final Logger LOGGER = LoggerFactory.getLogger(CustomHttpInterceptor.class);

    private static final int MAX_PAYLOAD_LENGTH = 1000;

    @Override
    public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response,
            final Object handler, final Exception ex) {
        final byte[] contentAsByteArray = ((ContentCachingResponseWrapper) response).getContentAsByteArray();

        LOGGER.info("Request body:\n" + getContentAsString(contentAsByteArray, response.getCharacterEncoding()));
    }

    private String getContentAsString(byte[] buf, String charsetName) {
        if (buf == null || buf.length == 0) {
            return "";
        }

        try {
            int length = Math.min(buf.length, MAX_PAYLOAD_LENGTH);

            return new String(buf, 0, length, charsetName);
        } catch (UnsupportedEncodingException ex) {
            return "Unsupported Encoding";
        }
    }

}
Bosco
источник
4

Ответ @ хана потребовал небольшой модификации, чтобы он работал для меня, но это, безусловно, самая настраиваемая вещь, которую я мог получить.

Это не сработало для меня, вероятно, потому что у меня также есть HandlerInterceptorAdapter [??], но я продолжал получать плохой ответ от сервера в этой версии. Вот моя модификация этого.

public class LoggableDispatcherServlet extends DispatcherServlet {

    private final Log logger = LogFactory.getLog(getClass());

    @Override
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

        long startTime = System.currentTimeMillis();
        try {
            super.doDispatch(request, response);
        } finally {
            log(new ContentCachingRequestWrapper(request), new ContentCachingResponseWrapper(response),
                    System.currentTimeMillis() - startTime);
        }
    }

    private void log(HttpServletRequest requestToCache, HttpServletResponse responseToCache, long timeTaken) {
        int status = responseToCache.getStatus();
        JsonObject jsonObject = new JsonObject();
        jsonObject.addProperty("httpStatus", status);
        jsonObject.addProperty("path", requestToCache.getRequestURI());
        jsonObject.addProperty("httpMethod", requestToCache.getMethod());
        jsonObject.addProperty("timeTakenMs", timeTaken);
        jsonObject.addProperty("clientIP", requestToCache.getRemoteAddr());
        if (status > 299) {
            String requestBody = null;
            try {
                requestBody = requestToCache.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
            } catch (IOException e) {
                e.printStackTrace();
            }
            jsonObject.addProperty("requestBody", requestBody);
            jsonObject.addProperty("requestParams", requestToCache.getQueryString());
            jsonObject.addProperty("tokenExpiringHeader",
                    responseToCache.getHeader(ResponseHeaderModifierInterceptor.HEADER_TOKEN_EXPIRING));
        }
        logger.info(jsonObject);
    }
}
AA_PV
источник
ваше приложение упаковано как war или jar? Я продолжаю получать сообщение об ошибке java.io.FileNotFoundException: Не удалось открыть ресурс ServletContext [/WEB-INF/loggingDispatcherServlet-servlet.xml]
Майанк Мадхав,
4

Если кому-то все еще это нужно, здесь простая реализация с Spring HttpTrace Actuator. Но, как они сказали наверху, это не регистрирует тела.

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.springframework.boot.actuate.trace.http.HttpTrace;
import org.springframework.boot.actuate.trace.http.InMemoryHttpTraceRepository;
import org.springframework.stereotype.Repository;

@Slf4j
@Repository
public class LoggingInMemoryHttpTraceRepository extends InMemoryHttpTraceRepository {
    public void add(HttpTrace trace) {
        super.add(trace);
        log.info("Trace:" + ToStringBuilder.reflectionToString(trace));
        log.info("Request:" + ToStringBuilder.reflectionToString(trace.getRequest()));
        log.info("Response:" + ToStringBuilder.reflectionToString(trace.getResponse()));
    }
}
Donz
источник
4

Пожалуйста, обратитесь к ссылке ниже для фактического ответа https://gist.github.com/int128/e47217bebdb4c402b2ffa7cc199307ba

Внесенные некоторые изменения из вышеупомянутого решения, запрос и ответ войдут в консоль и в файл, если уровень логгера - информация. мы можем напечатать или в консоли или в файле.

@Component
public class LoggingFilter extends OncePerRequestFilter {

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
        );
Logger log = LoggerFactory.getLogger(ReqAndResLoggingFilter.class);
private static final Path path = Paths.get("/home/ramesh/loggerReq.txt");
private static BufferedWriter writer = null;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    try {
        writer = Files.newBufferedWriter(path, Charset.forName("UTF-8"));
    if (isAsyncDispatch(request)) {
        filterChain.doFilter(request, response);
    } else {
        doFilterWrapped(wrapRequest(request), wrapResponse(response), filterChain);
    }
    }finally {
        writer.close();
    }
}

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

protected void beforeRequest(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response) throws IOException {
    if (log.isInfoEnabled()) {
        logRequestHeader(request, request.getRemoteAddr() + "|>");
    }
}

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

private void logRequestHeader(ContentCachingRequestWrapper request, String prefix) throws IOException {
    String queryString = request.getQueryString();
    if (queryString == null) {
        printLines(prefix,request.getMethod(),request.getRequestURI());
        log.info("{} {} {}", prefix, request.getMethod(), request.getRequestURI());
    } else {
        printLines(prefix,request.getMethod(),request.getRequestURI(),queryString);
        log.info("{} {} {}?{}", prefix, request.getMethod(), request.getRequestURI(), queryString);
    }
    Collections.list(request.getHeaderNames()).forEach(headerName ->
    Collections.list(request.getHeaders(headerName)).forEach(headerValue ->
    log.info("{} {}: {}", prefix, headerName, headerValue)));
    printLines(prefix);
    printLines(RequestContextHolder.currentRequestAttributes().getSessionId());
    log.info("{}", prefix);

    log.info(" Session ID: ", RequestContextHolder.currentRequestAttributes().getSessionId());
}

private void printLines(String ...args) throws IOException {

    try {
    for(String varArgs:args) {
            writer.write(varArgs);
            writer.newLine();
    }
        }catch(IOException ex){
            ex.printStackTrace();
    }

}

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

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

private 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 -> {
                try {
                    printLines(line);
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            });
//              log.info("{} {}", prefix, line));
        } catch (UnsupportedEncodingException e) {
            log.info("{} [{} bytes content]", prefix, content.length);
        }
    } else {

        log.info("{} [{} bytes content]", prefix, content.length);
    }
}

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

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

Вывод в файл:

127.0.0.1|>
POST
/createUser
127.0.0.1|>
session Id:C0793464532E7F0C7154913CBA018B2B
Request:
{
  "name": "asdasdas",
  "birthDate": "2018-06-21T17:11:15.679+0000"
}
127.0.0.1|<
200
OK
127.0.0.1|<
Response:
{"name":"asdasdas","birthDate":"2018-06-21T17:11:15.679+0000","id":4}
Рамеш
источник
1
Отличный ответ, единственным предложением было бы собрать все выходные данные в буфер и войти в один оператор.
Майк
2

Если вы видите только часть полезной нагрузки вашего запроса, вам нужно вызвать setMaxPayloadLengthфункцию, поскольку по умолчанию в теле запроса отображается только 50 символов. Кроме того, установка setIncludeHeadersв false является хорошей идеей, если вы не хотите регистрировать свои заголовки аутентификации!

@Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
    CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter();
    loggingFilter.setIncludeClientInfo(false);
    loggingFilter.setIncludeQueryString(false);
    loggingFilter.setIncludePayload(true);
    loggingFilter.setIncludeHeaders(false);
    loggingFilter.setMaxPayloadLength(500);
    return loggingFilter;
}
Крис Пайка
источник
Я пытаюсь использовать его весной mvc, и он не работает для меня. Требуются какие-либо дополнительные настройки, кроме регистрации этого компонента и добавления регистратора?
Номан Ахтар
1

Если вы используете Tomcat в своем загрузочном приложении, вы org.apache.catalina.filters.RequestDumperFilterнайдете путь к классу. (но это не даст вам «исключений в одном месте»).

Yura
источник
1

код, вставленный ниже, работает с моими тестами и может быть загружен из моего [github project] [1], предоставляя общий доступ после применения решения, основанного на этом, в производственном проекте.

@Configuration
public class LoggingFilter extends GenericFilterBean {

    /**
     * It's important that you actually register your filter this way rather then just annotating it
     * as @Component as you need to be able to set for which "DispatcherType"s to enable the filter
     * (see point *1*)
     * 
     * @return
     */
    @Bean
    public FilterRegistrationBean<LoggingFilter> initFilter() {
        FilterRegistrationBean<LoggingFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new LoggingFilter());

        // *1* make sure you sett all dispatcher types if you want the filter to log upon
        registrationBean.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));

        // *2* this should put your filter above any other filter
        registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);

        return registrationBean;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        ContentCachingRequestWrapper wreq = 
            new ContentCachingRequestWrapper(
                (HttpServletRequest) request);

        ContentCachingResponseWrapper wres = 
            new ContentCachingResponseWrapper(
                (HttpServletResponse) response);

        try {

            // let it be ...
            chain.doFilter(wreq, wres);

            // makes sure that the input is read (e.g. in 404 it may not be)
            while (wreq.getInputStream().read() >= 0);

            System.out.printf("=== REQUEST%n%s%n=== end request%n",
                    new String(wreq.getContentAsByteArray()));

            // Do whatever logging you wish here, in this case I'm writing request 
            // and response to system out which is probably not what you wish to do
            System.out.printf("=== RESPONSE%n%s%n=== end response%n",
                    new String(wres.getContentAsByteArray()));

            // this is specific of the "ContentCachingResponseWrapper" we are relying on, 
            // make sure you call it after you read the content from the response
            wres.copyBodyToResponse();

            // One more point, in case of redirect this will be called twice! beware to handle that
            // somewhat

        } catch (Throwable t) {
            // Do whatever logging you whish here, too
            // here you should also be logging the error!!!
            throw t;
        }

    }
}
Андреа Саба
источник
0

Чтобы регистрировать все запросы с входными параметрами и телом, мы можем использовать фильтры и перехватчики . Но при использовании фильтра или перехватчика мы не можем напечатать тело запроса несколько раз. Лучше всего использовать Spring-AOP. Используя это, мы можем отделить механизм регистрации от приложения. АОП может использоваться для регистрации входных и выходных данных каждого метода в приложении.

Мое решение:

 import org.aspectj.lang.ProceedingJoinPoint;
 import org.aspectj.lang.annotation.Around;
 import org.aspectj.lang.annotation.Aspect;
 import org.aspectj.lang.annotation.Pointcut;
 import org.aspectj.lang.reflect.CodeSignature;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Component;
 import com.fasterxml.jackson.databind.ObjectMapper;
 @Aspect
 @Component
public class LoggingAdvice {
private static final Logger logger = 
LoggerFactory.getLogger(LoggingAdvice.class);

//here we can provide any methodName, packageName, className 
@Pointcut(value = "execution(* com.package.name.*.*.*(..) )")
public void myPointcut() {

}

@Around("myPointcut()")
public Object applicationLogger(ProceedingJoinPoint pjt) throws Throwable {
    ObjectMapper mapper = new ObjectMapper();
    String methodName = pjt.getSignature().getName();
    String className = pjt.getTarget().getClass().toString();
    String inputParams = this.getInputArgs(pjt ,mapper);
    logger.info("method invoked from " + className + " : " + methodName + "--Request Payload::::"+inputParams);
    Object object = pjt.proceed();
    try {
        logger.info("Response Object---" + mapper.writeValueAsString(object));
    } catch (Exception e) {
    }
    return object;
}

private String getInputArgs(ProceedingJoinPoint pjt,ObjectMapper mapper) {
    Object[] array = pjt.getArgs();
    CodeSignature signature = (CodeSignature) pjt.getSignature();

    StringBuilder sb = new StringBuilder();
    sb.append("{");
    int i = 0;
    String[] parameterNames = signature.getParameterNames();
    int maxArgs = parameterNames.length;
    for (String name : signature.getParameterNames()) {
        sb.append("[").append(name).append(":");
        try {
            sb.append(mapper.writeValueAsString(array[i])).append("]");
            if(i != maxArgs -1 ) {
                sb.append(",");
            }
        } catch (Exception e) {
            sb.append("],");
        }
        i++;
    }
    return sb.append("}").toString();
}

}

Рушикеш Кулкарни
источник
0

Если у вас настроен Spring boot Config server, то просто включите Debug logger для класса:

Http11InputBuffer.Http11InputBuffer.java

Отладки будут регистрировать все запросы и ответы для каждого запроса

нарендра
источник
-1

Для регистрации запросов, которые приводят только к 400:

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.io.FileUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.AbstractRequestLoggingFilter;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.WebUtils;

/**
 * Implementation is partially copied from {@link AbstractRequestLoggingFilter} and modified to output request information only if request resulted in 400.
 * Unfortunately {@link AbstractRequestLoggingFilter} is not smart enough to expose {@link HttpServletResponse} value in afterRequest() method.
 */
@Component
public class RequestLoggingFilter extends OncePerRequestFilter {

    public static final String DEFAULT_AFTER_MESSAGE_PREFIX = "After request [";

    public static final String DEFAULT_AFTER_MESSAGE_SUFFIX = "]";

    private final boolean includeQueryString = true;
    private final boolean includeClientInfo = true;
    private final boolean includeHeaders = true;
    private final boolean includePayload = true;

    private final int maxPayloadLength = (int) (2 * FileUtils.ONE_MB);

    private final String afterMessagePrefix = DEFAULT_AFTER_MESSAGE_PREFIX;

    private final String afterMessageSuffix = DEFAULT_AFTER_MESSAGE_SUFFIX;

    /**
     * The default value is "false" so that the filter may log a "before" message
     * at the start of request processing and an "after" message at the end from
     * when the last asynchronously dispatched thread is exiting.
     */
    @Override
    protected boolean shouldNotFilterAsyncDispatch() {
        return false;
    }

    @Override
    protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain)
            throws ServletException, IOException {

        final boolean isFirstRequest = !isAsyncDispatch(request);
        HttpServletRequest requestToUse = request;

        if (includePayload && isFirstRequest && !(request instanceof ContentCachingRequestWrapper)) {
            requestToUse = new ContentCachingRequestWrapper(request, maxPayloadLength);
        }

        final boolean shouldLog = shouldLog(requestToUse);

        try {
            filterChain.doFilter(requestToUse, response);
        } finally {
            if (shouldLog && !isAsyncStarted(requestToUse)) {
                afterRequest(requestToUse, response, getAfterMessage(requestToUse));
            }
        }
    }

    private String getAfterMessage(final HttpServletRequest request) {
        return createMessage(request, this.afterMessagePrefix, this.afterMessageSuffix);
    }

    private String createMessage(final HttpServletRequest request, final String prefix, final String suffix) {
        final StringBuilder msg = new StringBuilder();
        msg.append(prefix);
        msg.append("uri=").append(request.getRequestURI());

        if (includeQueryString) {
            final String queryString = request.getQueryString();
            if (queryString != null) {
                msg.append('?').append(queryString);
            }
        }

        if (includeClientInfo) {
            final String client = request.getRemoteAddr();
            if (StringUtils.hasLength(client)) {
                msg.append(";client=").append(client);
            }
            final HttpSession session = request.getSession(false);
            if (session != null) {
                msg.append(";session=").append(session.getId());
            }
            final String user = request.getRemoteUser();
            if (user != null) {
                msg.append(";user=").append(user);
            }
        }

        if (includeHeaders) {
            msg.append(";headers=").append(new ServletServerHttpRequest(request).getHeaders());
        }

        if (includeHeaders) {
            final ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
            if (wrapper != null) {
                final byte[] buf = wrapper.getContentAsByteArray();
                if (buf.length > 0) {
                    final int length = Math.min(buf.length, maxPayloadLength);
                    String payload;
                    try {
                        payload = new String(buf, 0, length, wrapper.getCharacterEncoding());
                    } catch (final UnsupportedEncodingException ex) {
                        payload = "[unknown]";
                    }
                    msg.append(";payload=").append(payload);
                }
            }
        }
        msg.append(suffix);
        return msg.toString();
    }

    private boolean shouldLog(final HttpServletRequest request) {
        return true;
    }

    private void afterRequest(final HttpServletRequest request, final HttpServletResponse response, final String message) {
        if (response.getStatus() == HttpStatus.BAD_REQUEST.value()) {
            logger.warn(message);
        }
    }

}
Игорь М.
источник