Spring Boot - How to log all requests and responses with exceptions in single place?
Asked Answered
C

31

386

I'm working on REST API with spring boot. I need to log all requests with input params (with methods, eg. GET, POST, etc.), request path, query string, corresponding class method of this request, also response of this action, both success and errors. For example:

Successful request:

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

Log should look something like this:

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

Or request with error:

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

Log should be something like this:

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

I want Request/Response to be a single entity, with custom information related to this entity, both in successful and error cases.

What is best practice in spring to achieve this, may be with filters? if yes, can you provide concrete example?

I've played with @ControllerAdvice and @ExceptionHandler, but as I mentioned, I need to handle all success and error requests in single place (and single log).

Cowden answered 16/11, 2015 at 21:21 Comment(6)
Probably via a logging ServletFilter (e.g. https://mcmap.net/q/88066/-logging-response-body-html-from-httpservletresponse-using-spring-mvc-handlerinterceptoradapter ), alternatively HandlerInterceptor but that may not work well with logging the response as mentioned in the answer: concretepage.com/spring/spring-mvc/… - HandlerInterceptor has access to the method (method: "UsersController.getUser") though. That's not known in a servlet filter.Wanda
still , even if you add a filter or whatever solution at application layer , you will not log all the request , f.e. the HTTP 500 Server Error will not get logged , cause at the time that an unhandled exception will get thrown at the Application layer , the default embedded tomcat's on error page will be displayed after swallowing the exception and ofcourse will not preserve the log. Also if you check user1817243 answer , in case of any exception he will again not log the request but he will log the exception (!!).Poulard
Does that log format must be consistent with every character you wrote? Seems like a JSON translation would be optimal in your case: LogClass{ getRequestAndSaveIt()} Gson.toJson(LogClass) as pseudocodeLimousin
Future readers may benefit from my answer (url to follow in this comment). Basically, I was able to franken-stein together different posts about this question. PLEASE consider the actuator answer (in the answers below) before trying it by hand. But the answer I am posting allows "400, 404, 500" (any/all) to be logged, but setting the order-priority to the lowest-priority (or within by "8" if you look at the code). #10211145Mittel
I did follow the spring docs on logging from here: docs.spring.io/spring-boot/docs/current/reference/html/…Occlude
I managed to get this done for Spring boot with the help of logbook github.com/zalando/logbook Read the full article I wrote about this here medium.com/@pramodyahk/… This describes additional configs for masking, excluding and supporting SOAP and JMS.Wiseacre
K
227

Don't write any Interceptors, Filters, Components, Aspects, etc., this is a very common problem and has been solved many times over.

Spring Boot has a modules called Actuator, which provides HTTP request logging out of the box. There's an endpoint mapped to /trace (SB1.x) or /actuator/httptrace (SB2.0+) which will show you last 100 HTTP requests. You can customize it to log each request, or write to a DB.

To get the endpoints you want, you'll need the spring-boot-starter-actuator dependency, and also to "whitelist" the endpoints you're looking for, and possibly setup or disable security for it.

Also, where will this application run? Will you be using a PaaS? Hosting providers, Heroku for example, provide request logging as part of their service and you don't need to do any coding whatsoever then.

Kelda answered 30/8, 2016 at 18:47 Comment(32)
any more details? I found github.com/spring-projects/spring-boot/tree/master/…, but not much in beyond that.Prog
And is this just for the request, or the response as well?Prog
@TomHoward all implementations that spring provides consider only request.Ostensive
This cannot be used for debugging: unauthenticated requests (for example with spring security) do not get logged.Provolone
Actuator format is all JSON which you may or may not appreciate.Cobblestone
Actually Actuator doesn't have any specific components for enebling http logging. /trace - show only last N requests.Harms
@ike_love, how to confgure actuator such that it logging request (also POST body) to file ?Milkwort
@IcedDante, yes, e.g. providing a link to official documentation instead of copying and pasting example that are guaranteed to get out of date.Kelda
And... Where is at least a little example? Just log request and responce to fileIndentation
@Indentation - did you follow the link provided in the answer?Kelda
Trace will not log request and response body for you....everything else (header etc) but those.Lutenist
Does AWS log all requests anywhere?Ephemerality
"Trace will not log request and response body for you....everything else (header etc) but those" Yup. Is there a way to include request and response body? That is what is really needed most of the time.Sensuous
@Lutenist can you confirm that there's no means of enabling this?Carmarthenshire
Logging request/response payloads support has been dropped since spring boot 2.0. Source: github.com/spring-projects/spring-boot/issues/…Diachronic
If you want the body, this is not useful, please mention it.Saldana
this does not provide configuration for writing stuff to file, you cant configure anything here, the docs are eveyrthing but helpful and at some point simply tell you to "implement your own [insert class name here]" in order to have something as basic as a logfile with requests in it. I dont think this answer is in any way helpful for what OP wants.Anteversion
@Blauhirn, OP did not ask to how to write to a file, the question was how to log request/response information and that is that the answer provided. Writing to a file is a bad practice - 12factor.net/logsKelda
This does not log requests to actuator endpoints (as in "log all requests")Diagnostician
PS to my previous comment: Configure org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration to have actuator requests being handled as wellDiagnostician
This is totally different. The question is how to log requests and responses into a file. The actuator is a means to provide this information to external parties on demand, not proactively log it.Foreworn
spring boot actuator is not working without <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>logback-classic</artifactId> </exclusion>Doorn
The links to github are not working right now. I am looking for a way to implement custom trace without exposing actuator endpoints.Prosthodontist
the answer is not clear enough of how to implement itColumbary
Not sure why this answer gets that much upvotes. Actuator doesn't log request body. Things I've tried come from slackspace.de/articles/log-request-body-with-spring-boot. It works.Tabor
@Tabor yes actuator doesn't support request/response body github.com/spring-projects/spring-boot/issues/12953Petra
Seems it is also not recommended anymore as it consume a lot resources github.com/spring-projects/spring-boot/wiki/…Faires
https://mcmap.net/q/88068/-spring-boot-not-finding-httptrace-at-actuator-on-hal-browserAlcaraz
I just wasted half an hour because of this answer since I need to log the request bodies, and Spring actuator doesn't support it.Prudential
@skidoodle3336, sorry you wasted ALL of your development experience on my answer :(Kelda
Just mention that after Spring Boot 2.2.0 M3 Spring Framework 5.2 M2 actuator http trace and auditing are disabled by default since the default repositories implementations are in-memory and may consume too many resources and are not cluster friendly.Halfmast
In spring boot 3.2.2, the endpoint is named: /actuator/httpexchanges. You may enable it by adding this line: management.endpoints.web.exposure.include=health,httpexchanges and then you will need to define a bean like this, otherwise it will not work: ``` @Bean public HttpExchangeRepository getHttpExchangeRepo() { return new InMemoryHttpExchangeRepository(); } ```Mai
A
203

Spring already provides a filter that does this job. Add following bean to your config

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

Don't forget to change log level of org.springframework.web.filter.CommonsRequestLoggingFilter to DEBUG.

Alves answered 1/4, 2017 at 9:32 Comment(13)
Note that it does not log the responses, only the requests.Methodius
There is only requests. How to log responses bodies using CommonsRequestLoggingFilter ?Indentation
Also this does not log ExceptionHaberdasher
Well, that is expected as it's a request logging filter. More on this here: docs.spring.io/spring/docs/current/javadoc-api/org/…Alves
Sorry newbie question but...add it to our config? in springboot for instance?Ephemerality
But it only logs payload in after request message not in before request message.Is there a way to log payload in before request??Afterimage
If you have large JSON body, set the payload length to a big number to log the whole request body. loggingFilter.setMaxPayloadLength(100000);Imena
Do you know how to log the JSON body on a single line (ie remove pretty-print)?Thermostatics
Why does it log the request twice, i.e. before and after the request being processed. and the only difference is the after request log contains payload.Casie
make sure you have enable debug = true in application.propertiesHaya
how to measure latency ?Abelmosk
@Ephemerality Add logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG to the properties. Then look for "before request" and "after request" debug messages.Warp
It doesn't work for me. I have set log level to debug for that package.Saccharin
O
105

You could use javax.servlet.Filter if there wasn't a requirement to log java method that been executed.

But with this requirement you have to access information stored in handlerMapping of DispatcherServlet. That said, you can override DispatcherServlet to accomplish logging of request/response pair.

Below is an example of idea that can be further enhanced and adopted to your needs.

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 - contains the information about request handler.

You then can register this dispatcher as following:

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

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

And here's the sample of logs:

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

UPDATE

In case of errors Spring does automatic error handling. Therefore, BasicErrorController#error is shown as request handler. If you want to preserve original request handler, then you can override this behavior at spring-webmvc-4.2.5.RELEASE-sources.jar!/org/springframework/web/servlet/DispatcherServlet.java:971 before #processDispatchResult is called, to cache original handler.

Ostensive answered 29/8, 2016 at 13:23 Comment(10)
what happens when the response is a stream and the stream doesn't support seek? Will the above still work?Prog
I don't care about the method invoked, just the data received and sent. Filter appears to point me in the right direction and @ike_love's response has directed me to github.com/spring-projects/spring-boot/blob/master/…Prog
@TomHoward AFAIK, there's no out of the box "response logging" in spring. Therefore you can extend WebRequestTraceFilter or AbstractRequestLoggingFilter adding response logging logic.Ostensive
Works just fine!Salicylate
@Ostensive why did you use Dispatcher servlet for this? can same login not be added with filter in doFilter?Haberdasher
@Ostensive To make this solution work, is it necessary to change the packaging to war? Coz when i tried this in my spring boot rest api with jar packaging (embedded tomcat) , i keep getting the error java.io.FileNotFoundException: Could not open ServletContext resource [/WEB-INF/loggingDispatcherServlet-servlet.xml]Insolvency
@Ostensive dispatcher servlet is throwing Bad request for RequestPart params. Is there any way to solve it?Eclecticism
This custom class does not work when the request is of type "multipart/form-data" :(Orometer
ContentCachingResponseWrapper breaks SockJS fallback options(xhr_streaming, xhr_polling). Just adding this here in the hopes that someone googling it may find it.Dab
The doDispatch method gets executed twice when controller throws any exception in Spring Boot 3.2.0. First time there is no exception message in response payload, the second time there is no payload in request. Help?Cottontail
M
98

The Logbook library is specifically made for logging HTTP requests and responses. It supports Spring Boot using a special starter library.

To enable logging in Spring Boot all you need to do is adding the library to your project's dependencies. For example assuming you are using Maven:

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

By default the logging output looks like this:

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

It does however not output the class name that is handling the request. The library does have some interfaces for writing custom loggers.

Notes

In the meantime the library has significantly evolved, current version is 2.4.1, see https://github.com/zalando/logbook/releases. E.g. the default ouput format has changed, and can be configured, filtered, etc.

Do NOT forget to set the log level to TRACE, else you won't see anything:

logging:
  level:
    org.zalando.logbook: TRACE
Momently answered 24/12, 2017 at 18:17 Comment(13)
added as a dependency to a minimal spring boot app and tried to run - no change, no logging output at all in my app. I think there are some additional dependencies or classes this needs? Registering it as a filter doesn't seem to do anything either.Retributive
@Retributive You need to register it as a filter as explained in the docs here. github.com/zalando/logbookConsumerism
I know, that's why I said "registering it as a filter doesn't seem to do anything either"Retributive
Logbook doc says: "Logbook comes with a convenient auto configuration for Spring Boot users. It sets up all of the following parts automatically with sensible defaults." But it does not work.Curhan
@LeosLiterak I believe you need to add logging.level.org.zalando.logbook=TRACE to your application.properties (as is stated in the Readme)Carmarthenshire
Logbook autoconfiguration doesn't seem to work for spring-boot v2.0.5Edieedification
Successfully used Logbook with Spring Boot 2.1.8.Ancipital
Hi @Carmarthenshire thanks for your comment, it worked with Spring Boot 2.3.0.RELEASE, regards.Victuals
The log level is the key! Thanks @CarmarthenshireKiloton
Logbook doesn't support DispatcherServlet, meaning that Spring Boot error response logs (HTTP 400 or HTTP 500) will not have body.Pogue
After adding the logbook dependency, getting following error >> Caused by: java.lang.ClassNotFoundException: org.springframework.boot.autoconfigure.security.SecurityFilterAutoConfiguration at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:602)Bogus
It worked for me with Spring boot 2.7.2, but I think that a common cause of errors is adding the core library ('org.zalando:logbook-core:2.15.0') instead of the spring-boot library ('org.zalando:logbook-spring-boot-starter:2.15.0') to the project. Also I would prefer the HTTP format, expected by default, (more readable on the console), to JSON that apparently is the new default. Anyway, it is a great and handy tool.Yurikoyursa
Success here! I just added teh dependency and set the log level.Alimony
V
66

I had defined logging level in application.properties to print requests/responses, method url in the log file

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

I had used Spring Boot.

Vorticella answered 11/7, 2017 at 9:59 Comment(2)
Yes, you are right - this is valid answer for get requests logging to the same log file with all other results. However, @moreo asked to log GET, POST, etc. and to the separate file (as I understand)Figueroa
If you want the headers to be included in the log, then you should add: "spring.http.log-request-details=true" to you application.properties file.Gelatinous
M
37

Here is how I do it in spring data rest by using org.springframework.web.util.ContentCachingRequestWrapper and 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/
 * https://mcmap.net/q/88069/-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

  }


}
Margaux answered 3/2, 2017 at 11:37 Comment(3)
This did the trick for logging the request body for me. Important note though, that you should only log the body after the request has been processed - not before!Marsiella
Sadly it's actually more complex: The Body is a stream. It can only be read ONCE! So if the body is read by spring, you cannot read it again to log its content. The only way you have is to duplicate that stream, log one of them and write the other one back into the request itself.. This is possible. But very complex.Margaux
"can only log the request's body AFTER the request has been made and ContentCachingRequestWrapper did its work." How overcome this restriction?Caravel
I
34

This code works for me in a Spring Boot application - just register it as a filter

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

        }
    }
Interatomic answered 25/8, 2016 at 6:8 Comment(8)
This works well for response logging - although I had to put a limit on the number of bytes it logs otherwise it trashes the Intellij logging console output.Cobblestone
String getContent() { if (bos == null) { return String.format("called %s too early", BufferedResponseWrapper.class.getCanonicalName()); } byte[] bytes = bos.toByteArray(); return new String(Arrays.copyOf(bytes, 5000)) + "...."; }Cobblestone
It's also worth putting in a "log.isTraceEnabled()" switch around the logging too.Cobblestone
What would be cool is if Java added some default methods to HttpServletResponse so we don't need to write such a huge implementation.Cobblestone
This should be the accepted answer! It answers question correctly and to the point. Works perfectly!Consumerism
plus one for including the import statementsMittel
What does "just register it as a filter" means?Offence
@Offence It's explained here baeldung.com/spring-boot-add-filterWera
P
19

If you dont mind trying Spring AOP, this is something I have been exploring for logging purposes and it works pretty well for me. It wont log requests that have not been defined and failed request attempts though.

Add these three dependencies

spring-aop, aspectjrt, aspectjweaver

Add this to your xml config file <aop:aspectj-autoproxy/>

Create an annotation which can be used as a pointcut

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

Now annotate all your rest API methods which you want to log

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

Now on to the Aspect. component-scan the package which this class is in.

@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 runs when a matched method execution returns normally.

@AfterThrowing advice runs when a matched method execution exits by throwing an exception.

If you want to read in detail read through this. http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html

Pest answered 27/8, 2016 at 13:1 Comment(3)
This logs the method invocation, not what was actually received and sent at the HTTP level.Prog
How to write request BODY ? In my case it is POST BODY. on request.getReader or getInputStream I get error that stream is closed.Milkwort
yeah stream is closed because it will be consumed by Spring before aspect gets calledCaracaraballo
M
17

Currently Spring Boot has the Actuator feature to get the logs of requests and responses.

But you can also get the logs using Aspect(AOP).

Aspect provides you with annotations like: @Before, @AfterReturning, @AfterThrowing etc.

@Before logs the request, @AfterReturning logs the response and @AfterThrowing logs the error message, You may not need all endpoints' log, so you can apply some filters on the packages.

Here are some examples:

For Request:

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

Here @Before("within(your.package.where.endpoints.are..*)") has the package path. All endpoints within this package will generate the log.

For Response:

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

Here @AfterReturning("within(your.package.where.endpoints.are..*)") has the package path. All endpoints within this package will generate the log. Also Object returnValue contains the response.

For Exception:

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

Here @AfterThrowing(pointcut = ("within(your.package.where.endpoints.are..*)"), throwing = "e") has the package path. All endpoints within this package will generate the log. Also Exception e contains the error response.

Here is the full code:

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

Here, using @ConditionalOnExpression("${endpoint.aspect.enabled:true}") you can enable/disable the log. just add endpoint.aspect.enabled:true into the application.property and control the log

More info about AOP visit here:

Spring docs about AOP

Sample article about AOP

Monadelphous answered 4/9, 2018 at 10:49 Comment(2)
new ObjectMapper() is expensive, better share one mapper for allSeventeenth
Yeah, sure. This is demo code. In production we have to follow best practices.Monadelphous
V
15

After adding Actuators to the spring boot bassed application you have /trace endpoint available with latest requests informations. This endpoint is working based on TraceRepository and default implementation is InMemoryTraceRepository that saves last 100 calls. You can change this by implementing this interface by yourself and make it available as a Spring bean. For example to log all requests to log (and still use default implementation as a basic storage for serving info on /trace endpoint) I'm using this kind of implementation:

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

This traceInfo map contains basic informations about request and response in this kind of form: {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}}}. There is NO response content here.

EDIT! Logging POST data

You can access POST data by overriding WebRequestTraceFilter, but don't think it is a good idea (e.g. all uploaded file content will go to logs) Here is sample code, but don't use it:

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;
  }
}
Vassalize answered 30/3, 2017 at 5:42 Comment(6)
What about POST body?Overstudy
@dart I've added example for youVassalize
I was doing something like this, but the problem is the response body is not available to TraceRepository, how can we access to that?Dysprosium
@AmirPashazadeh you have to override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) but I'm not sure when this filter is executed - may be on request phase, so response body won't be ready there.Vassalize
Both of those classes are not available in Spring Boot version 2.1.6. Does anyone have a workaround for that?Prosthodontist
@Prosthodontist Since 2.0 there is HttpTraceRepository (instead of TraceRepository)Vassalize
P
11

Please refer to below link for actual answer https://gist.github.com/int128/e47217bebdb4c402b2ffa7cc199307ba

Made some changes from above referred solution , request and response will log in console and in file too if logger level is info. we can print either in console or file.

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

Output in File:

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}
Pentathlon answered 21/6, 2018 at 18:12 Comment(2)
Great answer, only suggestion would be to collect all the output into a buffer and log in a single statement.Resurgent
what if the api is returning Huge Stream of File. how to handle that?Wenoa
S
10

Here my solution (Spring 2.0.x)

Add the maven dependency:

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

Edit the application.properties and add the following line:

management.endpoints.web.exposure.include=* 

Once your spring boot application is started you can track the latest 100 http requests by calling this url: http://localhost:8070/actuator/httptrace

Steib answered 10/10, 2018 at 21:48 Comment(2)
This does not show the request body i suppose.Ciscaucasia
You also need to add an InMemoryHttpTraceRepository Bean. See: juplo.de/actuator-httptrace-does-not-work-with-spring-boot-2-2Revenant
L
8

You can also configure a custom Spring interceptor HandlerInterceptorAdapter for a simplified implementation of pre-only/post-only interceptors:

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

Then, you register as many interceptors as you want:

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    CustomHttpInterceptor customHttpInterceptor;

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

}

Note: just like stated by @Robert, you need to pay attention to the specific implementations of HttpServletRequest and HttpServletResponse your application is using.

For example, for apps using the ShallowEtagHeaderFilter, the response implementation would be a ContentCachingResponseWrapper, so you'd have:

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

}
Lollard answered 23/4, 2018 at 15:19 Comment(0)
R
7

the code pasted below works with my tests and can be downloaded from my [github project][1], sharing after applying a solution based on that on a production project.

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

    }
}
Rizzi answered 21/12, 2019 at 12:5 Comment(0)
W
6

If somebody still need it here is simple implementation with Spring HttpTrace Actuator. But as they have told upper it doesn't log bodies.

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()));
    }
}
Warehouseman answered 20/3, 2018 at 15:30 Comment(1)
no response bodyAlcaraz
O
5

@hahn's answer required a bit of modification for it to work for me, but it is by far the most customizable thing I could get.

It didn't work for me, probably because I also have a HandlerInterceptorAdapter[??] but I kept getting a bad response from the server in that version. Here's my modification of it.

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);
    }
}
Overwrought answered 8/12, 2016 at 15:46 Comment(1)
is your application packaged as war or jar? I keep getting the error java.io.FileNotFoundException: Could not open ServletContext resource [/WEB-INF/loggingDispatcherServlet-servlet.xml]Insolvency
G
4

Has there been any development with Actuator HTTP Trace since the initial question was posted i.e. is there a way to enrich it with the response body?

What about enriching it with custom metadata from MDC or from Spring-Sleuth or Zipkin, such as traceId and spanId?

Also for me Actuator HTTP Trace didn't work Spring Boot 2.2.3, and I found the fix here: https://juplo.de/actuator-httptrace-does-not-work-with-spring-boot-2-2/

pom.xml

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

application.properties

management.endpoints.web.exposure.include=httptrace

The fix:

The simple fix for this problem is, to add a @Bean of type InMemoryHttpTraceRepository to your @Configuration-class:

@Bean
public HttpTraceRepository htttpTraceRepository()
{
  return new InMemoryHttpTraceRepository();
}

The Explanation:

The cause of this problem is not a bug, but a legitimate change in the default configuration. Unfortunately, this change is not noted in the according section of the documentation. Instead it is burried in the Upgrade Notes for Spring Boot 2.2

The default-implementation stores the captured data in memory. Hence, it consumes much memory, without the user knowing, or even worse: needing it. This is especially undesirable in cluster environments, where memory is a precious good. And remember: Spring Boot was invented to simplify cluster deployments!

That is, why this feature is now turned of by default and has to be turned on by the user explicitly, if needed.

Girandole answered 30/10, 2020 at 11:57 Comment(4)
This doesn't work with latest Spring Boot. /actuator/ and /actuator/httptrace will still return 404Turnstone
It appears endpoint was changed to /httptrace. All endpoints: docs.spring.io/spring-boot/docs/2.1.7.RELEASE/reference/html/…Turnstone
You'll need to find your management.endpoints.web.base-path config if you want to know what the actual endpoint is.Turnstone
Definitely still needs the HttpTraceRepository Bean in your @Configuration class. Doesn't work without it.Turnstone
A
3

If you are seeing only part of your request payload, you need to call the setMaxPayloadLength function as it defaults to showing only 50 characters in your request body. Also, setting setIncludeHeaders to false is a good idea if you don't want to log your auth headers!

@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;
}
Aalesund answered 20/6, 2018 at 18:7 Comment(1)
I am trying to use it in spring mvc and it's not working for me, It is required any additional setting except registering this bean and adding logger?Newmann
S
3

As suggested previously, Logbook is just about perfect for this, but I did have a little trouble setting it up when using Java modules, due to a split package between logbook-api and logbook-core.

For my Gradle + Spring Boot project, I needed

build.gradle

dependencies {
    compileOnly group: 'org.zalando', name: 'logbook-api', version: '2.4.1'
    runtimeOnly group: 'org.zalando', name: 'logbook-spring-boot-starter', version: '2.4.1'
    //...
}

logback-spring.xml

<configuration>
    <!-- HTTP Requests and Responses -->
    <logger name="org.zalando.logbook" level="trace" />
</configuration>
Spic answered 1/12, 2020 at 20:7 Comment(0)
I
3

I created a file called LoggingConfig.java with contents:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.CommonsRequestLoggingFilter;

@Configuration
public class LoggingConfig {

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

In application.properties I added:

logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG
Insufficiency answered 29/3, 2022 at 7:45 Comment(0)
E
2

In order to log all the requests with input parameters and body, we can use filters and interceptors. But while using a filter or interceptor, we cannot print the request body multiple times. The better way is we can use spring-AOP. By using this we can decouple the logging mechanism from the application. AOP can be used for logging Input and output of each method in the application.

My solution is:

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

}

Enculturation answered 11/1, 2020 at 9:55 Comment(1)
I just answered the same thing and thought maybe best to checkout all the other answers as well. AOP is perfect for logging, as it is cross-cutInapprehensible
S
2

Note

 @Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
...
}

approach will not work with spring security filter chains. You have to manually add CommonsRequestLoggingFilter like

protected void configure(HttpSecurity http) throws Exception {
         HttpSecurity filter = http
        .cors().and().addFilterBefore(new CommonsRequestLoggingFilter(), CorsFilter.class);
}
Shelia answered 4/8, 2021 at 13:32 Comment(0)
I
2

I created a Filter class which extends the Spring class CommonsRequestLoggingFilter. It logs all requests to file and database.

package <my.package>
//...

public class LogRequestFilter extends CommonsRequestLoggingFilter {

    @Autowired
    private LogRequestRepository logRequestRepository;
    
    @Autowired
    private ObjectMapper jsonMapper;
    
    @Getter
    @Setter
    private boolean includeParams = false;
    
    @Getter
    @Setter
    private boolean includeResponsePayload = false;
    
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        
        long startTime = System.currentTimeMillis();
        ContentCachingRequestWrapper wrappedRequest = null;
        ContentCachingResponseWrapper wrappedResponse = null;
        LogRequest logRequest = null;
        try {
            
            // filter calls by methods
            if (!request.getMethod().equals("OPTIONS")) {
                
                // read request
                String requestHeaders = isIncludeHeaders() ? getRequestHeaders(request) : null;
                String requestParams = isIncludeParams() ? getRequestParams(request) : null;
                String requestUri = getRequestURI(request);
                String userId = getUserId(request);
                
                // wrap request/response to intercept them
                wrappedRequest = new ContentCachingRequestWrapper(request);
                wrappedResponse = new ContentCachingResponseWrapper(response);

                // forward request
                super.doFilterInternal(wrappedRequest, wrappedResponse, filterChain);
                
                String requestPayload = null;
                if (isIncludePayload()) {
                    requestPayload = super.getMessagePayload(wrappedRequest);
                    if (requestPayload != null && "application/json".equalsIgnoreCase(request.getContentType())) {
                        Object o = jsonMapper.readValue(requestPayload, Object.class);
                        requestPayload = jsonObjectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(o);
                    }
                }
                
                String responsePayload = null;
                if (isIncludeResponsePayload()) {
                    responsePayload = getResponsePayload(wrappedResponse);
                    if (responsePayload != null && "application/json".equalsIgnoreCase(response.getContentType())) {
                        Object o = jsonMapper.readValue(responsePayload, Object.class);
                        responsePayload = jsonObjectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(o);
                    }
                }

                // prepare log
                logRequest = new LogRequest();
                logRequest.setThreadId(Thread.currentThread().getId());
                logRequest.setRequestHeaders(requestHeaders);
                logRequest.setRequestParams(requestParams);
                logRequest.setRequestUri(requestUri.toString());
                logRequest.setUserId(userId);
                logRequest.setRequestPayload(requestPayload);
                logRequest.setResponsePayload(responsePayload);
                logRequest.setHttpStatus(response.getStatus());
                //logRequest.setContentType(response.getContentType() + "; charset=" + response.getCharacterEncoding());
                logRequest.setResponseContentType(response.getContentType());
                
            }
            else {
                super.doFilterInternal(request, response, filterChain);
            }
            
        }
        finally {
            // save log to db
            if (logRequest != null) {
                logRequest.setTime((System.currentTimeMillis() - startTime) / 1000.0);
                logRequestRepository.save(logRequest);
            }
            
            // important! copy to the original response
            if (wrappedResponse != null)
                wrappedResponse.copyBodyToResponse();
        }
    }

    private String getRequestURI(HttpServletRequest request) {
        StringBuilder requestUri = new StringBuilder();
        requestUri.append(request.getMethod()).append(" ").append(request.getRequestURI());
        if (isIncludeQueryString()) {
            String queryString = request.getQueryString();
            if (queryString != null) requestUri.append("?").append(queryString);
        }
        return requestUri.toString();
    }
    
    private String getUserId(HttpServletRequest request) {
        String userId = request.getParameter("userId");
        if (userId == null) userId = request.getParameter("userid");
        return userId;
    }

    private String getRequestParams(HttpServletRequest request) {
        Enumeration<String> parameterNames = request.getParameterNames();
        StringBuilder requestParameters = new StringBuilder();
        while (parameterNames.hasMoreElements()) {
            String parameterName = parameterNames.nextElement();
            requestParameters.append(parameterName).append(": ").append(request.getParameter(parameterName)).append("\n");
        }
        return requestParameters.toString();
    }

    private String getRequestHeaders(HttpServletRequest request) {
        Enumeration<String> headerNames = request.getHeaderNames();
        StringBuilder requestHeaders = new StringBuilder();
        while (headerNames.hasMoreElements()) {
            String headerName = (String) headerNames.nextElement();
            requestHeaders.append(headerName).append(": ").append(request.getHeader(headerName)).append("\n");
        }
        return requestHeaders.toString();
    }

    @SuppressWarnings("unused")
    private String getResponseHeaders(HttpServletResponse response) {
        Collection<String> responseHeaderNames = response.getHeaderNames();
        StringBuilder responseHeaders = new StringBuilder();
        for (Iterator<String> iterator = responseHeaderNames.iterator(); iterator.hasNext();) {
            String responseHeaderName = iterator.next();
            responseHeaders.append(responseHeaderName).append(": ").append(response.getHeader(responseHeaderName)).append("\n");
        }
        return responseHeaders.toString();
    }
    
    protected 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, getMaxPayloadLength());
                try {
                    return new String(buf, 0, length, wrapper.getCharacterEncoding());
                }
                catch (UnsupportedEncodingException ex) {
                    return "[unknown]";
                }
            }
        }
        return null;
    }
    
}

I configured it in @Configuration class:

@Value("${logging.filter.logRequest.maxPayloadLength}")
public int requestLogFilterMaxPayloadLength;
// etc...

@Bean
public CommonsRequestLoggingFilter requestLogFilter() {
    LogRequestFilter filter = new LogRequestFilter();
    filter.setAfterMessagePrefix("REQUEST:[");
    filter.setAfterMessageSuffix("]");
    filter.setIncludeClientInfo(requestLogFilterIncludeClientInfo);
    filter.setIncludeHeaders(requestLogFilterIncludeHeaders);
    filter.setIncludeParams(requestLogFilterIncludeParams);
    filter.setIncludePayload(requestLogFilterIncludePayload);
    filter.setIncludeResponsePayload(requestLogFilterIncludeResponsePayload);   
    filter.setIncludeQueryString(requestLogFilterIncludeQueryString);
    filter.setMaxPayloadLength(requestLogFilterMaxPayloadLength);
    return filter;
}

and finally in the application.yml:

logging:
  file: log/logRequest.log
  filter:
    logRequest:
      includeClientInfo: false
      includeHeaders: true
      includeParams: true
      includePayload: true
      includeQueryString: true
      includeResponsePayload: true
      maxPayloadLength: 100000
  level:
    root: WARN
    <my.package>.LogRequestFilter: DEBUG
Immensurable answered 26/3, 2023 at 15:10 Comment(0)
S
1

if you use Tomcat in your boot app here is org.apache.catalina.filters.RequestDumperFilter in a class path for you. (but it will not provide you "with exceptions in single place").

Scummy answered 25/7, 2017 at 13:19 Comment(0)
Z
1

If you have Spring boot Config server configured then just enable Debug logger for class :

Http11InputBuffer.Http11InputBuffer.java

Debugs will log all the requests and responses for every request

Zachariah answered 19/5, 2020 at 16:6 Comment(1)
Adding "logging.level.org.apache.coyote.http11=DEBUG" to application.properties logs request & response but any request made to other back end services using restTemplate are not logged same way.Vogul
A
1

Logging Request + payload in a CUSTOM FORMAT:

For a custom format, just override the super implementation of the Spring logger Bean - org/springframework/web/filter/AbstractRequestLoggingFilter.java

Let's say we whish skipping GET requests and only tracing write requests (PUT, PATCH, DELETE, etc) with INFO log level:

@Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
CommonsRequestLoggingFilter logFilter = new CommonsRequestLoggingFilter() {

  @Override
  protected boolean shouldLog(HttpServletRequest request) {
    return logger.isInfoEnabled() && !Objects.equals(request.getMethod(), "GET");
  }


  @Override
  protected void beforeRequest(HttpServletRequest request, String message) {
    // Do nothing if you need logging payload.
    // As, Before the Request, the payload is not read from the input-stream, yet.
  }


  @Override
  protected void afterRequest(HttpServletRequest request, String message) {
    logger.info(message); // Or log to a file here, as OP asks.
  }


  @Override
  protected @NonNull String createMessage(HttpServletRequest request, @NonNull String prefix, @NonNull String suffix) {
    // Output: [PUT][/api/my-entity], user:[my-loging], payload was:[{ "id": 33, "value": 777.00}]
    StringBuilder msg = new StringBuilder()
        .append(prefix)
        .append("[").append(request.getMethod()).append("]")
        .append("[").append(request.getRequestURI()).append("]");

    String user = request.getRemoteUser();
    msg.append(", user:[").append(null == user ? "" : user).append("]");

    String payload = getMessagePayload(request);
    if (payload != null) {
      // It's not null on After event. As, on Before event, the Input stream was not read, yet.
      msg.append(", payload was:[").append(payload.replace("\n", "")).append("]");  // Remove /n to be compliant with elastic search readers.
    }

    msg.append(suffix);
    return msg.toString();
  }
};
logFilter.setBeforeMessagePrefix("Incoming REST call: -->>>[");
logFilter.setBeforeMessageSuffix("]...");
logFilter.setAfterMessagePrefix("REST call processed: -<<<[");
logFilter.setAfterMessageSuffix("]");
logFilter.setIncludePayload(true);
logFilter.setMaxPayloadLength(64000);
return logFilter;
}

Loggging Requeest + Response/status:

see https://www.baeldung.com/spring-http-logging#custom-request-logging

(I can add the exact code example to here if the answer gets the demand/ reaches 50+ upvotes)

Acetylate answered 8/4, 2022 at 15:6 Comment(0)
G
1

This logs request and response body:


import java.io.IOException;
import java.io.UnsupportedEncodingException;

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

@Component
public class LoggingFilter extends OncePerRequestFilter {

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

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);

        long startTime = System.currentTimeMillis();
        filterChain.doFilter(requestWrapper, responseWrapper);
        long timeTaken = System.currentTimeMillis() - startTime;

        String requestBody = getStringValue(requestWrapper.getContentAsByteArray(),
                request.getCharacterEncoding());
        String responseBody = getStringValue(responseWrapper.getContentAsByteArray(),
                response.getCharacterEncoding());

        LOGGER.info(
                "FINISHED PROCESSING : METHOD={}; REQUESTURI={}; REQUEST PAYLOAD={}; RESPONSE CODE={}; RESPONSE={}; TIM TAKEN={} milliseconds",
                request.getMethod(), request.getRequestURI(), requestBody, response.getStatus(), responseBody,
                timeTaken);
        responseWrapper.copyBodyToResponse();
    }

    private String getStringValue(byte[] contentAsByteArray, String characterEncoding) {
        try {
            return new String(contentAsByteArray, 0, contentAsByteArray.length, characterEncoding);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return "";
    }

}
Gratulant answered 29/8, 2023 at 15:3 Comment(2)
Many solutions here did not work for me. I have a requirement that logging level in PROD cannot be finer than INFO. This is a good solution!Cocoa
Getting "requestBody" after call "doFilter" prevent have incremental log of request/method processing/response. how overcome it?Caravel
R
0

You can use Actuator in Spring Boot.
It logs requests and responses and more information about servlet and system operating.
You just add it as a dependency of your project and config, if more details needed to log.
Take a look at this example:
Actuator in Spring Boot example

Ramillies answered 28/6, 2022 at 12:2 Comment(2)
actuator doesn't log the body of the request or response.Deathbed
Just mention that after Spring Boot 2.2.0 M3 Spring Framework 5.2 M2 actuator http trace and auditing are disabled by default since the default repositories implementations are in-memory and may consume too many resources and are not cluster friendly.Halfmast
W
0

I managed to get this done for Spring boot with the help of logbook https://github.com/zalando/logbook

Read the full article I wrote about this here https://medium.com/@pramodyahk/configure-request-response-logging-for-spring-boot-quickly-using-logbook-183503c4676c This describes additional configs for masking, excluding and supporting SOAP and JMS.

Summary below

Added logbook to the project

implementation group: 'org.zalando', name: 'logbook-spring-boot-starter', version: 'X.XX.X'

Then modified the RestTemplate as below

@Bean
public RestTemplate restTemplate( LogbookClientHttpRequestInterceptor interceptor) {
  
    var requestFactory = new HttpComponentsClientHttpRequestFactory(HttpClients.createDefault());
        requestFactory.setReadTimeout((int) TimeUnit.SECONDS.toMillis(30));
        requestFactory.setConnectTimeout((int) TimeUnit.SECONDS.toMillis(30));
        ClientHttpRequestFactory factory = new BufferingClientHttpRequestFactory(requestFactory);
RestTemplate restTemplate = new RestTemplate(factory);
    restTemplate.setInterceptors(Collections.singletonList(interceptor));
    ............
    return restTemplate;
}

Then set the log level to trace logging.level.org.zalando.logbook=TRACE

And it will start logging something like this to above log for all the requests coming to through the controllers and any outgoing calls using the same RestTemplate

{
  "origin": "local",
  "type": "response",
  "correlation": "b0dc1cd78680e879",
  "duration": 2420808,
  "protocol": "HTTP/1.1",
  "status": 200,
  "headers": {
    "Cache-Control": [
      "no-cache, no-store, max-age=0, must-revalidate"
    ],
    "Connection": [
      "keep-alive"
    ],
    "Content-Type": [
      "application/json;charset=UTF-8"
    ]
  },
  "body": {
      // JSON body here
  }
}
Wiseacre answered 16/2, 2024 at 2:45 Comment(0)
W
-1

In order to log requests that result in 400 only:

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

}
Wiredraw answered 12/4, 2018 at 20:32 Comment(0)
K
-6

you can use Aspect Oriented Programming To Handle All These In One Plcae.

Kellar answered 6/3, 2022 at 12:38 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.