How to Log Request and Response Bodies in Spring WebFlux?

To log request and response bodies in Spring WebFlux, we need to use filters or logging mechanisms that can intercept and log the necessary information. In this article, we use WebFilter to filter the request types in the Spring application.

Intercepting the Request Body:

  • The getRequest().getBody() method returns a Flux<DataBuffer>.
  • Map over this Flux to read the request body bytes and convert them into a string for logging purposes.
  • Then continue the filter chain with chain.filter(exchange).

Intercepting the Response Body:

  • The doOnSuccess method is used to log the response body after the request has been processed.
  • exchange.getResponse().beforeCommit() ensures that we subscribe to the response body before it is committed.
  • Similar to the request body, the response body is read from the DataBuffer and logged.

Prerequisites:

You should have knowledge of the below-listed topics to understand this article.

  • Spring Framework
  • Spring WebFlux
  • Publisher and Consumer
  • Operators in Spring reactor
  • Maven
  • Events Flow and Subscription
  • Web Filters
  • HTTP Request and Response

Tools & Technologies:

  • Spring Framework
  • Spring Reactive programming
  • Spring Tool Suite
  • project type : maven

Steps to Implement Log Request and Response Bodies in Spring WebFlux

Here we created one simple spring reactive project Below we provide the in detail explanation with examples for better understanding the concept.

Dependencies:

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

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

Project folder Structure:

After creating the project, the structure will be like below:


Step 1: Logging Configuration

To enable logs in Spring application, we need configure the logs in the application properties file. Open application properties file which is located in resource folder of project. You can observe this in the above project folder image. Below we provide the code for configure the log functionality in spring application.

spring.application.name=log

# application.properties
logging.level.org.springframework.web=DEBUG
logging.level.com.example.logging=DEBUG


Step 2: LoggingFilter Implementation

Now, we create a Component class in the project package by using @Component spring annotation. We can use @Component throughout the application to mark beans as managed Spring components.

LoggingFilter.java:

Java
package com.app;

import java.nio.charset.StandardCharsets;

import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Component
public class LoggingFilter implements WebFilter {

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

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();

        return DataBufferUtils.join(request.getBody())
                .flatMap(dataBuffer -> {
                    byte[] bytes = new byte[dataBuffer.readableByteCount()];
                    dataBuffer.read(bytes);
                    String body = new String(bytes, StandardCharsets.UTF_8);
                    logger.info("Request Body: {}", body);
                    return chain.filter(exchange.mutate().request(request.mutate().build()).build())
                            .then(Mono.defer(() -> {
                                ServerHttpResponse response = exchange.getResponse();
                                ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(response) {
                                    @Override
                                    public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                                        Flux<? extends DataBuffer> flux = Flux.from(body);
                                        return super.writeWith(flux.map(dataBuffer -> {
                                            byte[] content = new byte[dataBuffer.readableByteCount()];
                                            dataBuffer.read(content);
                                            String responseBody = new String(content, StandardCharsets.UTF_8);
                                            logger.info("Response Body: {}", responseBody);
                                            return dataBuffer;
                                        }));
                                    }

                                    @Override
                                    public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
                                        return writeWith(Flux.from(body).flatMapSequential(p -> p));
                                    }
                                };
                                return chain.filter(exchange.mutate().response(decoratedResponse).build());
                            }));
                });
    }
 }


In the above code, we implemented the user-created class to WebFilter. And override the filter method to log request and response bodies. Ensure that the filter is registered in your Spring Boot application by annotating it with @Component.

After creating the class we define the Logger inside the class.

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

After this, we write our own logic for overriding the filter method and It can take ServerWebExchange, and WebFilterChain as arguments. In the filter method, we define the ServerHttpRequest object by using the ServerWebExchange object. Then return the Mono object with void type.

 @Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();

return DataBufferUtils.join(request.getBody())
.flatMap(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
String body = new String(bytes, StandardCharsets.UTF_8);
logger.info("Request Body: {}", body);
return chain.filter(exchange.mutate().request(request.mutate().build()).build())
.then(Mono.defer(() -> {
ServerHttpResponse response = exchange.getResponse();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(response) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
Flux<? extends DataBuffer> flux = Flux.from(body);
return super.writeWith(flux.map(dataBuffer -> {
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
String responseBody = new String(content, StandardCharsets.UTF_8);
logger.info("Response Body: {}", responseBody);
return dataBuffer;
}));
}

@Override
public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
return writeWith(Flux.from(body).flatMapSequential(p -> p));
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}));
});
}


Step 3: Sample RestController

After this, we create one RestController class in the main package of the project by using @RestController annotation. Inside the class we created a method called sayHello() with @RequestMapping(“/hello”) and this method returns a String value in the form of Mono publisher.

Java
package com.app;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@RestController
@RequestMapping("/hello")
public class HelloController {

    @GetMapping
    public Mono<String> sayHello() {
        return Mono.just("Hello, WebFlux!");
    }
}


Step 4: Run the Application

Now, run this project by using maven commends or run as Spring Boot App. This project runs on Netty Server on port number 8080 by default. Here we run this project as a Spring Boot App.


Step 5: Testing API

Now test the API. Here we use the Post Man tool for testing the API.

http://localhost:8080/hello

Output:


After hitting the API, you can observe the logs in the console.


Note:

  • The code provided is a simplified version and may need enhancements for production use, such as handling larger request or response bodies and considering performance impacts.
  • Ensure proper exception handling and consider using non-blocking approaches if the payload size is significant.
  • You might also need to handle different content types and potentially encode/decode binary data accordingly.


Contact Us