Generic Top

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Introduction

In this quick tutorial, we’ll demonstrate the basics of logging incoming requests using Spring's logging filter. If we’re just getting started with logging, we can check out this logging intro article, as well as the SLF4J article.

2. Maven Dependencies

The logging dependencies will be the same as the ones in the intro article; we'll simply add Spring here:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>5.2.2.RELEASE</version>   
</dependency>

The latest version can be found here for spring-core.

3. Basic Web Controller

First, we'll define a controller to use in our example:

@RestController
public class TaxiFareController {

    @GetMapping("/taxifare/get/")
    public RateCard getTaxiFare() {
        return new RateCard();
    }
    
    @PostMapping("/taxifare/calculate/")
    public String calculateTaxiFare(
      @RequestBody @Valid TaxiRide taxiRide) {
 
        // return the calculated fare
    }
}

4. Custom Request Logging

Spring provides a mechanism for configuring user-defined interceptors to perform actions before and after web requests.

Among the Spring request interceptors, one of the noteworthy interfaces is HandlerInterceptor, which we can use to log the incoming request by implementing the following methods:

  1. preHandle() – we execute this method before the actual controller service method
  2. afterCompletion() – we execute this method after the controller is ready to send the response

Furthermore, Spring provides the default implementation of the HandlerInterceptor interface in the form of the HandlerInterceptorAdaptor class, which the user can extend.

Let's create our own interceptor by extending HandlerInterceptorAdaptor as:

@Component
public class TaxiFareRequestInterceptor 
  extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(
      HttpServletRequest request, 
      HttpServletResponse response, 
      Object handler) {
        return true;
    }

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

Finally, we'll configure the TaxiRideRequestInterceptor inside the MVC lifecycle to capture the pre and post-processing of controller method invocations that map to the path /taxifare defined in the TaxiFareController class:

@Configuration
public class TaxiFareMVCConfig implements WebMvcConfigurer {

    @Autowired
    private TaxiFareRequestInterceptor taxiFareRequestInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(taxiFareRequestInterceptor)
          .addPathPatterns("/taxifare/*/");
    }
}

In conclusion, the WebMvcConfigurer adds the TaxiFareRequestInterceptor inside the spring MVC lifecycle by invoking the addInterceptors() method.

The biggest challenge is to get the copies of the request and response payload for logging, and still leave the requested payload for the servlet to process it:

The main issue with the reading request is that, as soon as the input stream is read for the first time, it's marked as consumed and cannot be read again.

The application will throw an exception after reading the request stream:

{
  "timestamp": 1500645243383,
  "status": 400,
  "error": "Bad Request",
  "exception": "org.springframework.http.converter
    .HttpMessageNotReadableException",
  "message": "Could not read document: Stream closed; 
    nested exception is java.io.IOException: Stream closed",
  "path": "/rest-log/taxifare/calculate/"
}

To overcome this problem, we can leverage caching to store the request stream and use it for logging.

Spring provides a few useful classes, such as ContentCachingRequestWrapper and ContentCachingResponseWrapper, which can be used for caching the request data for logging purposes.

Let's adjust our preHandle() of the TaxiRideRequestInterceptor class to cache the request object using the ContentCachingRequestWrapper class:

@Override
public boolean preHandle(HttpServletRequest request, 
  HttpServletResponse response, Object handler) {
 
    HttpServletRequest requestCacheWrapperObject
      = new ContentCachingRequestWrapper(request);
    requestCacheWrapperObject.getParameterMap();
    // Read inputStream from requestCacheWrapperObject and log it
    return true;
}

As we can see, we cache the request object using the ContentCachingRequestWrapper class, which we can use to read the payload data for logging without disturbing the actual request object:

requestCacheWrapperObject.getContentAsByteArray();

Limitation

  • The ContentCachingRequestWrapper class only supports the following:
Content-Type:application/x-www-form-urlencoded
Method-Type:POST
  • We must invoke the following method to ensure that the request data is cached in ContentCachingRequestWrapper before using it:
requestCacheWrapperObject.getParameterMap();

5. Spring Built-In Request Logging

Spring provides a built-in solution to log payloads. We can use the ready-made filters by plugging into the Spring application using configuration.

AbstractRequestLoggingFilter is a filter that provides the basic functions of logging. Subclasses should override the beforeRequest() and afterRequest() methods to perform the actual logging around the request.

The Spring framework provides three concrete implementation classes that we can use to log the incoming request. These three classes are:

  • CommonsRequestLoggingFilter
  • Log4jNestedDiagnosticContextFilter (deprecated)
  • ServletContextRequestLoggingFilter

Now let’s move on to the CommonsRequestLoggingFilter, and configure it to capture incoming requests for logging.

5.1. Configure Spring Boot Application

We can configure the Spring Boot application by adding a bean definition to enable request logging:

@Configuration
public class RequestLoggingFilterConfig {

    @Bean
    public CommonsRequestLoggingFilter logFilter() {
        CommonsRequestLoggingFilter filter
          = new CommonsRequestLoggingFilter();
        filter.setIncludeQueryString(true);
        filter.setIncludePayload(true);
        filter.setMaxPayloadLength(10000);
        filter.setIncludeHeaders(false);
        filter.setAfterMessagePrefix("REQUEST DATA : ");
        return filter;
    }
}

This logging filter also requires us to set the log level to DEBUG. We can enable the DEBUG mode by adding the below element in logback.xml:

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

Another way of enabling the DEBUG level log is to add the following in application.properties:

logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=
  DEBUG

5.2. Configure Traditional Web Application

In the standard Spring web application, we can set the Filter via either XML configuration or Java configuration. So let's set up the CommonsRequestLoggingFilter using conventional Java based configuration.

As we know, the includePayload attribute of the CommonsRequestLoggingFilter is set to false by default. We would need a custom class to override the value of the attribute to enable includePayload before injecting into the container using Java configuration:

public class CustomeRequestLoggingFilter 
  extends CommonsRequestLoggingFilter {

    public CustomeRequestLoggingFilter() {
        super.setIncludeQueryString(true);
        super.setIncludePayload(true);
        super.setMaxPayloadLength(10000);
    }
}

Then we need to inject the CustomeRequestLoggingFilter using the Java based web initializer:

public class CustomWebAppInitializer implements 
  WebApplicationInitializer {
    public void onStartup(ServletContext container) {
        
        AnnotationConfigWebApplicationContext context 
          = new AnnotationConfigWebApplicationContext();
	context.setConfigLocation("com.baeldung");
	container.addListener(new ContextLoaderListener(context));
        
	ServletRegistration.Dynamic dispatcher 
          = container.addServlet("dispatcher", 
          new DispatcherServlet(context));
	dispatcher.setLoadOnStartup(1);
	dispatcher.addMapping("/");	
		
	container.addFilter("customRequestLoggingFilter", 
          CustomeRequestLoggingFilter.class)
          .addMappingForServletNames(null, false, "dispatcher");
    }
}

6. Example in Action

Finally, we can wire up a Spring Boot with context to see in action that the logging of incoming requests works as expected:

@Test
public void givenRequest_whenFetchTaxiFareRateCard_thanOK() {
    TestRestTemplate testRestTemplate = new TestRestTemplate();
    TaxiRide taxiRide = new TaxiRide(true, 10l);
    String fare = testRestTemplate.postForObject(
      URL + "calculate/", 
      taxiRide, String.class);
 
    assertThat(fare, equalTo("200"));
}

7. Conclusion

In this article, we learned how to implement basic web request logging using interceptors. We also explored the limitations and challenges of this solution.

Then we discussed the built-in filter class, which provides ready to use and simple logging mechanisms.

As always, the implementation of the example and code snippets are available over on GitHub.

Generic bottom

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> CHECK OUT THE COURSE
Generic footer banner
4 Comments
Oldest
Newest
Inline Feedbacks
View all comments
Comments are closed on this article!