The Spring WebFlux WebClient interface enables you to handle web requests from service to service. But you're going to need to take extra steps if you want detailed logging.

Fortunately, it's not that difficult.

In this guide, I'll show you how to log WebClient requests.

Feel free to just visit the code if you'd rather learn by example.

Otherwise, read on.

The Business Requirements

Your boss Smithers walks into your office. He's in a wee bit of a snit.

"The folks upstairs are tired of incomplete logging!" he shouts. "We need more logging for those service-to-service requests!"

He's talking about the CRM app you're working on. Right now, the contact service makes a REST call to the user service to get details about the current user. It's all handled with WebClient.

But there's no logging. So when something goes wrong, it's a tedious process to figure out what happened.

"I need better logging!" Smithers shouts again. "They need better logging!! Everybody needs better logging!!!"

He leaves your office shouting to himself as he walks down the hall.

Forging the Filter

There are a few different ways to skin this "log the request" cat. You can use the Google to find several of them.

Or use DuckDuckGo if that's how you roll.

But for the purposes of this guide, I'll focus on using ExchangeFilterFunction.

Now what the heck is that?

It's a functional interface. That means it's got one (1) unimplemented method.

That method is called filter() and it accepts two parameters: ClientRequest and ExchangeFunction.

So what's the point of the interface? It acts as a filter for an HTTP request.

In other words, it's a lot like an HTTP request filter in Angular. If you know about that.

It's also similar to the HTTP interceptors you might sometimes use in Spring Boot.

In other words, it looks at the request, performs some action, and then passes the request on to the next stage (which may be another filter).

And that's what you're going to do here. You'll use the filter to examine the header, method, and URL. Then you'll log all of that info and move on.

Now if you're wondering: "What about logging the request body in a POST?" then I have some bad news.

Yes, you can log the request body. But it's way more complicated than it should be.

Instead, I recommend just using Jackson or JAXB to transform the object and log it outside of the filter. It's way easier.

So with that in mind, create a new class.

package com.careydevelopment.contact.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;

import reactor.core.publisher.Mono;

public class WebClientFilter {

    private static final Logger LOG = LoggerFactory.getLogger(WebClientFilter.class);

	
	public static ExchangeFilterFunction logRequest() {
		return ExchangeFilterFunction.ofRequestProcessor(request -> {
			logMethodAndUrl(request);
			logHeaders(request);
			
			return Mono.just(request);
		});
	}
	
	
	private static void logHeaders(ClientRequest request) {
		request.headers().forEach((name, values) -> {
			values.forEach(value -> {
				logNameAndValuePair(name, value);
			});
		});
	}
	
	
	private static void logNameAndValuePair(String name, String value) {
		LOG.debug("{}={}", name, value);
	}
	
	
	private static void logMethodAndUrl(ClientRequest request) {
		StringBuilder sb = new StringBuilder();
		sb.append(request.method().name());
		sb.append(" to ");
		sb.append(request.url());
		
		LOG.debug(sb.toString());
	}
}

There's only a single public method in that class: logRequest(). That's what the WebClient will use to handle logging info about the outgoing request.

The body of the method is a single lambda expression. That ofRequestProcessor() static method that you see there accepts a Function as its only parameter. So the code handles that with a lambda expression.

That lambda expression does a couple of things before it makes its escape: it logs the HTTP method with the URL and then it logs the headers.

You can see the methods for handling both of those tasks in the code block above.

Finally, the code gets out. But remember, it's in a reactive framework. So it has to return a publisher before it can leave.

That's why you see the Mono.just(request) line above. The code sends back the ClientRequest object once it's finished.

Why is it necessary to return the request? For the same reason you would do so in other similar frameworks.

It's possible to clone the request and make a change (like adding a new header). Then, you'd send the cloned request back to the chain for further processing.

Full disclosure, though: I haven't done that. And I think if you wanted to go that route you should implement the filter() function rather than using ofRequestProcessor().

Sounds like a great subject for a future article!

Working With WebClient

Now update UserService to use the filter that you just created. You can do that in the constructor.

    public UserService(@Value("${ecosystem.properties.file.location}") String ecosystemFile) {
        PropertiesUtil propertiesUtil = new PropertiesUtil(ecosystemFile);
        String endpoint = propertiesUtil.getProperty("ecosystem-user-service.endpoint");
        
        userClient = WebClient
	        		.builder()
	        		.baseUrl(endpoint)
	        		.filter(WebClientFilter.logRequest())
	        		.build();
    }

The code uses the WebClient builder to construct the object. It does that by first setting the base URL as retrieved from the external properties file.

But then it does something else: it adds the filter that will log requests.

You can see that with the .filter() method.

And that's really all you need to do to add logging to your WebClient requests.

A Beautiful Test

It's time to test this thing out. I prefer to do that in initialization code:

@Component
public class ApplicationListenerInitialize implements ApplicationListener<ApplicationReadyEvent>  {
	
	@Autowired
	private UserService userService;
	
    public void onApplicationEvent(ApplicationReadyEvent event) {        	
    	SalesOwner owner = userService.fetchUser("Bearer eyJhbGciOiJIUzU...");
    	
    	try {
	    	ObjectMapper objectMapper = new ObjectMapper();
	    	objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
	    	objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
	    	System.err.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(owner));
    	} catch (Exception e) {
    		e.printStackTrace();
    	}
    }
}

Remember: to retrieve a user from the user service, you'll need a bearer token. So you'll have to login to the user service via Postman to get that token.

I've abbreviated the token up above.

Everything else is pretty standard fare. The code instantiates the SalesOwner object from the downstream service response and prints it out in JSON format.

Now save that file and restart your Spring Boot application. After it runs for a while, you should something like this nice output in red:

{
  "id" : "544403f2b",
  "firstName" : "Darth",
  "lastName" : "Vader",
  "email" : "darth@xmail.com",
  "username" : "darth",
  "phoneNumber" : "474-555-1212"
}

But that's not why you're here, is it? You want to see the logging from the request.

So scroll up a little bit in your console log and you should something like this:

[main] DEBUG c.c.contact.service.WebClientFilter - GET to http://localhost:8081/user/me
[main] DEBUG c.c.contact.service.WebClientFilter - Authorization=Bearer eyJhbGciOiJI...

There you go. That's the logging you're looking for.

Wrapping It Up

Congrats! You've now logged your outgoing WebClient request with a filter.

Now it's up to you to make improvements as you see fit. Perhaps you'd like to only log certain headers or specific types of requests. It's easy to make that happen with a few adjustments.

Also, feel free to grab the code on GitHub.

Have fun!

Photo by Khari Hayden from Pexels