Logo
Published on

Log Spring WebClient Request and Response

Authors
  • Name
    Twitter

In this blog, We will look into logging Spring WebClient Request and Response calls including body.

If you never used Spring WebClient before, here is my story on Spring WebClient To Send Synchronous Http Requests.

The default HttpClient used by WebClient is Netty. There is no straight way to log request and response including body in a specific format using Netty. So we are gonna change WebClient default http client to jetty.

Jetty uses listeners to provide hooks for all possible request and response events. We will use these hooks to log request and response content.

Below is how we do it:

Include below dependency to pom.xml

<dependency>  
    <groupId>org.eclipse.jetty</groupId>  
    <artifactId>jetty-reactive-httpclient</artifactId>  
    <version>4.0.2</version>  
</dependency>

LoggerService

We will create a LoggerService with only one method which takes Jetty request object. Inside the method we will add request and response hooks to log events.

//Added inline comments for detailed explanation.

package com.commons.rest.client.service;  
  
import lombok.extern.slf4j.Slf4j;  
import org.eclipse.jetty.client.Request;  
import org.eclipse.jetty.http.HttpField;  
import org.springframework.stereotype.Service;  
  
import java.nio.charset.StandardCharsets;  
  
@Slf4j  
@Service  
public class LoggerService {  
    /**  
     *  
     * @param inboundRequest of type jetty client Request Object  
     * @return return same request again after preparing logging  
     */  
    public Request logRequestResponse(Request inboundRequest) {  
        // Created a string builder object to append logs belongs to specific request  
        StringBuilder logBuilder = new StringBuilder();  
  
        // Request Logging  
  
        /*  
            This listener will be invoked when the request is being processed in order to be sent.  
            When this is invoked we are appending uri and method to string builder with line break.  
  
         */  
        inboundRequest.onRequestBegin(request -> logBuilder.append("Request: \n")  
                                                           .append("URI: ")  
                                                           .append(request.getURI())  
                                                           .append("\n")  
                                                           .append("Method: ")  
                                                           .append(request.getMethod()));  
  
        /*  
            This listener will be invoked when the request headers are ready to be sent.  
            When this is invoked we are appending Headers to string builder with proper format.  
            make sure debugging is enabled if not remove log.isDebugEnabled condition.  
         */  
        inboundRequest.onRequestHeaders(request -> {  
            if(log.isDebugEnabled()) {  
                logBuilder.append("\nHeaders:\n");  
                for (HttpField header : request.getHeaders()) {  
                    logBuilder.append("\t\t").append(header.getName()).append(" : ").append(header.getValue()).append("\n");  
                }  
            }  
        });  
  
        /*  
            This listener will be invoked when request content has been sent successfully.  
            When this is invoked we are converting bytes to String and appending to StringBuilder .  
            make sure debugging is enabled if not remove log.isDebugEnabled condition.  
         */  
  
        inboundRequest.onRequestContent((request, content) ->  {  
            if(log.isDebugEnabled()) {  
                var bufferAsString = StandardCharsets.UTF_8.decode(content).toString();  
                logBuilder.append("Body: \n\t")  
                          .append(bufferAsString);  
            }  
        });  
  
  
        // Response Logging  
  
        /*  
            This listener will be invoked  when the response containing HTTP version, HTTP status code and reason has been received and parsed.  
            When this is invoked we are appending response status to string builder with line break for formatting.  
         */  
        inboundRequest.onResponseBegin(response -> logBuilder.append("\nResponse:\n")  
                                                             .append("Status: ")  
                                                             .append(response.getStatus())  
                                                             .append("\n"));  
  
         /*  
            This listener will be invoked when the response headers are received and parsed.  
            When this is invoked we are appending Headers to string builder with proper format.  
            make sure debugging is enabled if not remove log.isDebugEnabled condition.  
         */  
        inboundRequest.onResponseHeaders(response -> {  
            logBuilder.append("Headers:\n");  
            for (HttpField header : response.getHeaders()) {  
                logBuilder.append("\t\t").append(header.getName()).append(" : ").append(header.getValue()).append("\n");  
            }  
        });  
  
        /*  
            This listener will be invoked when response content has been received and parsed.  
            When this is invoked we are converting bytes to String and appending to StringBuilder .  
            make sure debugging is enabled if not remove log.isDebugEnabled condition.  
         */  
        inboundRequest.onResponseContent(((response, content) -> {  
            if(log.isDebugEnabled()) {  
                var bufferAsString = StandardCharsets.UTF_8.decode(content).toString();  
                logBuilder.append("Response Body:\n").append(bufferAsString);  
            }  
        }));  
          
          
        inboundRequest.onResponseSuccess(response -> log.info(logBuilder.toString()));  
  
        inboundRequest.onRequestFailure((request, throwable) -> log.error(logBuilder.toString(), throwable));  
        inboundRequest.onResponseFailure((response, throwable) -> log.error(logBuilder.toString(), throwable));  
  
        return inboundRequest;  
    }  
}

WebClient Instance

We need to create a new Jetty Http Client and we should override new request method. We need to pass request to loggerservice for logging.

Then we need to give the created jetty http client to Spring WebClient, so that WebClient will use jetty as http client for http requests. We need to autowire WebClient in the service where we would like to use it.

package com.commons.config;  
  
import com.samaritan.commons.rest.client.service.LoggerService;  
import lombok.extern.slf4j.Slf4j;  
import org.eclipse.jetty.client.HttpClient;  
import org.eclipse.jetty.client.Request;  
import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.Configuration;  
import org.springframework.http.client.reactive.JettyClientHttpConnector;  
import org.springframework.web.reactive.function.client.WebClient;  
  
import java.net.URI;  
  
@Slf4j  
@Configuration  
public class CommonsConfig {  
  
    @Bean  
    protected WebClient.Builder webClientBuilder(LoggerService loggerService) {  
        HttpClient httpClient = new HttpClient() {  
            @Override  
            public Request newRequest(URI uri) {  
                Request request = super.newRequest(uri);  
                return loggerService.logRequestResponse(request);  
            }  
        };  
        return WebClient.builder().clientConnector(new JettyClientHttpConnector(httpClient));  
    }  
}

Here is the Request interface which has more hooks if you like to configure.

I hope this helps you to log request and response content using Spring WebClient.