Controlling Spring WebClient metrics cardinality
Spring 5 introduced WebClient
which provides a “a reactive, non-blocking WebClient for HTTP requests.”
WebClient can be configured to work with metrics by adding a filter:
var metricsFilter = new MetricsWebClientFilterFunction(
meterRegistry,
new DefaultWebClientExchangeTagsProvider(),
"downstream-service",
AutoTimer.ENABLED
);WebClient webClient = WebClient.builder()
.baseUrl(baseUrl)
.filter(metricsFilter)
.build();
DefaultWebClientExchangeTagsProvider
auto-configures tags applied to each request metric including the HTTP method, client name and URI.
An example request:
webClient.get()
.uri(uriBuilder -> uriBuilder.path("/v1/users{userId}").build(1)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(new ParameterizedTypeReference<List<User>>(){});
The metric exposed by Prometheus looks like:
name_seconds_count{app="downstream-service", clientName="downstream-service.envoy.tw.ee",method="GET",outcome="SUCCESS",status="200",uri="/v1/users/1/",} 2.0
However the cardinality of the URI tag can explode the number of metrics recorded if the request includes many variations of path variables or queries over time — as a new metric will be created for each unique URI.
For example, a request for User 2 will create the metric:
metric_name_seconds_count{app="downstream-service", clientName="downstream-service.envoy.tw.ee",method="GET",outcome="SUCCESS",status="200",uri="/v1/users/2/",} 2.0
This can be particularly bad for bucketed metrics such as histograms.
You can prevent this by passing the request URI to the WebClient in the following way:
String path = "/v1/users/{userId}";
webClient
.get()
.uri(path, uriBuilder -> uriBuilder.queryParam("k","v").build(1);
Alternatively:
String path = "/v1/users/{userId}?k={v}";
webClient
.get()
.uri(path, 1, "v");
The resulting request URI is/v1/users/1?k=v
metric_name_seconds_count{app="downstream-service",clientName="downstream-service.envoy.tw.ee",method="GET",outcome="SUCCESS",status="200",uri="/v1/users/{userId}",} 2.0
Passing the URI in this way prevents the explosion of request metrics because the underlying WebClient.uri
method adds the URI template to the request as an attribute with the key:
WebClient.class.getName() + ".uriTemplate"
This key’s value (the URI template) is used by WebClientExchangeTags.uri(ClientRequest request)
when creating the tag for the metric (in the above example this is done by DefaultWebClientExchangeTagsProvider
) instead of the request URI.