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 7ec4df10de..6722619712 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 0000000000..dc7d24a807
--- /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 6f619440d9..86d957096b 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 f5d7a66caf..48a19de979 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 fa35d78a8a..dfd8e7bf8a 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 19a90db871..0bd8cb5b33 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 0000000000..b02843bb85
--- /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 0000000000..842b01fba7
--- /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 0000000000..b0ad69866b
--- /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 0000000000..65dc499661
--- /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 0000000000..4946f46186
--- /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 0000000000..8987632c99
--- /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 0000000000..7444c332f9
--- /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 0000000000..7459267757
--- /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 9429916966..1a62d49f48 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 0000000000..6c43347295
--- /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 0000000000..0adb320c4d
--- /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 0000000000..2f4d7b9513
--- /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 0000000000..f3dce2b13d
--- /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 bf290b5619..5ec53b5031 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`.