Spring Boot - Jak zalogować wszystkie żądania i odpowiedzi z wyjątkami w jednym miejscu?

216

Pracuję nad spoczynkowym API z wiosennym bagażnikiem. Muszę rejestrować wszystkie żądania za pomocą parametrów wejściowych (metodami, np. GET, POST itp.), Ścieżką żądania, ciągiem zapytania, odpowiednią metodą klasy tego żądania, a także odpowiedzią na to działanie, zarówno sukcesem, jak i błędami.

Dla przykładu:

udane żądanie:

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

Dziennik powinien wyglądać mniej więcej tak:

{
   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: "[email protected]"   
      }
   },
   exceptions: []       
}

Lub poproś o błąd:

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

Dziennik powinien być mniej więcej taki:

    {
       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: ...................    
       ]       
    }

Chcę, aby żądanie / odpowiedź było pojedynczą jednostką z niestandardowymi informacjami związanymi z tą jednostką, zarówno w przypadkach udanych, jak i błędów.

Jaka jest najlepsza praktyka wiosną, aby to osiągnąć, może być z filtrami? jeśli tak, czy możesz podać konkretny przykład?

(Grałem z @ControllerAdvice i @ExceptionHandler, ale jak już wspomniałem, muszę obsłużyć wszystkie żądania dotyczące sukcesu i błędów w jednym miejscu (i jednym dzienniku)).

Teimuraz
źródło
Prawdopodobnie poprzez ServletFilter logowania (np stackoverflow.com/a/2171633/995891 ), alternatywnie HandlerInterceptor, ale to nie może pracować również z zalogowaniu odpowiedź jak wspomniano w odpowiedzi: concretepage.com/spring/spring-mvc/... - HandlerInterceptor ma dostęp do metody (metoda: „UsersController.getUser”). Nie jest to znane w filtrze serwletu.
zapl
1
mimo to, nawet jeśli dodasz filtr lub inne rozwiązanie w warstwie aplikacji, nie zarejestrujesz całego żądania, ponieważ błąd serwera HTTP 500 nie zostanie zarejestrowany, ponieważ w momencie zgłoszenia nieobsługiwanego wyjątku zostanie zgłoszony w warstwie aplikacji, domyślna strona osadzona na stronie błędu zostanie wyświetlona po połknięciu wyjątku i oczywiście nie zachowa dziennika. Również jeśli zaznaczysz odpowiedź user1817243, w przypadku jakiegokolwiek wyjątku ponownie nie zarejestruje żądania, ale zarejestruje wyjątek (!!).
AntJavaDev,
Czy ten format dziennika musi być spójny z każdym pisanym znakiem? Wydaje się, że tłumaczenie JSON byłoby optymalne w twoim przypadku: LogClass{ getRequestAndSaveIt()} Gson.toJson(LogClass)jako pseudokod
Vale
1
Przyszli czytelnicy mogą skorzystać z mojej odpowiedzi (adres URL do śledzenia w tym komentarzu). Zasadniczo mogłem franken-stein zebrać różne posty dotyczące tego pytania. PROSZĘ rozważyć odpowiedź siłownika (w odpowiedziach poniżej) przed wypróbowaniem jej ręcznie. Ale odpowiedź, którą publikuję, pozwala na zalogowanie „400, 404, 500” (dowolne / wszystkie), ale ustawienie priorytetu zamówienia na najniższy priorytet (lub w granicach „8”, jeśli spojrzysz na kod). stackoverflow.com/questions/10210645/…
granadaCoder

Odpowiedzi:

148

Nie pisz żadnych elementów przechwytujących, filtrów, komponentów, aspektów itp., Jest to bardzo częsty problem, który został rozwiązany wiele razy.

Spring Boot ma moduły o nazwie Actuator , które umożliwiają wylogowanie żądania HTTP po wyjęciu z pudełka. Istnieje punkt końcowy odwzorowany na /trace(SB1.x) lub/actuator/httptrace (SB2.0 +), który pokaże Ci ostatnie 100 żądań HTTP. Możesz go dostosować do rejestrowania każdego żądania lub zapisywać w bazie danych.

Aby uzyskać pożądane punkty końcowe, potrzebujesz zależności rozruchu rozruchowego i elementu wykonawczego, a także „białej listy” punktów końcowych, których szukasz, i ewentualnie skonfiguruj lub wyłącz zabezpieczenia.

Ponadto, gdzie będzie działać ta aplikacja? Czy będziesz korzystać z PaaS? Dostawcy usług hostingowych, na przykład Heroku, zapewniają rejestrowanie żądań w ramach swoich usług i nie trzeba wtedy żadnego kodowania.

SergeyB
źródło
4
jakieś szczegóły? Znalazłem github.com/spring-projects/spring-boot/tree/master/… , ale niewiele więcej poza tym.
Tom Howard
16
Nie można tego użyć do debugowania: nieuwierzytelnione żądania (na przykład z zabezpieczeniami wiosennymi) nie są rejestrowane.
bekce
11
W rzeczywistości Actuator nie ma żadnych konkretnych składników do rejestrowania HTTP. / trace - pokaż tylko ostatnie N żądań.
Vladimir Filipchenko
18
@ike_love, jak skonfigurować siłownik w taki sposób, aby rejestrował żądanie (także ciało POST) do pliku?
11
Śledzenie nie będzie rejestrować treści żądania i odpowiedzi dla Ciebie .... wszystko inne (nagłówek itp.) Oprócz nich.
Lekkie
94

Wiosna już zapewnia filtr, który wykonuje tę pracę. Dodaj następującą fasolę do swojej konfiguracji

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

Nie zapomnij zmienić poziomu dziennika org.springframework.web.filter.CommonsRequestLoggingFilterna DEBUG.

Yogesh Badke
źródło
75
Pamiętaj, że nie rejestruje odpowiedzi, tylko żądania.
Wim Deblauwe
1
Są tylko prośby. Jak rejestrować treści odpowiedzi za pomocą CommonsRequestLoggingFilter?
user2602807
3
Nie rejestruje to również wyjątku
BhendiGawaar
Jest to oczekiwane, ponieważ jest to filtr rejestrujący żądania. Więcej na ten temat tutaj: docs.spring.io/spring/docs/current/javadoc-api/org/…
Yogesh Badke 8'18
4
Jeśli masz dużą treść JSON, ustaw długość ładunku na dużą liczbę, aby zarejestrować całą treść żądania. loggingFilter.setMaxPayloadLength (100000);
Venkatesh Nannan
58

Możesz użyć javax.servlet.Filter jeśli nie ma wymogu rejestrowania metody Java, która została wykonana.

Ale z tego wymogu masz dostępu do informacji przechowywanych w handlerMappingz DispatcherServlet. To powiedziawszy, możesz zastąpićDispatcherServlet aby wykonać rejestrowanie pary żądanie / odpowiedź.

Poniżej znajduje się przykład pomysłu, który można ulepszyć i dostosować do własnych potrzeb.

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 - zawiera informacje o module obsługi zapytań.

Następnie możesz zarejestrować tego dyspozytora w następujący sposób:

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

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

A oto próbka dzienników:

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"}'}

AKTUALIZACJA

W przypadku błędów Spring wykonuje automatyczną obsługę błędów. Dlatego BasicErrorController#errorjest wyświetlany jako moduł obsługi żądań. Jeśli chcesz zachować oryginalny moduł obsługi żądań, możesz zastąpić to zachowanie spring-webmvc-4.2.5.RELEASE-sources.jar!/org/springframework/web/servlet/DispatcherServlet.java:971wcześniej #processDispatchResultwywołanym, aby buforować oryginalny moduł obsługi.

hahn
źródło
2
co dzieje się, gdy odpowiedź jest strumieniem, a strumień nie obsługuje wyszukiwania? Czy powyższe nadal będzie działać?
Tom Howard
Nie obchodzi mnie wywoływana metoda, tylko dane otrzymane i wysłane. Filtr wydaje się wskazywać mi właściwy kierunek, a odpowiedź @ ike_love skierowała mnie na github.com/spring-projects/spring-boot/blob/master/…
Tom Howard
@TomHoward AFAIK, wiosną nie ma gotowego „rejestrowania odpowiedzi”. Dlatego można rozszerzyć WebRequestTraceFilter lub AbstractRequestLoggingFilter, dodając logikę rejestrowania odpowiedzi.
hahn
Działa dobrze!
Pavel Vlasov,
@ Hahn, dlaczego użyłeś do tego serwletu Dispatcher? czy nie można dodać tego samego loginu z filtrem w doFilter?
BhendiGawaar
39

Logbook biblioteka jest wykonany specjalnie dla żądań HTTP rejestrowania i odpowiedzi. Obsługuje Spring Boot przy użyciu specjalnej biblioteki startowej.

Aby włączyć logowanie Spring Boot, wystarczy dodać bibliotekę do zależności projektu. Na przykład zakładając, że używasz Maven:

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

Domyślnie dane wyjściowe rejestrowania wyglądają następująco:

{
  "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"
}

Nie wyświetla jednak nazwy klasy, która obsługuje żądanie. Biblioteka ma kilka interfejsów do pisania niestandardowych programów rejestrujących.

sihaya
źródło
4
dodano jako zależność do minimalnej wiosennej aplikacji rozruchowej i próbowałem uruchomić - bez zmian, w ogóle nie rejestrowałem danych wyjściowych w mojej aplikacji. Myślę, że są jakieś dodatkowe zależności lub klasy, których potrzebuje? Zarejestrowanie go jako filtru również nic nie robi.
eis
1
@eis Musisz zarejestrować go jako filtr, jak wyjaśniono w dokumentacji tutaj. github.com/zalando/logbook
Pratik Singhal
2
Dokument Logbook mówi: „Logbook jest wyposażony w wygodną automatyczną konfigurację dla użytkowników Spring Boot. Automatycznie konfiguruje wszystkie poniższe części z rozsądnymi ustawieniami domyślnymi”. Ale to nie działa.
Leos Literak,
5
@LeosLiterak Uważam, że musisz dodać logging.level.org.zalando.logbook=TRACE do swojego application.properties(jak stwierdzono w Readme)
TolkienWASP
2
Automatyczna konfiguracja dziennika nie wydaje się działać w wersji wiosennej 2.0.0
Yashveer Rana
26

Zdefiniowałem poziom logowania application.propertiesdo drukowania żądań / odpowiedzi, adres URL metody w pliku dziennika

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

Użyłem Spring Boot.

Ankit
źródło
2
Tak, masz rację - jest to poprawna odpowiedź na żądania logowania do tego samego pliku dziennika ze wszystkimi pozostałymi wynikami. Jednak @moreo poprosił o zalogowanie się GET, POST itp. I do osobnego pliku (jak rozumiem)
Manushin Igor
4
Ten mi się podoba. zero dramatu
Quirino Gervacio
1
Jeśli chcesz, aby nagłówki były dołączane do dziennika, dodaj: „spring.http.log-request-details = true” do pliku application.properties.
jfajunior
20

Oto jak to zrobić w wiosennym spoczynku danych przy użyciu org.springframework.web.util.ContentCachingRequestWrapper i 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

  }


}
Robert
źródło
18

Jeśli nie masz nic przeciwko wypróbowaniu Spring AOP, to jest coś, co badałem do celów logowania i działa całkiem dobrze dla mnie. Nie rejestruje jednak żądań, które nie zostały zdefiniowane, i nieudanych prób żądania.

Dodaj te trzy zależności

spring-aop, aspectjrt, aspectjweaver

Dodaj to do pliku konfiguracyjnego xml <aop:aspectj-autoproxy/>

Utwórz adnotację, która może być używana jako punkt orientacyjny

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

Teraz dodaj adnotacje do wszystkich pozostałych metod API, które chcesz zalogować

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

Przejdźmy do aspektu. skanuj komponent pakiet, w którym znajduje się ta klasa.

@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 advice uruchamia się, gdy wykonanie dopasowanej metody powróci normalnie.

Porada @AfterThrowing jest uruchamiana, gdy kończy się wykonanie dopasowanej metody przez zgłoszenie wyjątku.

Jeśli chcesz przeczytać szczegółowo, przeczytaj to. http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html

Szara gęś
źródło
1
To rejestruje wywołanie metody, a nie to, co faktycznie otrzymano i wysłano na poziomie HTTP.
Tom Howard
1
Jak napisać prośbę BODY? W moim przypadku jest to CIAŁO POCZTOWE. na request.getReader lub getInputStream Pojawia się błąd, że strumień jest zamknięty.
13

Po dodaniu siłowników do aplikacji opartej na rozruchu wiosennym masz /tracepunkt końcowy dostępny z najnowszymi informacjami o żądaniach. Ten punkt końcowy działa w oparciu o TraceRepository, a domyślną implementacją jest InMemoryTraceRepository, która zapisuje 100 ostatnich połączeń. Możesz to zmienić, wdrażając ten interfejs samodzielnie i udostępniając go jako Spring Bean. Na przykład do rejestrowania wszystkich żądań do logowania (i nadal używam domyślnej implementacji jako podstawowej pamięci do udostępniania informacji na /tracepunkcie końcowym) Używam tego rodzaju implementacji:

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

Ta traceInfomapa zawiera podstawowe informacje na temat żądania i odpowiedzi na tego rodzaju postaci: {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}}}. Tutaj nie ma treści odpowiedzi.

EDYTOWAĆ! Rejestrowanie danych POST

Możesz uzyskać dostęp do danych POST, zastępując WebRequestTraceFilter , ale nie sądzę, że to dobry pomysł (np. Cała przesłana zawartość pliku trafi do dzienników) Oto przykładowy kod, ale nie używaj go:

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;
  }
}
Piotr Chowaniec
źródło
1
Co z ciałem POST?
Pavel Vyazankin
@dart Dodałem dla ciebie przykład
Piotr Chowaniec
1
Robiłem coś takiego, ale problem polega na tym, że ciało odpowiedzi nie jest dostępne TraceRepository, w jaki sposób możemy uzyskać do tego dostęp?
Amir Pashazadeh
@AmirPashazadeh musisz przesłonić, protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)ale nie jestem pewien, kiedy ten filtr jest wykonywany - może być na etapie żądania, więc treść odpowiedzi nie będzie tam gotowa.
Piotr Chowaniec,
1
@Kekar ​​Od wersji 2.0 istnieje HttpTraceRepository (zamiast TraceRepository)
Piotr Chowaniec
12

Ten kod działa dla mnie w aplikacji Spring Boot - wystarczy zarejestrować go jako filtr

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

        }
    }
użytkownik1817243
źródło
Działa to dobrze w przypadku rejestrowania odpowiedzi - chociaż musiałem ograniczyć liczbę rejestrowanych bajtów, w przeciwnym razie psuje wyjście konsoli rejestrowania Intellij.
Adam
String getContent () {if (bos == null) {return String.format („wywołano% s za wcześnie”, BufferedResponseWrapper.class.getCanonicalName ()); } byte [] bytes = bos.toByteArray (); zwraca nowy ciąg (Arrays.copyOf (bytes, 5000)) + "...."; }
Adam,
Warto również umieścić przełącznik „log.isTraceEnabled ()” wokół rejestrowania.
Adam,
6
Byłoby fajnie, gdyby Java dodała pewne domyślne metody do HttpServletResponse, więc nie musimy pisać tak ogromnej implementacji.
Adam,
1
plus jeden za dołączenie wyciągów z importu
dołączenie
7

Oto moje rozwiązanie (Spring 2.0.x)

Dodaj zależność od maven:

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

Zmodyfikuj plik application.properties i dodaj następujący wiersz:

management.endpoints.web.exposure.include=* 

Po uruchomieniu aplikacji do rozruchu wiosennego możesz śledzić ostatnie 100 żądań http, dzwoniąc pod ten adres URL: http: // localhost: 8070 / aktuator / httptrace

Enrico Giurin
źródło
7

Obecnie Spring Boot ma funkcję aktywatora, aby uzyskać dzienniki żądań i odpowiedzi.

Ale można również uzyskać dzienniki za pomocą Aspect (AOP).

Aspekt oferuje adnotacji, takich jak: @Before, @AfterReturning, @AfterThrowingitd.

@Beforerejestruje żądanie, @AfterReturningrejestruje odpowiedź i @AfterThrowingloguje komunikat o błędzie. Być może nie potrzebujesz dziennika wszystkich punktów końcowych, więc możesz zastosować niektóre filtry w pakietach.

Oto kilka przykładów :

Na prośbę:

@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) {
            }
        }
    }

Oto @Before("within(your.package.where.endpoints.are..*)")ścieżka pakietu. Wszystkie punkty końcowe w tym pakiecie wygenerują dziennik.

Dla odpowiedzi:

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

Oto @AfterReturning("within(your.package.where.endpoints.are..*)")ścieżka pakietu. Wszystkie punkty końcowe w tym pakiecie wygenerują dziennik. Również Object returnValuezawiera odpowiedź.

Wyjątek:

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

Oto @AfterThrowing(pointcut = ("within(your.package.where.endpoints.are..*)"), throwing = "e") ścieżka pakietu. Wszystkie punkty końcowe w tym pakiecie wygenerują dziennik. Exception eZawiera również odpowiedź na błąd.

Oto pełny kod:

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

Tutaj za pomocą @ConditionalOnExpression("${endpoint.aspect.enabled:true}")możesz włączyć / wyłączyć dziennik. po prostu dodaj endpoint.aspect.enabled:truedo application.propertyi kontroluj dziennik

Więcej informacji o AOP odwiedź tutaj:

Wiosenne doki o AOP

Przykładowy artykuł o AOP

Md. Sajedul Karim
źródło
1
new ObjectMapper()jest drogi, lepiej udostępnij jeden program mapujący dla wszystkich
Sam
Tak, jasne. To jest kod demo. W produkcji musimy przestrzegać najlepszych praktyk.
Md. Sajedul Karim
5

Można również skonfigurować niestandardowy przechwytywacz sprężynowy w HandlerInterceptorAdaptercelu uproszczonej implementacji przechwytywaczy tylko przed / po:

@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
    }
}

Następnie rejestrujesz dowolną liczbę przechwytywaczy:

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    CustomHttpInterceptor customHttpInterceptor;

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

}

Uwaga: tak jak stwierdzono w @Robert , musisz zwrócić uwagę na konkretne implementacje i aplikacje. HttpServletRequestHttpServletResponse

Na przykład w przypadku aplikacji korzystających z ShallowEtagHeaderFilterimplementacji odpowiedzi będzie a ContentCachingResponseWrapper, więc masz:

@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
źródło
4

Odpowiedź @ hahn wymagała nieco modyfikacji, aby działała dla mnie, ale jest to zdecydowanie najbardziej konfigurowalna rzecz, jaką mogłem uzyskać.

Nie działało to dla mnie, prawdopodobnie dlatego, że mam również HandlerInterceptorAdapter [??], ale wciąż otrzymywałem złą odpowiedź z serwera w tej wersji. Oto moja modyfikacja.

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
źródło
czy twoja aplikacja jest zapakowana jako wojna czy słoik? Wciąż pojawia się błąd java.io.FileNotFoundException: Nie można otworzyć zasobu ServletContext [/WEB-INF/loggingDispatcherServlet-servlet.xml]
Mayank Madhav
4

Jeśli ktoś nadal tego potrzebuje, to prosta implementacja z Spring HttpTrace Actuator. Ale jak powiedzieli górnej, nie rejestruje ciał.

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
źródło
4

Rzeczywista odpowiedź znajduje się pod poniższym linkiem https://gist.github.com/int128/e47217bebdb4c402b2ffa7cc199307ba

Wprowadzono pewne zmiany w stosunku do wyżej wymienionego rozwiązania, żądanie i odpowiedź będą logować się w konsoli i pliku, jeśli poziom rejestratora to info. możemy wydrukować w konsoli lub pliku.

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

Dane wyjściowe w pliku:

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}
Ramesh
źródło
1
Świetna odpowiedź, jedyną sugestią byłoby zebranie wszystkich danych wyjściowych do bufora i zalogowanie pojedynczej instrukcji.
Mike
2

Jeśli widzisz tylko część ładunku żądania, musisz wywołać tę setMaxPayloadLengthfunkcję, ponieważ domyślnie wyświetla tylko 50 znaków w treści żądania. Ustawienie wartości setIncludeHeadersfalse jest dobrym pomysłem, jeśli nie chcesz rejestrować nagłówków uwierzytelniania!

@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;
}
Chris Paika
źródło
Próbuję użyć go na wiosnę mvc i to nie działa dla mnie, Wymagane jest dodatkowe ustawienie oprócz rejestracji tej fasoli i dodania rejestratora?
Noman Akhtar
1

jeśli używasz Tomcat w aplikacji rozruchowej, oto jest org.apache.catalina.filters.RequestDumperFilter ścieżka dla ciebie. (ale nie zapewni Ci „wyjątków w jednym miejscu”).

Yura
źródło
1

kod wklejony poniżej działa z moimi testami i można go pobrać z mojego [projektu github] [1], udostępniając po zastosowaniu rozwiązania opartego na tym w projekcie produkcyjnym.

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

    }
}
Andrea Saba
źródło
0

Aby zarejestrować wszystkie żądania z parametrami wejściowymi i treścią, możemy użyć filtrów i przechwytywaczy . Ale podczas korzystania z filtru lub przechwytywacza nie możemy wydrukować treści żądania wiele razy. Lepszym sposobem jest użycie Spring-AOP. Korzystając z tego, możemy odłączyć mechanizm rejestrowania od aplikacji. AOP może być używany do rejestrowania Wejście i wyjście z każdej metody w aplikacji.

Moje rozwiązanie to:

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

}

Rushikesh Kulkarni
źródło
0

Jeśli masz skonfigurowany serwer Spring Boot Config, po prostu włącz rejestrator debugowania dla klasy:

Http11InputBuffer.Http11InputBuffer.java

Debugowanie rejestruje wszystkie żądania i odpowiedzi dla każdego żądania

Narendra
źródło
-1

Aby rejestrować żądania, których wynikiem jest tylko 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);
        }
    }

}
Ihor M.
źródło