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
This commit is contained in:
Brian Clozel 2018-04-27 13:54:14 +02:00
parent 621874708d
commit 1ef0098ab5
20 changed files with 1047 additions and 16 deletions

View File

@ -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?");
}
}

View File

@ -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}.
*
* <p>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());
}
}

View File

@ -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

View File

@ -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,\

View File

@ -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() {

View File

@ -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);

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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<Tag> 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));
}
}
}

View File

@ -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);
}
});
}
}

View File

@ -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<ClientResponse> filter(ClientRequest clientRequest,
ExchangeFunction exchangeFunction) {
long startTime = System.nanoTime();
return exchangeFunction.exchange(clientRequest)
.doOnSuccessOrError((clientResponse, throwable) -> {
Iterable<Tag> 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);
});
}
}

View File

@ -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);
}
}

View File

@ -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<Tag> tags(ClientRequest request, ClientResponse response, Throwable throwable);
}

View File

@ -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;

View File

@ -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<Tag> httpRequestTags(ServerWebExchange exchange, Throwable ex);

View File

@ -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<Tag> 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<Tag> 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<Tag> 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<Tag> 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"));
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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"));
}
}

View File

@ -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`.