Spring Boot - Як реєструвати всі запити та відповіді за винятком в одному місці?


216

Я працюю на відпочинку 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"), хоча. Це не відомо в фільтрі сервлетів.
zapl

1
все-таки, навіть якщо ви додасте фільтр або будь-яке рішення на рівні програми, ви не будете реєструвати весь запит, якщо помилка сервера HTTP 500 не буде зареєстрована, оскільки в той момент, коли необроблений виняток буде кинутий на рівень програми, після проковтування винятку буде відображено типову вбудовану програму tomcat на сторінці помилок, і, звичайно, не буде збережено журнал. Крім того, якщо ви перевірите відповідь user1817243, у разі будь-якого винятку він знову не записуватиме запит, але він зареєструє виняток (!!).
AntJavaDev

Чи повинен цей формат журналу відповідати кожному написаному вами символу? Схоже, переклад JSON був би оптимальним у вашому випадку: LogClass{ getRequestAndSaveIt()} Gson.toJson(LogClass)як псевдокод
Вале

1
Майбутні читачі можуть скористатись моєю відповіддю (URL-адреса слід у цьому коментарі). В основному, я зміг відверто викрадати різні пости з цього питання. ПРОСМОЖАЙТЕ розглянути відповідь виконавця (у відповідях нижче), перш ніж спробувати його вручну. Але відповідь, яку я публікую, дозволяє реєструвати "400, 404, 500" (будь-які / всі), але встановлюючи пріоритет замовлення на найнижчий пріоритет (або в межах "8", якщо ви подивитесь на код). stackoverflow.com/questions/10210645 / ...
granadaCoder

Я дотримувався весняних документів з реєстрації звідси: docs.spring.io/spring-boot/docs/current/reference/html/…
T04435

Відповіді:


148

Не пишіть жодних перехоплювачів, фільтрів, компонентів, аспектів тощо. Це дуже поширена проблема і вона вже багато разів вирішується.

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

Щоб отримати потрібні кінцеві точки, вам знадобиться залежність ведучого завантажувача-пускача-пускача, а також "додати до списку" кінцеві точки, які ви шукаєте, і, можливо, встановити або відключити безпеку для цього.

Також, де ця програма буде працювати? Чи будете ви використовувати PaaS? Хостинг - провайдерів, Heroku, наприклад, надати запит реєстрацію в якості частини їх служби та вам не потрібно робити будь- кодування взагалі тоді.


4
ще деталі? Я знайшов github.com/spring-projects/spring-boot/tree/master/… , але не дуже багато цього.
Том Говард

16
Це не можна використовувати для налагодження: неаутентифіковані запити (наприклад, із пружинною безпекою) не реєструються.
bekce

11
Насправді в Актуаторі немає конкретних компонентів для ввімкнення http-журналу. / слід - показати лише останні N запитів.
Володимир Філіпченко

18
@ike_love, як конфігурувати виконавчий механізм таким чином, щоб він запитував реєстрацію запиту (також POST body)?

11
Trace не записуватиме запит та орган відповіді для вас .... все інше (заголовок тощо), крім них.
Lekkie

94

Весна вже пропонує фільтр, який виконує цю роботу. Додайте в конфігурацію наступний квасоля

@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
Зауважте, що він не реєструє відповіді, а лише запити.
Вім Деблауве

1
Є лише запити. Як реєструвати органи відповідей за допомогою CommonsRequestLoggingFilter?
user2602807

3
Крім того, це не реєструє Виняток
BhendiGawaar

Що ж, це очікується, оскільки це фільтр реєстрації запитів. Більше про це тут: 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виклик, щоб кешувати оригінальний обробник.


2
що відбувається, коли відповідь є потоком і потік не підтримує пошук? Чи буде вищезазначене все-таки спрацювати?
Том Говард

Мене не хвилює метод, який викликається, лише отримані та надіслані дані. Фільтр, начебто, спрямовує мене в потрібному напрямку, і відповідь @ ike_love скеровує мене до github.com/spring-projects/spring-boot/blob/master/…
Том Говард

@TomHoward AFAIK, весною немає "вікна реєстрації відповідей". Тому ви можете розширити WebRequestTraceFilter або AbstractRequestLoggingFilter, додавши логіку реєстрації відповідей.
Хан

Працює просто чудово!
Павло Власов

@hahn, чому ти використовував для цього серветку Dispatcher? чи не може бути додано такий самий логін із фільтром у doFilter?
BhendiGawaar

39

Logbook бібліотека спеціально для лісозаготівельних 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"
}

Однак воно не виводить ім'я класу, яке обробляє запит. Бібліотека має деякі інтерфейси для запису користувальницьких реєстраторів.


4
додано як залежність до мінімальної весняної завантажувальної програми та намагався запустити - жодних змін, ані реєстраційного виходу в моєму додатку. Я думаю, є якісь додаткові залежності чи класи, для яких це потрібно? Зареєструватися як фільтр, здається, теж нічого не робить.
eis

1
@eis Вам потрібно зареєструвати його як фільтр, як пояснено в документах тут. github.com/zalando/logbook
Сінгал

2
Довідник журналу Logbook говорить: "Бортжурнал поставляється зі зручною автоматичною конфігурацією для користувачів Spring Boot. Він встановлює всі наступні частини автоматично з розумними за замовчуванням". Але це не працює.
Леос Літерак

5
@LeosLiterak Я вважаю, що вам потрібно додати logging.level.org.zalando.logbook=TRACE до свого application.properties(як зазначено в Readme)
TolkienWASP


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/>

Створіть примітку, яку можна використовувати як точку

@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 запускається, коли виконання відповідного методу закінчується, викидаючи виняток.

Якщо ви хочете прочитати детально, прочитайте це. http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html


1
Це записує виклик методу, а не те, що було фактично отримано та надіслано на рівні HTTP.
Том Говард

1
Як написати запит BODY? У моєму випадку це ПОСТ-ТІЛЬ. on request.getReader або getInputStream Я отримую помилку, що потік закритий.

13

Після додавання виконавчих пристроїв у додаток на базі весняного завантаження у вас є /traceдоступна кінцева точка з останньою інформацією про запити. Ця кінцева точка працює на основі TraceRepository, а реалізація за замовчуванням - InMemoryTraceRepository, яка зберігає останні 100 викликів. Ви можете змінити це, застосувавши цей інтерфейс самостійно та зробивши його доступним у вигляді весняних бобів. Наприклад, для реєстрації всіх запитів для входу в систему (і все ще використовую реалізацію за замовчуванням як основну сховище для подання інформації про /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 Я додав приклад для вас
Piotr Chowaniec

1
Я робив щось подібне, але проблема полягає в тому, що орган реагування недоступний TraceRepository, як ми можемо отримати доступ до цього?
Амір Пашазаде

@AmirPashazadeh вам доведеться переосмислити, protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)але я не впевнений, коли цей фільтр виконаний - може бути на етапі запиту, тому орган відповіді там не буде готовий.
Piotr Chowaniec

1
@Kekar ​​З 2.0 є HttpTraceRepository (замість TraceRepository)
Piotr Chowaniec

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();
            }

        }
    }

Це добре працює для ведення журналу відповідей - хоча мені довелося поставити обмеження на кількість байтів, які він реєструє, інакше воно втрачає вихід консолі журналу Intellij.
Адам

Рядок getContent () {if (bos == null) {return String.format ("закликається% s занадто рано", BufferedResponseWrapper.class.getCanonicalName ()); } байт [] байт = bos.toByteArray (); повернути новий рядок (Arrays.copyOf (байти, 5000)) + "...."; }
Адам

Також варто включити перемикач "log.isTraceEnabled ()" навколо журналу.
Адам

6
Що було б здорово, якби Java додала до HttpServletResponse деякі методи за замовчуванням, тому нам не потрібно писати таку величезну реалізацію.
Адам

1
плюс один для включення імпортних заяв
granadaCoder

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 / actuator / httptrace


7

Наразі Spring Boot має функцію Актуатор для отримання журналів запитів та відповідей.

Але ви також можете отримати журнали за допомогою 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журнал і керуйте ним

Більше інформації про AOP відвідайте тут:

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

Прикладна стаття про АОП


1
new ObjectMapper()дорого, краще поділіться одним картографом для всіх
Сем

Так, звісно. Це демо-код. У виробництві ми повинні слідувати передовій практиці.
Пані Саєдул Карим

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, реалізація відповіді буде a 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";
        }
    }

}

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);
    }
}

Ваша програма упакована як війна чи баночка? Я продовжую отримувати помилку java.io.FileNotFoundException: Не вдалося відкрити ресурс ServletContext [/WEB-INF/loggingDispatcherServlet-servlet.xml]
Mayank Madhav

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()));
    }
}

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шлях до класу для вас. (але це не забезпечить вас "винятками в одному місці").


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, тоді просто увімкніть реєстратор налагодження для класу:

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);
        }
    }

}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.