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:
parent
621874708d
commit
1ef0098ab5
|
|
@ -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?");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,\
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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`.
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue