Course – LS – All

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

>> CHECK OUT THE COURSE

1. Introduction

Sometimes we might have to do some additional processing, such as logging, on the HTTP request payload. Logging the incoming HTTP request is very helpful in debugging applications.

In this quick tutorial, we’ll learn the basics of logging incoming requests using Spring Boot’s logging filter.

2. Maven Dependency

Let’s start by adding the spring-boot-starter-web dependency to our pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.7.11</version>
</dependency>

This dependency provides all the core requirements to log the incoming requests in a Spring Boot application.

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

In the next sections, we’ll see how to log incoming requests to the Spring Boot application.

4. Using Custom Request Logging

We want to use a custom Filter to capture the request payload before a controller receives the request.

4.1. Wrapping HTTP Request

We need to wrap the HTTP request and log its payload. The HttpServletRequestWrapper class provides the ability to wrap and modify incoming HttpServletRequest objects.

Firstly, let’s create the CachedHttpServletRequest class that extends HttpServletRequestWrapper class:

public class CachedHttpServletRequest extends HttpServletRequestWrapper {

    private byte[] cachedPayload;

    public CachedHttpServletRequest(HttpServletRequest request) throws IOException {
        super(request);
        InputStream requestInputStream = request.getInputStream();
        this.cachedPayload = StreamUtils.copyToByteArray(requestInputStream);
    }
}

In the above code, first, we read the actual request body inside the wrapper constructor and store it in the cachedPayload byte array.

Then, we override getInputStream() and getReader() methods:

@Override
public ServletInputStream getInputStream() {
    return new CachedServletInputStream(this.cachedPayload);
}

@Override
public BufferedReader getReader() {
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedPayload);
    return new BufferedReader(new InputStreamReader(byteArrayInputStream));
}

In the getInputStream() method, we return the new extended ServletInputStream object with the cachedPayload. Also, we override the getReader() method. This method returns a BufferedReader object that can be used to read the body of the request as character data.

4.2. Extending ServletInputStream

As we mentioned, we need to extend ServletInputStream class to create our wrapper. Let’s create the CachedServletInputStream class:

public class CachedServletInputStream extends ServletInputStream {

    private final static Logger LOGGER = LoggerFactory.getLogger(CachedServletInputStream.class);
    private InputStream cachedInputStream;

    public CachedServletInputStream(byte[] cachedBody) {
        this.cachedInputStream = new ByteArrayInputStream(cachedBody);
    }
}

In this class, first, we create a new constructor with cachedBody and return a new ByteArrayInputStream object from it. We use this InputStream object in other overridden methods.

Then, we override the isFinished(), isReady(), setReadListener(), and read() methods:

@Override
public boolean isFinished() {
    try {
        return cachedInputStream.available() == 0;
    } catch (IOException exp) {
        LOGGER.error(exp.getMessage());
    }
    return false;
}

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

@Override
public void setReadListener(ReadListener readListener) {
    throw new UnsupportedOperationException();
}

@Override
public int read() throws IOException {
    return cachedInputStream.read();
}

The isReady() method is used to check if data can be read without blocking and then the data is read. The read() method reads from the cachedInputStream object. The isFinished() method returns true when all the data from the stream has been read, else it returns false.

4.3. Custom Filter

After that, we’ll create a filter class that extends OncePerRequestFilter class. Then, we’ll override the doFilterInternal() method. In doFilterInternal() method, we use our custom request wrapper to get the request payload and log it.

Let’s create a RequestCachingFilter class:

@Order(value = Ordered.HIGHEST_PRECEDENCE)
@Component
@WebFilter(filterName = "RequestCachingFilter", urlPatterns = "/*")
public class RequestCachingFilter extends OncePerRequestFilter {

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

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
      FilterChain filterChain) throws ServletException, IOException {
        CachedHttpServletRequest cachedHttpServletRequest = new CachedHttpServletRequest(request);
        LOGGER.info("REQUEST DATA: " + IOUtils.toString(cachedHttpServletRequest.getInputStream(), StandardCharsets.UTF_8));
        filterChain.doFilter(cachedHttpServletRequest, response);
    }
}

Additionally, the filter is mapped to all of the requests in the application with the URL mapping /*.

In the next section, we’ll use Spring Boot’s built-in request logging solution.

5. Using Spring Boot Built-In Request Logging

Spring Boot provides a built-in solution to log payloads. We can use the ready-made filters by plugging into the Spring Boot 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 Boot framework provides three concrete implementation classes that we can use to log incoming requests:

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

5.2. Configure Logging Level

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

Now, the application is ready to test.

6. Example in Action

Finally, let’s write a test to see how to log the incoming request:

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

When we execute our test case, we’ll see the following output in the console:

18:24:04.318 [] INFO  c.b.web.log.app.RequestCachingFilter - REQUEST DATA: {"isNightSurcharge":true,"distanceInMile":10}
18:24:04.321 [] DEBUG o.s.w.f.CommonsRequestLoggingFilter - Before request [POST /spring-rest/taxifare/calculate/]
18:24:04.429 [] DEBUG o.s.w.f.CommonsRequestLoggingFilter - REQUEST DATA: POST /spring-rest/taxifare/calculate/, payload={"isNightSurcharge":true,"distanceInMile":10}]

We can see that the request payload is printed to the console due to the invocation of the RequestCachingFilter and CommonsRequestLoggingFilter classes.

7. Conclusion

In this article, we learned how to implement basic web request logging using a custom filter in the Spring Boot application. Then we discussed the Spring Boot 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.

Course – LS – All

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

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are closed on this article!