From 1ef0098ab58ee12677da1bf9d42ce03331ca3226 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 27 Apr 2018 13:54:14 +0200 Subject: [PATCH] Add WebClient metrics support This commit adds support for Actuator Metrics for WebClient. This support mirrors the current behavior for `RestTemplate`, reusing the same metric name `"http.client.requests"` and tags. `WebClient` is instrumented by a `MetricsWebClientFilterFunction` which is applied by a `WebClientCustomizer`. This instrumentation happens automatically only if you create an instance of `WebClient` using an auto-configured `WebClient.Builder` bean. This infrastructure is reusing de facto the `MeterFilter` that has been added for `RestTemplate` in order to limit the "uri" tag cardinality. Closes gh-12228 --- .../RestTemplateMetricsAutoConfiguration.java | 4 +- .../WebClientMetricsAutoConfiguration.java | 72 ++++++++++ .../WebFluxMetricsAutoConfiguration.java | 4 +- .../main/resources/META-INF/spring.factories | 1 + .../metrics/test/MetricsRun.java | 2 + ...TemplateMetricsAutoConfigurationTests.java | 4 +- ...ebClientMetricsAutoConfigurationTests.java | 124 ++++++++++++++++++ .../WebFluxMetricsAutoConfigurationTests.java | 78 +++++++++++ .../DefaultWebClientExchangeTagsProvider.java | 50 +++++++ .../client/MetricsWebClientCustomizer.java | 57 ++++++++ .../MetricsWebClientFilterFunction.java | 69 ++++++++++ .../client/WebClientExchangeTags.java | 107 +++++++++++++++ .../client/WebClientExchangeTagsProvider.java | 42 ++++++ .../web/reactive/client/package-info.java | 21 +++ .../reactive/server/WebFluxTagsProvider.java | 4 +- ...ultWebClientExchangeTagsProviderTests.java | 99 ++++++++++++++ .../MetricsWebClientCustomizerTests.java | 67 ++++++++++ .../MetricsWebClientFilterFunctionTests.java | 119 +++++++++++++++++ .../client/WebClientExchangeTagsTests.java | 114 ++++++++++++++++ .../asciidoc/production-ready-features.adoc | 25 ++-- 20 files changed, 1047 insertions(+), 16 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebClientMetricsAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebClientMetricsAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProvider.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientCustomizer.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunction.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTags.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsProvider.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/package-info.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProviderTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientCustomizerTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunctionTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/RestTemplateMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/RestTemplateMetricsAutoConfiguration.java index 7ec4df10dea..67226197121 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/RestTemplateMetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/RestTemplateMetricsAutoConfiguration.java @@ -74,7 +74,7 @@ public class RestTemplateMetricsAutoConfiguration { @Bean @Order(0) - public MeterFilter metricsWebClientUriTagFilter(MetricsProperties properties) { + public MeterFilter metricsHttpClientUriTagFilter(MetricsProperties properties) { String metricName = properties.getWeb().getClient().getRequestsMetricName(); MeterFilter denyFilter = new MaximumUriTagsReachedMeterFilter(metricName); return MeterFilter.maximumAllowableTags(metricName, "uri", @@ -109,7 +109,7 @@ public class RestTemplateMetricsAutoConfiguration { if (this.logger.isWarnEnabled()) { this.logger.warn( "Reached the maximum number of URI tags for '" + this.metricName - + "'. Are you using uriVariables on RestTemplate calls?"); + + "'. Are you using uriVariables on HTTP client calls?"); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebClientMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebClientMetricsAutoConfiguration.java new file mode 100644 index 00000000000..dc7d24a8077 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebClientMetricsAutoConfiguration.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.web.reactive; + +import io.micrometer.core.instrument.MeterRegistry; + +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties; +import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.web.client.RestTemplateMetricsAutoConfiguration; +import org.springframework.boot.actuate.metrics.web.reactive.client.DefaultWebClientExchangeTagsProvider; +import org.springframework.boot.actuate.metrics.web.reactive.client.MetricsWebClientCustomizer; +import org.springframework.boot.actuate.metrics.web.reactive.client.WebClientExchangeTagsProvider; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for instrumentation + * of {@link org.springframework.web.reactive.function.client.WebClient}. + * + *

This is reusing the {@link io.micrometer.core.instrument.config.MeterFilter} + * defined in {@link RestTemplateMetricsAutoConfiguration} for limiting the + * cardinality of "uri" tags. + * + * @author Brian Clozel + * @since 2.1.0 + */ +@Configuration +@ConditionalOnClass(WebClient.class) +@AutoConfigureAfter({MetricsAutoConfiguration.class, + SimpleMetricsExportAutoConfiguration.class}) +@AutoConfigureBefore(WebClientAutoConfiguration.class) +@ConditionalOnBean(MeterRegistry.class) +public class WebClientMetricsAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public WebClientExchangeTagsProvider defaultWebClientExchangeTagsProvider() { + return new DefaultWebClientExchangeTagsProvider(); + } + + @Bean + public MetricsWebClientCustomizer metricsWebClientCustomizer(MeterRegistry meterRegistry, + WebClientExchangeTagsProvider tagsProvider, + MetricsProperties properties) { + return new MetricsWebClientCustomizer(meterRegistry, tagsProvider, + properties.getWeb().getClient().getRequestsMetricName()); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfiguration.java index 6f619440d97..86d957096b6 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfiguration.java @@ -33,8 +33,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** - * {@link EnableAutoConfiguration Auto-configuration} for instrumentation of Spring - * WebFlux MVC annotation-based programming model request mappings. + * {@link EnableAutoConfiguration Auto-configuration} for instrumentation + * of Spring WebFlux applications. * * @author Jon Schneider * @since 2.0.0 diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories index f5d7a66caf4..48a19de9797 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories @@ -52,6 +52,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.export.wavefront.Wavefron org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.orm.jpa.HibernateMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.web.client.RestTemplateMetricsAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.metrics.web.reactive.WebClientMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.web.reactive.WebFluxMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.web.servlet.WebMvcMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.web.tomcat.TomcatMetricsAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/test/MetricsRun.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/test/MetricsRun.java index fa35d78a8ae..dfd8e7bf8a2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/test/MetricsRun.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/test/MetricsRun.java @@ -39,6 +39,7 @@ import org.springframework.boot.actuate.autoconfigure.metrics.export.statsd.Stat import org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.orm.jpa.HibernateMetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.web.client.RestTemplateMetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.web.reactive.WebClientMetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.web.reactive.WebFluxMetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.web.servlet.WebMvcMetricsAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -78,6 +79,7 @@ public final class MetricsRun { DataSourcePoolMetricsAutoConfiguration.class, HibernateMetricsAutoConfiguration.class, RestTemplateMetricsAutoConfiguration.class, + WebClientMetricsAutoConfiguration.class, WebFluxMetricsAutoConfiguration.class, WebMvcMetricsAutoConfiguration.class); private MetricsRun() { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/RestTemplateMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/RestTemplateMetricsAutoConfigurationTests.java index 19a90db871e..0bd8cb5b336 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/RestTemplateMetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/RestTemplateMetricsAutoConfigurationTests.java @@ -75,7 +75,9 @@ public class RestTemplateMetricsAutoConfigurationTests { @Test public void afterMaxUrisReachedFurtherUrisAreDenied() { - this.contextRunner.run((context) -> { + this.contextRunner + .withPropertyValues("management.metrics.web.client.max-uri-tags=10") + .run((context) -> { MetricsProperties properties = context.getBean(MetricsProperties.class); int maxUriTags = properties.getWeb().getClient().getMaxUriTags(); MeterRegistry registry = context.getBean(MeterRegistry.class); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebClientMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebClientMetricsAutoConfigurationTests.java new file mode 100644 index 00000000000..b02843bb851 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebClientMetricsAutoConfigurationTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.web.reactive; + +import io.micrometer.core.instrument.MeterRegistry; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import reactor.core.publisher.Mono; + +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties; +import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; +import org.springframework.boot.actuate.metrics.web.reactive.client.WebClientExchangeTagsProvider; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.rule.OutputCapture; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.mock.http.client.reactive.MockClientHttpResponse; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link WebClientMetricsAutoConfiguration} + * + * @author Brian Clozel + */ +public class WebClientMetricsAutoConfigurationTests { + + private ApplicationContextRunner contextRunner = + new ApplicationContextRunner().with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(WebClientAutoConfiguration.class)); + + private ClientHttpConnector connector; + + @Rule + public OutputCapture out = new OutputCapture(); + + @Before + public void setup() { + this.connector = mock(ClientHttpConnector.class); + given(this.connector.connect(any(), any(), any())) + .willReturn(Mono.just(new MockClientHttpResponse(HttpStatus.OK))); + } + + @Test + public void webClientCreatedWithBuilderIsInstrumented() { + this.contextRunner.run((context) -> { + WebClient.Builder builder = context.getBean(WebClient.Builder.class); + WebClient webClient = builder.clientConnector(this.connector).build(); + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("http.client.requests").meter()).isNull(); + ClientResponse response = webClient.get() + .uri("http://example.org/projects/{project}", "spring-boot") + .exchange().block(); + assertThat(registry.find("http.client.requests") + .tags("uri", "/projects/{project}").meter()).isNotNull(); + }); + } + + @Test + public void shouldNotOverrideCustomTagsProvider() { + this.contextRunner.withUserConfiguration(CustomTagsProviderConfig.class) + .run((context) -> { + assertThat(context) + .getBeans(WebClientExchangeTagsProvider.class) + .hasSize(1).containsKey("customTagProvider"); + }); + } + + @Test + public void afterMaxUrisReachedFurtherUrisAreDenied() { + this.contextRunner + .withPropertyValues("management.metrics.web.client.max-uri-tags=10") + .run((context) -> { + WebClient.Builder builder = context.getBean(WebClient.Builder.class); + WebClient webClient = builder.clientConnector(this.connector).build(); + MetricsProperties properties = context.getBean(MetricsProperties.class); + int maxUriTags = properties.getWeb().getClient().getMaxUriTags(); + MeterRegistry registry = context.getBean(MeterRegistry.class); + for (int i = 0; i < maxUriTags + 10; i++) { + webClient.get() + .uri("http://example.org/projects/" + i) + .exchange().block(); + } + assertThat(registry.get("http.client.requests").meters()).hasSize(maxUriTags); + assertThat(this.out.toString()) + .contains("Reached the maximum number of URI tags " + + "for 'http.client.requests'"); + }); + } + + @Configuration + protected static class CustomTagsProviderConfig { + + @Bean + public WebClientExchangeTagsProvider customTagProvider() { + return mock(WebClientExchangeTagsProvider.class); + } + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfigurationTests.java new file mode 100644 index 00000000000..842b01fba70 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfigurationTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.autoconfigure.metrics.web.reactive; + +import org.junit.Test; + +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; +import org.springframework.boot.actuate.metrics.web.reactive.server.DefaultWebFluxTagsProvider; +import org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter; +import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsProvider; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link WebFluxMetricsAutoConfiguration} + * + * @author Brian Clozel + */ +public class WebFluxMetricsAutoConfigurationTests { + + private ReactiveWebApplicationContextRunner contextRunner = + new ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of( + MetricsAutoConfiguration.class, + SimpleMetricsExportAutoConfiguration.class, + WebFluxMetricsAutoConfiguration.class)); + + @Test + public void shouldProvideWebFluxMetricsBeans() { + this.contextRunner + .run((context) -> { + assertThat(context) + .getBeans(MetricsWebFilter.class).hasSize(1); + assertThat(context) + .getBeans(DefaultWebFluxTagsProvider.class).hasSize(1); + }); + } + + @Test + public void shouldNotOverrideCustomTagsProvider() { + this.contextRunner + .withUserConfiguration(CustomWebFluxTagsProviderConfig.class) + .run((context) -> { + assertThat(context) + .getBeans(WebFluxTagsProvider.class) + .hasSize(1).containsKey("customWebFluxTagsProvider"); + }); + } + + @Configuration + protected static class CustomWebFluxTagsProviderConfig { + + @Bean + public WebFluxTagsProvider customWebFluxTagsProvider() { + return mock(WebFluxTagsProvider.class); + } + } +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProvider.java new file mode 100644 index 00000000000..b0ad69866ba --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.web.reactive.client; + +import java.util.Arrays; + +import io.micrometer.core.instrument.Tag; + +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; + +/** + * Default implementation of {@link WebClientExchangeTagsProvider}. + * + * @author Brian Clozel + * @since 2.1.0 + */ +public class DefaultWebClientExchangeTagsProvider + implements WebClientExchangeTagsProvider { + + @Override + public Iterable tags(ClientRequest request, ClientResponse response, + Throwable throwable) { + Tag method = WebClientExchangeTags.method(request); + Tag uri = WebClientExchangeTags.uri(request); + Tag clientName = WebClientExchangeTags.clientName(request); + if (response != null) { + return Arrays.asList(method, uri, clientName, + WebClientExchangeTags.status(response)); + } + else { + return Arrays.asList(method, uri, clientName, + WebClientExchangeTags.status(throwable)); + } + } +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientCustomizer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientCustomizer.java new file mode 100644 index 00000000000..65dc4996613 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientCustomizer.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.web.reactive.client; + +import io.micrometer.core.instrument.MeterRegistry; + +import org.springframework.boot.web.reactive.function.client.WebClientCustomizer; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * {@link WebClientCustomizer} that configures the {@link WebClient} + * to record request metrics. + * + * @author Brian Clozel + * @since 2.1.0 + */ +public class MetricsWebClientCustomizer implements WebClientCustomizer { + + private final MetricsWebClientFilterFunction filterFunction; + + /** + * Create a new {@code MetricsWebClientFilterFunction} that will record + * metrics using the given {@code meterRegistry} with tags provided by the + * given {@code tagProvider}. + * @param meterRegistry the meter registry + * @param tagProvider the tag provider + * @param metricName the name of the recorded metric + */ + public MetricsWebClientCustomizer(MeterRegistry meterRegistry, + WebClientExchangeTagsProvider tagProvider, String metricName) { + this.filterFunction = new MetricsWebClientFilterFunction(meterRegistry, + tagProvider, metricName); + } + + @Override + public void customize(WebClient.Builder webClientBuilder) { + webClientBuilder.filters(filterFunctions -> { + if (!filterFunctions.contains(this.filterFunction)) { + filterFunctions.add(0, this.filterFunction); + } + }); + } +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunction.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunction.java new file mode 100644 index 00000000000..4946f46186c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunction.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.web.reactive.client; + +import java.util.concurrent.TimeUnit; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Timer; +import reactor.core.publisher.Mono; + +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ExchangeFunction; + +/** + * {@link ExchangeFilterFunction} applied via a + * {@link MetricsWebClientCustomizer} to record metrics. + * + * @author Brian Clozel + * @since 2.1.0 + */ +public class MetricsWebClientFilterFunction implements ExchangeFilterFunction { + + private final MeterRegistry meterRegistry; + + private final WebClientExchangeTagsProvider tagProvider; + + private final String metricName; + + public MetricsWebClientFilterFunction(MeterRegistry meterRegistry, + WebClientExchangeTagsProvider tagProvider, String metricName) { + this.meterRegistry = meterRegistry; + this.tagProvider = tagProvider; + this.metricName = metricName; + } + + @Override + public Mono filter(ClientRequest clientRequest, + ExchangeFunction exchangeFunction) { + long startTime = System.nanoTime(); + return exchangeFunction.exchange(clientRequest) + .doOnSuccessOrError((clientResponse, throwable) -> { + Iterable tags = this.tagProvider.tags(clientRequest, + clientResponse, throwable); + Timer.builder(this.metricName) + .tags(tags) + .description("Timer of WebClient operation") + .register(this.meterRegistry) + .record(System.nanoTime() - startTime, TimeUnit.NANOSECONDS); + }); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTags.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTags.java new file mode 100644 index 00000000000..8987632c99b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTags.java @@ -0,0 +1,107 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.web.reactive.client; + +import java.io.IOException; + +import io.micrometer.core.instrument.Tag; + +import org.springframework.http.client.reactive.ClientHttpRequest; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * Factory methods for creating {@link Tag Tags} related to a request-response exchange + * performed by a {@link WebClient}. + * + * @author Brian Clozel + * @since 2.1.0 + */ +public final class WebClientExchangeTags { + + private static final String URI_TEMPLATE_ATTRIBUTE = WebClient.class.getName() + ".uriTemplate"; + + private static final Tag IO_ERROR = Tag.of("status", "IO_ERROR"); + + private static final Tag CLIENT_ERROR = Tag.of("status", "CLIENT_ERROR"); + + private WebClientExchangeTags() { + } + + /** + * Creates a {@code method} {@code Tag} for the + * {@link ClientHttpRequest#getMethod() method} of the given {@code request}. + * @param request the request + * @return the method tag + */ + public static Tag method(ClientRequest request) { + return Tag.of("method", request.method().name()); + } + + /** + * Creates a {@code uri} {@code Tag} for the URI path of the given {@code request}. + * @param request the request + * @return the uri tag + */ + public static Tag uri(ClientRequest request) { + String uri = (String) request.attribute(URI_TEMPLATE_ATTRIBUTE) + .orElseGet(() -> request.url().getPath()); + return Tag.of("uri", extractPath(uri)); + } + + private static String extractPath(String url) { + String path = url.replaceFirst("^https?://[^/]+/", ""); + return path.startsWith("/") ? path : "/" + path; + } + + /** + * Creates a {@code status} {@code Tag} derived from the + * {@link ClientResponse#statusCode()} of the given {@code response}. + * @param response the response + * @return the status tag + */ + public static Tag status(ClientResponse response) { + return Tag.of("status", response.statusCode().toString()); + } + + /** + * Creates a {@code status} {@code Tag} derived from the + * exception thrown by the client. + * @param throwable the exception + * @return the status tag + */ + public static Tag status(Throwable throwable) { + return throwable instanceof IOException ? IO_ERROR : CLIENT_ERROR; + } + + /** + * Create a {@code clientName} {@code Tag} derived from + * the {@link java.net.URI#getHost host} + * of the {@link ClientRequest#url() URL} of the given {@code request}. + * @param request the request + * @return the clientName tag + */ + public static Tag clientName(ClientRequest request) { + String host = request.url().getHost(); + if (host == null) { + host = "none"; + } + return Tag.of("clientName", host); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsProvider.java new file mode 100644 index 00000000000..7444c332f95 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsProvider.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.web.reactive.client; + +import io.micrometer.core.instrument.Tag; + +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; + +/** + * {@link Tag Tags} provider for an exchange performed by a + * {@link org.springframework.web.reactive.function.client.WebClient}. + * + * @author Brian Clozel + * @since 2.1.0 + */ +@FunctionalInterface +public interface WebClientExchangeTagsProvider { + + /** + * Provide tags to be associated with metrics for the client exchange. + * @param request the client request + * @param response the server response (may be {@code null}) + * @param throwable the exception (may be {@code null}) + * @return tags to associate with metrics for the request and response exchange + */ + Iterable tags(ClientRequest request, ClientResponse response, Throwable throwable); +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/package-info.java new file mode 100644 index 00000000000..74592677574 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Actuator support for + * {@link org.springframework.web.reactive.function.client.WebClient} metrics. + */ +package org.springframework.boot.actuate.metrics.web.reactive.client; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsProvider.java index 94299169668..1a62d49f481 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsProvider.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ public interface WebFluxTagsProvider { /** * Provides tags to be associated with metrics for the given {@code exchange}. * @param exchange the exchange - * @param ex the current exception (may be {@code null} + * @param ex the current exception (may be {@code null}) * @return tags to associate with metrics for the request and response exchange */ Iterable httpRequestTags(ServerWebExchange exchange, Throwable ex); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProviderTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProviderTests.java new file mode 100644 index 00000000000..6c43347295b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProviderTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.web.reactive.client; + + +import java.io.IOException; +import java.net.URI; + +import io.micrometer.core.instrument.Tag; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link DefaultWebClientExchangeTagsProvider} + * + * @author Brian Clozel + */ +public class DefaultWebClientExchangeTagsProviderTests { + + private static final String URI_TEMPLATE_ATTRIBUTE = WebClient.class.getName() + ".uriTemplate"; + + private WebClientExchangeTagsProvider tagsProvider = new DefaultWebClientExchangeTagsProvider(); + + private ClientRequest request; + + private ClientResponse response; + + @Before + public void setup() { + this.request = ClientRequest + .create(HttpMethod.GET, URI.create("http://example.org/projects/spring-boot")) + .attribute(URI_TEMPLATE_ATTRIBUTE, "http://example.org/projects/{project}") + .build(); + this.response = mock(ClientResponse.class); + given(this.response.statusCode()).willReturn(HttpStatus.OK); + } + + @Test + public void tagsShouldBePopulated() { + Iterable tags = this.tagsProvider.tags(this.request, this.response, null); + assertThat(tags).containsExactlyInAnyOrder( + Tag.of("method", "GET"), Tag.of("uri", "/projects/{project}"), + Tag.of("clientName", "example.org"), Tag.of("status", "200")); + } + + @Test + public void tagsWhenNoUriTemplateShouldProvideUriPath() { + ClientRequest request = ClientRequest + .create(HttpMethod.GET, URI.create("http://example.org/projects/spring-boot")) + .build(); + Iterable tags = this.tagsProvider.tags(request, this.response, null); + assertThat(tags).containsExactlyInAnyOrder( + Tag.of("method", "GET"), Tag.of("uri", "/projects/spring-boot"), + Tag.of("clientName", "example.org"), Tag.of("status", "200")); + } + + @Test + public void tagsWhenIoExceptionShouldReturnIoErrorStatus() { + Iterable tags = this.tagsProvider.tags(this.request, + null, new IOException()); + assertThat(tags).containsExactlyInAnyOrder( + Tag.of("method", "GET"), Tag.of("uri", "/projects/{project}"), + Tag.of("clientName", "example.org"), Tag.of("status", "IO_ERROR")); + } + + @Test + public void tagsWhenExceptionShouldReturnClientErrorStatus() { + Iterable tags = this.tagsProvider.tags(this.request, + null, new IllegalArgumentException()); + assertThat(tags).containsExactlyInAnyOrder( + Tag.of("method", "GET"), Tag.of("uri", "/projects/{project}"), + Tag.of("clientName", "example.org"), Tag.of("status", "CLIENT_ERROR")); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientCustomizerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientCustomizerTests.java new file mode 100644 index 00000000000..0adb320c4dd --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientCustomizerTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.web.reactive.client; + +import io.micrometer.core.instrument.MeterRegistry; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link MetricsWebClientCustomizer} + * + * @author Brian Clozel + */ +public class MetricsWebClientCustomizerTests { + + private MetricsWebClientCustomizer customizer; + + private WebClient.Builder clientBuilder; + + @Before + public void setup() { + this.customizer = new MetricsWebClientCustomizer(mock(MeterRegistry.class), + mock(WebClientExchangeTagsProvider.class), "test"); + this.clientBuilder = WebClient.builder(); + } + + @Test + public void customizeShouldAddFilterFunction() { + this.clientBuilder.filter(mock(ExchangeFilterFunction.class)); + this.customizer.customize(this.clientBuilder); + this.clientBuilder.filters(filters -> + assertThat(filters) + .hasSize(2) + .first().isInstanceOf(MetricsWebClientFilterFunction.class)); + } + + @Test + public void customizeShouldNotAddDuplicateFilterFunction() { + this.customizer.customize(this.clientBuilder); + this.clientBuilder.filters(filters -> assertThat(filters).hasSize(1)); + this.customizer.customize(this.clientBuilder); + this.clientBuilder.filters(filters -> + assertThat(filters) + .hasSize(1) + .first().isInstanceOf(MetricsWebClientFilterFunction.class)); + } +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunctionTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunctionTests.java new file mode 100644 index 00000000000..2f4d7b95136 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunctionTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.web.reactive.client; + +import java.io.IOException; +import java.net.URI; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.MockClock; +import io.micrometer.core.instrument.simple.SimpleConfig; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; + +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFunction; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + + +/** + * Tests for {@link MetricsWebClientFilterFunction} + * + * @author Brian Clozel + */ +public class MetricsWebClientFilterFunctionTests { + + private static final String URI_TEMPLATE_ATTRIBUTE = WebClient.class.getName() + ".uriTemplate"; + + private MeterRegistry registry; + + private MetricsWebClientFilterFunction filterFunction; + + private ClientResponse response; + + private ExchangeFunction exchange; + + @Before + public void setup() { + this.registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock()); + this.filterFunction = new MetricsWebClientFilterFunction(this.registry, + new DefaultWebClientExchangeTagsProvider(), "http.client.requests"); + this.response = mock(ClientResponse.class); + this.exchange = r -> Mono.just(this.response); + } + + @Test + public void filterShouldRecordTimer() { + ClientRequest request = ClientRequest + .create(HttpMethod.GET, URI.create("http://example.com/projects/spring-boot")) + .build(); + given(this.response.statusCode()).willReturn(HttpStatus.OK); + this.filterFunction.filter(request, this.exchange).block(); + assertThat(this.registry.get("http.client.requests") + .tags("method", "GET", "uri", "/projects/spring-boot", "status", "200").timer() + .count()).isEqualTo(1); + } + + @Test + public void filterWhenUriTemplatePresentShouldRecordTimer() { + ClientRequest request = ClientRequest + .create(HttpMethod.GET, URI.create("http://example.com/projects/spring-boot")) + .attribute(URI_TEMPLATE_ATTRIBUTE, "/projects/{project}") + .build(); + given(this.response.statusCode()).willReturn(HttpStatus.OK); + this.filterFunction.filter(request, this.exchange).block(); + assertThat(this.registry.get("http.client.requests") + .tags("method", "GET", "uri", "/projects/{project}", "status", "200").timer() + .count()).isEqualTo(1); + } + + @Test + public void filterWhenIoExceptionThrownShouldRecordTimer() { + ClientRequest request = ClientRequest + .create(HttpMethod.GET, URI.create("http://example.com/projects/spring-boot")) + .build(); + ExchangeFunction errorExchange = r -> Mono.error(new IOException()); + this.filterFunction.filter(request, errorExchange) + .onErrorResume(IOException.class, t -> Mono.empty()).block(); + assertThat(this.registry.get("http.client.requests") + .tags("method", "GET", "uri", "/projects/spring-boot", "status", "IO_ERROR").timer() + .count()).isEqualTo(1); + } + + @Test + public void filterWhenExceptionThrownShouldRecordTimer() { + ClientRequest request = ClientRequest + .create(HttpMethod.GET, URI.create("http://example.com/projects/spring-boot")) + .build(); + ExchangeFunction exchange = r -> Mono.error(new IllegalArgumentException()); + this.filterFunction.filter(request, exchange) + .onErrorResume(IllegalArgumentException.class, t -> Mono.empty()).block(); + assertThat(this.registry.get("http.client.requests") + .tags("method", "GET", "uri", "/projects/spring-boot", "status", "CLIENT_ERROR").timer() + .count()).isEqualTo(1); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsTests.java new file mode 100644 index 00000000000..f3dce2b13db --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.actuate.metrics.web.reactive.client; + +import java.io.IOException; +import java.net.URI; + +import io.micrometer.core.instrument.Tag; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link WebClientExchangeTags} + * + * @author Brian Clozel + */ +public class WebClientExchangeTagsTests { + + private static final String URI_TEMPLATE_ATTRIBUTE = WebClient.class.getName() + ".uriTemplate"; + + private ClientRequest request; + + private ClientResponse response; + + @Before + public void setup() { + this.request = ClientRequest + .create(HttpMethod.GET, URI.create("http://example.org/projects/spring-boot")) + .attribute(URI_TEMPLATE_ATTRIBUTE, "http://example.org/projects/{project}") + .build(); + this.response = mock(ClientResponse.class); + given(this.response.statusCode()).willReturn(HttpStatus.OK); + } + + @Test + public void method() { + assertThat(WebClientExchangeTags.method(this.request)) + .isEqualTo(Tag.of("method", "GET")); + } + + @Test + public void uriWhenAbsoluteTemplateIsAvailableShouldReturnTemplate() { + assertThat(WebClientExchangeTags.uri(this.request)) + .isEqualTo(Tag.of("uri", "/projects/{project}")); + } + + @Test + public void uriWhenRelativeTemplateIsAvailableShouldReturnTemplate() { + this.request = ClientRequest + .create(HttpMethod.GET, URI.create("http://example.org/projects/spring-boot")) + .attribute(URI_TEMPLATE_ATTRIBUTE, "/projects/{project}") + .build(); + assertThat(WebClientExchangeTags.uri(this.request)) + .isEqualTo(Tag.of("uri", "/projects/{project}")); + } + + @Test + public void uriWhenTemplateIsMissingShouldReturnPath() { + this.request = ClientRequest + .create(HttpMethod.GET, URI.create("http://example.org/projects/spring-boot")) + .build(); + assertThat(WebClientExchangeTags.uri(this.request)) + .isEqualTo(Tag.of("uri", "/projects/spring-boot")); + } + + @Test + public void clientName() { + assertThat(WebClientExchangeTags.clientName(this.request)) + .isEqualTo(Tag.of("clientName", "example.org")); + } + + @Test + public void status() { + assertThat(WebClientExchangeTags.status(this.response)) + .isEqualTo(Tag.of("status", "200")); + } + + @Test + public void statusWhenIOException() { + assertThat(WebClientExchangeTags.status(new IOException())) + .isEqualTo(Tag.of("status", "IO_ERROR")); + } + + @Test + public void statusWhenClientException() { + assertThat(WebClientExchangeTags.status(new IllegalArgumentException())) + .isEqualTo(Tag.of("status", "CLIENT_ERROR")); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc index bf290b56199..5ec53b50310 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -1713,16 +1713,22 @@ To customize the tags, provide a `@Bean` that implements `WebFluxTagsProvider`. -[[production-ready-metrics-rest-template]] -==== RestTemplate Metrics -The instrumentation of any `RestTemplate` created using the auto-configured -`RestTemplateBuilder` is enabled. It is also possible to apply -`MetricsRestTemplateCustomizer` manually. +[[production-ready-metrics-http-clients]] +==== HTTP Client Metrics +Spring Boot Actuator manages the instrumentation of both `RestTemplate` and `WebClient`. +For that, you have to get injected with an auto-configured builder +and use it to create instances: + +* `RestTemplateBuilder` for `RestTemplate` +* `WebClient.Builder` for `WebClient` + +It is also possible to apply manually the customizers responsible for this instrumentation, +namely `MetricsRestTemplateCustomizer` and `MetricsWebClientCustomizer`. By default, metrics are generated with the name, `http.client.requests`. The name can be customized by setting the `management.metrics.web.client.requests-metric-name` property. -By default, metrics generated by an instrumented `RestTemplate` are tagged with the +By default, metrics generated by an instrumented client are tagged with the following information: * `method`, the request's method (for example, `GET` or `POST`). @@ -1731,9 +1737,10 @@ example, `/api/person/{id}`). * `status`, the response's HTTP status code (for example, `200` or `500`). * `clientName`, the host portion of the URI. -To customize the tags, provide a `@Bean` that implements -`RestTemplateExchangeTagsProvider`. There are convenience static functions in -`RestTemplateExchangeTags`. +To customize the tags, and depending on your choice of client, you can provide +a `@Bean` that implements `RestTemplateExchangeTagsProvider` or +`WebClientExchangeTagsProvider`. There are convenience static functions in +`RestTemplateExchangeTags` and `WebClientExchangeTags`.