springspring-webfluxspring-boot-actuatorspring-webclientspring-micrometer

Add tags for Spring webclient default metrics


I'm currently working on a Spring webflux project that has Actuator, Micrometer dependencies as follows,

    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-core</artifactId>
    </dependency>

to expose default metrics (that include Spring webClient metrics). I'm having 4 different endpoints that I call using the Spring WebClient. I was wondering if there was a way to add specific tags to each of those webclient calls that get added to the default metrics. I have some metrics like Histogram from Webclient exposed at /actuator/prometheus endpoint as follows,

http_client_requests_seconds_bucket{clientName="my-app.com",method="GET",outcome="SUCCESS",status="200",uri="/shops",le="0.001048576",} 0.0
http_client_requests_seconds_bucket{clientName="my-app.com",method="GET",outcome="SUCCESS",status="200",uri="/shops",le="0.002088576",} 1.0

In my code I would like to add few extra tags in the Webclient call that I want for all the metrics above. For example something like this,

http_client_requests_seconds_bucket{clientName="my-app.com",method="GET",outcome="SUCCESS",status="200",uri="/shops",le="0.001048576",investor="A", version="v1"} 0.0
http_client_requests_seconds_bucket{clientName="my-app.com",method="GET",outcome="SUCCESS",status="200",uri="/shops",le="0.002088576",investor="A", version="v1"} 1.0

Notice the 2 custom tags I added investor="A", version="v1". I'm looking for some code that may look like this,

@Autowire
private WebClient webclient; // Assume there is already a bean created for us

public Mono<String> getShopsList(String... extraTags) {
     return webclient.baseUrl("http://my-app.com")
         .build()
         .get()
         .uri("/shops")
         .tags(extraTags) // Some extra tags I want callers of the method to pass. Note there are only 4-5 methods that call "getShopsList()" method
         .retrieve() 
         .bodyToMono(String.class);
 }
 

Can someone please help about the best way possible to achieve this?


Solution

  • The expected way to do that is by introducing your custom tags provider:

    @Component
    public class CustomWebClientExchangeTagsProvider extends DefaultWebClientExchangeTagsProvider {
    
      public static final String VERSION_ATTRIBUTE = "custom.webclient.version";
      public static final String INVESTOR_ATTRIBUTE = "custom.webclient.investor";
    
      @Override
      public Iterable<Tag> tags(ClientRequest request, ClientResponse response, Throwable throwable) {
        Tag method = WebClientExchangeTags.method(request);
        Tag investor = getInvestorTag(request);
        Tag version = getVersionTag(request);
        return asList(method, investor, version, WebClientExchangeTags.status(response, throwable), WebClientExchangeTags.outcome(response));
      }
    
      private Tag getInvestorTag(ClientRequest request) {
        return request.attribute(INVESTOR_ATTRIBUTE)
            .map(name -> Tag.of("investor", (String) name))
            .orElse(WebClientExchangeTags.clientName(request));
      }
    
      private Tag getVersionTag(ClientRequest request) {
        return request.attribute(VERSION_ATTRIBUTE)
            .map(uri -> Tag.of("version", (String) uri))
            .orElse(WebClientExchangeTags.uri(request));
      }
    
    }
    

    You have to instrument your custom web client this way:

    @Bean
    public WebClient webClient(MetricsWebClientCustomizer metricsCustomizer) {
        TcpClient timeoutClient = ...
        WebClient.Builder builder = WebClient.builder();
        metricsCustomizer.customize(builder);
        return ...;
    }
    

    Finally, you need to set the two attributes like this:

    return webClient.get()
            .uri(filePath)
            .attribute(INVESTOR_ATTRIBUTE, "A")
            .attribute(VERSION_ATTRIBUTE, "v1")
            .retrieve()
            .bodyToMono(String.class);
    

    Sample result:

    http_client_requests_seconds_count{investor="A",method="GET",outcome="CLIENT_ERROR",status="401",version="v1",} 1.0
    http_client_requests_seconds_sum{investor="A",method="GET",outcome="CLIENT_ERROR",status="401",version="v1",} 0.073818807
    

    Edit

    According to the docs:

    S attributes(Consumer<Map<String,Object>> attributesConsumer)

    Provides access to every attribute declared so far with the possibility to add, replace, or remove values.

    So yes, you could use it to add multiple attributes.