Monitoring the outcome of your HTTP calls is essential in a distributed system. Increased response times or error status codes can break or slow down your application. That's why you should monitor them properly. If you use Spring WebClient as your HTTP client, there is no need to track this manually. With Spring Boot Actuator, there is a way to expose important metrics of your Spring WebClient automatically. With this blog post, you'll learn how to configure your Spring Boot application to use this automatic mechanism.
Spring Boot Project Setup
First, let's have a look at the project setup. The application uses both the spring-boot-starter-web
and spring-boot-starter-webflux
. Hence Spring Boot autoconfigures a Tomcat but also ensures to use non-blocking parts of WebFlux like the WebClient
.
In addition, to actually expose metrics, we need the spring-boot-starter-actuator
dependency:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.3</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>de.rieckpil.blog</groupId> <artifactId>spring-web-client-expose-metrics</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring-web-client-expose-metrics</name> <description>Spring WebClient Expose Metrics</description> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <!-- .... --> </dependencies> <!-- .... --> </project> |
To demonstrate the usage of the WebClient
and explore its metrics, the application hits two different API's during application startup:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | @SpringBootApplication public class Application implements CommandLineRunner { private final RandomQuoteClient randomQuoteClient; private final RandomUserClient randomUserClient; public Application(RandomQuoteClient randomQuoteClient, RandomUserClient randomUserClient) { this.randomQuoteClient = randomQuoteClient; this.randomUserClient = randomUserClient; } public static void main(String[] args) { SpringApplication.run(Application.class, args); } @Override public void run(String... args) throws Exception { for (int i = 0; i < 5; i++) { try { System.out.println(randomQuoteClient.fetchRandomQuotes()); System.out.println(randomUserClient.getRandomUserById(i)); } catch (WebClientResponseException ex) { System.out.println(ex.getRawStatusCode()); } } } } |
We'll see the code for the two injected beans later on, but for now, all they do is to access a remote API on the Internet and return a JsonNode
. The rather ugly try-catch is used to not break the application during startup as one API returns a 404 HTTP status code for the first execution.
This is intended to, later on, see that the collected metrics also include the different HTTP status codes.
Spring WebClient Configuration
Next comes the most important part: the correct usage of the WebClient
. To automatically report metrics, we must not manually construct the client like the following:
1 | WebClient client = WebClient.builder().build(); |
Even though this works fine for making HTTP calls, we won't get metrics from such instances out-of-the-box. We could configure these clients to use the pre-defined MetricsWebClientCustomizer
, but there is a simpler solution…
Instead of creating WebClient
instances from scratch (like above), we can inject an instance ofWebClient.Builder
and start from there. This builder instance is already configured to report metrics automatically.
By the way, the same works for the RestTemplate
using the RestTemplateBuilder
respectively.
Given this pre-configured builder, we're defining a WebClient
bean for the whole application to add general timeouts. This is optional, and we can also directly use WebClient.Builder
within our classes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @Configuration public class WebClientConfiguration { @Bean public WebClient webClient(WebClient.Builder webClientBuilder) { HttpClient httpClient = HttpClient.create() .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2_000) .doOnConnected(connection -> connection.addHandlerLast(new ReadTimeoutHandler(2)) .addHandlerLast(new WriteTimeoutHandler(2))); return webClientBuilder .defaultHeader(HttpHeaders.USER_AGENT, "SAMPLE_APP") .clientConnector(new ReactorClientHttpConnector(httpClient)) .build(); } } |
Any Java class which makes HTTP calls can now inject thisWebClient
and use it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | @Service public class RandomUserClient { private final WebClient webClient; public RandomUserClient(WebClient webClient) { this.webClient = webClient; } public JsonNode getRandomUserById(int id) { return webClient .get() .uri("https://jsonplaceholder.typicode.com/todos/{id}", id) .retrieve() .bodyToMono(JsonNode.class) .block(); } } |
Another important thing to note is the construction of the URI for your HTTP call.
As we usually want the templated URI string like "/todos/{id}"
for reporting and not multiple metrics e.g."/todos/1337"
or "/todos/42"
. The WebClient
offers several ways to construct the URI (overloaded .uri(...)
), which you can all use, except one. There is one version that takes Function<UriBuilder,URI>
as an argument and can be used like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public JsonNode getRandomUserById(int id) { return webClient .get() .uri(uriBuilder -> // this does **not** report the URI template uriBuilder .scheme("https") .host("jsonplaceholder.typicode.com") .path("/todos/{id}") .build(id) ) .retrieve() .bodyToMono(JsonNode.class) .block(); } |
With this solution the WebClient
doesn't know the URI template origin, as it gets passed the final URI. Using this approach, we would get metrics for each different invocation like:
1 2 3 4 | "/todos/2", "/todos/1", "/todos/4", "/todos/3" |
Assuming we want URI templates within your reporting "/todos/{id}"
, use any URI construction, except the one which uses Function<UriBuilder,URI>
.
Accessing WebClient Metrics with Spring Boot Actuator
Finally, we can expose the metrics
endpoint for Spring Boot Actuator for a quick investigation in the browser with the following configuration inside our application.properties
file:
1 | management.endpoints.web.exposure.include=health, info, metrics |
Once we start the application, let all API calls finish and then access http://localhost:8080/actuator/metrics/http.client.requests
, we get the following output:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | { "name": "http.client.requests", "description": "Timer of WebClient operation", "baseUnit": "seconds", "measurements": [ { "statistic": "COUNT", "value": 10 }, { "statistic": "TOTAL_TIME", "value": 1.858713178 }, { "statistic": "MAX", "value": 0.825347033 } ], "availableTags": [ { "tag": "method", "values": [ "GET" ] }, { "tag": "clientName", "values": [ "jsonplaceholder.typicode.com", "quotes.rest" ] }, { "tag": "uri", "values": [ "/qod", "/todos/{id}" ] }, { "tag": "outcome", "values": [ "CLIENT_ERROR", "SUCCESS" ] }, { "tag": "status", "values": [ "404", "200" ] } ] } |
This is a first glance of what we get out of the box: counting, timing & status codes.
Spring Boot Actuator provides different tags (clientName
, uri
, outcome
, status
) to further drill down in our monitoring system. Given these tags, we can create dashboards to visualize the response times and status codes per different clients over time.
We usually don't expose this endpoint for applications running in production and rather configure Spring Boot Actuator to export the metrics to a compatible monitoring system (e.g., Datadog or Prometheus).
For more content on Spring's WebClient, have a look at the following blog posts:
- Spring WebTestClient for efficient testing of your REST API
- Use Spring WebClient for RESTful communication
- Spring WebClient OAuth2 Integration for Spring WebFlux
- Spring WebClient OAuth2 Integration for Spring Web (Servlet)
You can find the source code for this sample Spring Boot application on GitHub.
Have fun accessing Spring WebClient metrics,
Phil