Use Observation infrastructure for instrumenting WebClient

As of spring-projects/spring-framework#28341, `WebClient` is
instrumented directly for `Observation`.
This commit removes the custom `ExchangeFilterFunction` that previously
instrumented the client for metrics.

As a result, the relevant tag providers are now deprecated and adapted
as `ObservationConvention` for the time being.

Closes gh-32518
pull/29403/merge
Brian Clozel 2 years ago
parent f0e40bb00d
commit 8b4a20d6cd

@ -149,16 +149,6 @@ public class MetricsProperties {
*/
private String metricName = "http.client.requests";
/**
* Auto-timed request settings.
*/
@NestedConfigurationProperty
private final AutoTimeProperties autotime = new AutoTimeProperties();
public AutoTimeProperties getAutotime() {
return this.autotime;
}
@DeprecatedConfigurationProperty(replacement = "management.observations.http.client.requests.name")
public String getMetricName() {
return this.metricName;

@ -0,0 +1,86 @@
/*
* Copyright 2012-2022 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
*
* https://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.client;
import io.micrometer.common.KeyValues;
import io.micrometer.core.instrument.Tag;
import io.micrometer.observation.Observation;
import org.springframework.boot.actuate.metrics.web.reactive.client.WebClientExchangeTagsProvider;
import org.springframework.web.reactive.function.client.ClientObservationContext;
import org.springframework.web.reactive.function.client.ClientObservationConvention;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.WebClient;
/**
* Adapter class that applies {@link WebClientExchangeTagsProvider} tags as a
* {@link ClientObservationConvention}.
*
* @author Brian Clozel
*/
@SuppressWarnings("deprecation")
class ClientObservationConventionAdapter implements ClientObservationConvention {
private static final String URI_TEMPLATE_ATTRIBUTE = WebClient.class.getName() + ".uriTemplate";
private final String metricName;
private final WebClientExchangeTagsProvider tagsProvider;
ClientObservationConventionAdapter(String metricName, WebClientExchangeTagsProvider tagsProvider) {
this.metricName = metricName;
this.tagsProvider = tagsProvider;
}
@Override
public boolean supportsContext(Observation.Context context) {
return context instanceof ClientObservationContext;
}
@Override
public KeyValues getLowCardinalityKeyValues(ClientObservationContext context) {
KeyValues keyValues = KeyValues.empty();
mutateClientRequest(context);
Iterable<Tag> tags = this.tagsProvider.tags(context.getCarrier(), context.getResponse(),
context.getError().orElse(null));
for (Tag tag : tags) {
keyValues = keyValues.and(tag.getKey(), tag.getValue());
}
return keyValues;
}
/*
* {@link WebClientExchangeTagsProvider} relies on a request attribute to get the URI
* template, we need to adapt to that.
*/
private static void mutateClientRequest(ClientObservationContext context) {
ClientRequest clientRequest = ClientRequest.from(context.getCarrier())
.attribute(URI_TEMPLATE_ATTRIBUTE, context.getUriTemplate()).build();
context.setCarrier(clientRequest);
}
@Override
public KeyValues getHighCardinalityKeyValues(ClientObservationContext context) {
return KeyValues.empty();
}
@Override
public String getName() {
return this.metricName;
}
}

@ -18,6 +18,8 @@ package org.springframework.boot.actuate.autoconfigure.metrics.web.client;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.config.MeterFilter;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties;
@ -30,33 +32,43 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration;
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.context.annotation.Import;
import org.springframework.core.annotation.Order;
/**
* {@link EnableAutoConfiguration Auto-configuration} for HTTP client-related metrics.
* {@link EnableAutoConfiguration Auto-configuration} for HTTP client-related
* observations.
*
* @author Jon Schneider
* @author Phillip Webb
* @author Stephane Nicoll
* @author Raheela Aslam
* @since 2.1.0
* @author Brian Clozel
* @since 3.0.0
*/
@AutoConfiguration(after = { ObservationAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class,
RestTemplateAutoConfiguration.class, WebClientAutoConfiguration.class })
@ConditionalOnClass(MeterRegistry.class)
@ConditionalOnBean(MeterRegistry.class)
@Import({ RestTemplateObservationConfiguration.class, WebClientMetricsConfiguration.class })
public class HttpClientMetricsAutoConfiguration {
@Bean
@Order(0)
public MeterFilter metricsHttpClientUriTagFilter(MetricsProperties properties) {
String metricName = properties.getWeb().getClient().getRequest().getMetricName();
MeterFilter denyFilter = new OnlyOnceLoggingDenyMeterFilter(() -> String
.format("Reached the maximum number of URI tags for '%s'. Are you using 'uriVariables'?", metricName));
return MeterFilter.maximumAllowableTags(metricName, "uri", properties.getWeb().getClient().getMaxUriTags(),
denyFilter);
@ConditionalOnClass(Observation.class)
@ConditionalOnBean(ObservationRegistry.class)
@Import({ RestTemplateObservationConfiguration.class, WebClientObservationConfiguration.class })
public class HttpClientObservationsAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(MeterRegistry.class)
@ConditionalOnBean(MeterRegistry.class)
class MeterFilterConfiguration {
@Bean
@Order(0)
MeterFilter metricsHttpClientUriTagFilter(MetricsProperties properties) {
String metricName = properties.getWeb().getClient().getRequest().getMetricName();
MeterFilter denyFilter = new OnlyOnceLoggingDenyMeterFilter(() -> String.format(
"Reached the maximum number of URI tags for '%s'. Are you using 'uriVariables'?", metricName));
return MeterFilter.maximumAllowableTags(metricName, "uri", properties.getWeb().getClient().getMaxUriTags(),
denyFilter);
}
}
}

@ -16,7 +16,6 @@
package org.springframework.boot.actuate.autoconfigure.metrics.web.client;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.beans.factory.ObjectProvider;
@ -39,8 +38,8 @@ import org.springframework.web.client.RestTemplate;
* @author Brian Clozel
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ RestTemplate.class, Observation.class })
@ConditionalOnBean({ RestTemplateBuilder.class, ObservationRegistry.class })
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(RestTemplateBuilder.class)
@SuppressWarnings("deprecation")
class RestTemplateObservationConfiguration {

@ -16,41 +16,42 @@
package org.springframework.boot.actuate.autoconfigure.metrics.web.client;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties;
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties.Web.Client.ClientRequest;
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.autoconfigure.observation.ObservationProperties;
import org.springframework.boot.actuate.metrics.web.reactive.client.ObservationWebClientCustomizer;
import org.springframework.boot.actuate.metrics.web.reactive.client.WebClientExchangeTagsProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.ClientObservationConvention;
import org.springframework.web.reactive.function.client.DefaultClientObservationConvention;
import org.springframework.web.reactive.function.client.WebClient;
/**
* Configure the instrumentation of {@link WebClient}.
*
* @author Brian Clozel
* @author Stephane Nicoll
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(WebClient.class)
class WebClientMetricsConfiguration {
@SuppressWarnings("deprecation")
class WebClientObservationConfiguration {
@Bean
@ConditionalOnMissingBean
WebClientExchangeTagsProvider defaultWebClientExchangeTagsProvider() {
return new DefaultWebClientExchangeTagsProvider();
}
@Bean
MetricsWebClientCustomizer metricsWebClientCustomizer(MeterRegistry meterRegistry,
WebClientExchangeTagsProvider tagsProvider, MetricsProperties properties) {
ClientRequest request = properties.getWeb().getClient().getRequest();
return new MetricsWebClientCustomizer(meterRegistry, tagsProvider, request.getMetricName(),
request.getAutotime());
ObservationWebClientCustomizer metricsWebClientCustomizer(ObservationRegistry observationRegistry,
ObservationProperties observationProperties,
ObjectProvider<WebClientExchangeTagsProvider> optionalTagsProvider, MetricsProperties metricsProperties) {
String metricName = metricsProperties.getWeb().getClient().getRequest().getMetricName();
String observationName = observationProperties.getHttp().getClient().getRequests().getName();
String name = (observationName != null) ? observationName : metricName;
WebClientExchangeTagsProvider tagsProvider = optionalTagsProvider.getIfAvailable();
ClientObservationConvention observationConvention = (tagsProvider != null)
? new ClientObservationConventionAdapter(name, tagsProvider)
: new DefaultClientObservationConvention(name);
return new ObservationWebClientCustomizer(observationRegistry, observationConvention);
}
}

@ -1905,16 +1905,28 @@
{
"name": "management.metrics.web.client.request.autotime.enabled",
"description": "Whether to automatically time web client requests.",
"defaultValue": true
"defaultValue": true,
"deprecation": {
"level": "error",
"reason": "Should be applied at the ObservationRegistry level."
}
},
{
"name": "management.metrics.web.client.request.autotime.percentiles",
"description": "Computed non-aggregable percentiles to publish."
"description": "Computed non-aggregable percentiles to publish.",
"deprecation": {
"level": "error",
"reason": "Should be applied at the ObservationRegistry level."
}
},
{
"name": "management.metrics.web.client.request.autotime.percentiles-histogram",
"description": "Whether percentile histograms should be published.",
"defaultValue": false
"defaultValue": false,
"deprecation": {
"level": "error",
"reason": "Should be applied at the ObservationRegistry level."
}
},
{
"name": "management.metrics.web.client.requests-metric-name",

@ -77,7 +77,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.r2dbc.ConnectionPoolMetri
org.springframework.boot.actuate.autoconfigure.metrics.redis.LettuceMetricsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.startup.StartupTimeMetricsListenerAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.task.TaskExecutorMetricsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.web.client.HttpClientMetricsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.web.client.HttpClientObservationsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.web.jetty.JettyMetricsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.web.reactive.WebFluxMetricsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.web.servlet.WebMvcMetricsAutoConfiguration

@ -41,7 +41,7 @@ import org.springframework.boot.actuate.autoconfigure.metrics.amqp.RabbitMetrics
import org.springframework.boot.actuate.autoconfigure.metrics.cache.CacheMetricsAutoConfiguration;
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.HttpClientMetricsAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.web.client.HttpClientObservationsAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.web.reactive.WebFluxMetricsAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.web.servlet.WebMvcMetricsAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
@ -140,7 +140,7 @@ class MetricsIntegrationTests {
JvmMetricsAutoConfiguration.class, LogbackMetricsAutoConfiguration.class,
SystemMetricsAutoConfiguration.class, RabbitMetricsAutoConfiguration.class,
CacheMetricsAutoConfiguration.class, DataSourcePoolMetricsAutoConfiguration.class,
HibernateMetricsAutoConfiguration.class, HttpClientMetricsAutoConfiguration.class,
HibernateMetricsAutoConfiguration.class, HttpClientObservationsAutoConfiguration.class,
WebFluxMetricsAutoConfiguration.class, WebMvcMetricsAutoConfiguration.class, JacksonAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, RestTemplateAutoConfiguration.class,
WebMvcAutoConfiguration.class, DispatcherServletAutoConfiguration.class,

@ -0,0 +1,89 @@
/*
* Copyright 2012-2022 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
*
* https://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.client;
import java.net.URI;
import io.micrometer.common.KeyValue;
import io.micrometer.observation.Observation;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.metrics.web.reactive.client.DefaultWebClientExchangeTagsProvider;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.function.client.ClientObservationContext;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ClientObservationConventionAdapter}.
*
* @author Brian Clozel
*/
@SuppressWarnings("deprecation")
class ClientObservationConventionAdapterTests {
private static final String TEST_METRIC_NAME = "test.metric.name";
private ClientObservationConventionAdapter convention = new ClientObservationConventionAdapter(TEST_METRIC_NAME,
new DefaultWebClientExchangeTagsProvider());
private ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/resource/test")).build();
private ClientResponse response = ClientResponse.create(HttpStatus.OK).body("foo").build();
private ClientObservationContext context;
@BeforeEach
void setup() {
this.context = new ClientObservationContext();
this.context.setCarrier(this.request);
this.context.setResponse(this.response);
this.context.setUriTemplate("/resource/{name}");
}
@Test
void shouldUseConfiguredName() {
assertThat(this.convention.getName()).isEqualTo(TEST_METRIC_NAME);
}
@Test
void shouldOnlySupportClientHttpObservationContext() {
assertThat(this.convention.supportsContext(this.context)).isTrue();
assertThat(this.convention.supportsContext(new OtherContext())).isFalse();
}
@Test
void shouldPushTagsAsLowCardinalityKeyValues() {
assertThat(this.convention.getLowCardinalityKeyValues(this.context)).contains(KeyValue.of("status", "200"),
KeyValue.of("outcome", "SUCCESS"), KeyValue.of("uri", "/resource/{name}"),
KeyValue.of("method", "GET"));
}
@Test
void shouldNotPushAnyHighCardinalityKeyValue() {
assertThat(this.convention.getHighCardinalityKeyValues(this.context)).isEmpty();
}
static class OtherContext extends Observation.Context {
}
}

@ -60,7 +60,7 @@ class RestTemplateObservationConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple())
.withBean(ObservationRegistry.class, TestObservationRegistry::create)
.withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.class,
RestTemplateAutoConfiguration.class, HttpClientMetricsAutoConfiguration.class));
RestTemplateAutoConfiguration.class, HttpClientObservationsAutoConfiguration.class));
@Test
void contributesCustomizerBean() {
@ -139,7 +139,7 @@ class RestTemplateObservationConfigurationTests {
@Test
void backsOffWhenRestTemplateBuilderIsMissing() {
new ApplicationContextRunner().with(MetricsRun.simple())
.withConfiguration(AutoConfigurations.of(HttpClientMetricsAutoConfiguration.class))
.withConfiguration(AutoConfigurations.of(HttpClientObservationsAutoConfiguration.class))
.run((context) -> assertThat(context).doesNotHaveBean(DefaultRestTemplateExchangeTagsProvider.class)
.doesNotHaveBean(ObservationRestTemplateCustomizer.class));
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2022 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.
@ -18,15 +18,17 @@ package org.springframework.boot.actuate.autoconfigure.metrics.web.client;
import java.time.Duration;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.distribution.HistogramSnapshot;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistryAssert;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun;
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
import org.springframework.boot.actuate.metrics.web.reactive.client.DefaultWebClientExchangeTagsProvider;
import org.springframework.boot.actuate.metrics.web.reactive.client.ObservationWebClientCustomizer;
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;
@ -47,22 +49,30 @@ import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link WebClientMetricsConfiguration}
* Tests for {@link WebClientObservationConfiguration}
*
* @author Brian Clozel
* @author Stephane Nicoll
*/
@ExtendWith(OutputCaptureExtension.class)
class WebClientMetricsConfigurationTests {
@SuppressWarnings("deprecation")
class WebClientObservationConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple())
.withBean(ObservationRegistry.class, TestObservationRegistry::create)
.withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.class,
WebClientAutoConfiguration.class, HttpClientMetricsAutoConfiguration.class));
WebClientAutoConfiguration.class, HttpClientObservationsAutoConfiguration.class));
@Test
void contributesCustomizerBean() {
this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ObservationWebClientCustomizer.class)
.doesNotHaveBean(DefaultWebClientExchangeTagsProvider.class));
}
@Test
void webClientCreatedWithBuilderIsInstrumented() {
this.contextRunner.run((context) -> {
MeterRegistry registry = context.getBean(MeterRegistry.class);
TestObservationRegistry registry = context.getBean(TestObservationRegistry.class);
WebClient.Builder builder = context.getBean(WebClient.Builder.class);
validateWebClient(builder, registry);
});
@ -77,8 +87,9 @@ class WebClientMetricsConfigurationTests {
@Test
void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) {
this.contextRunner.withPropertyValues("management.metrics.web.client.max-uri-tags=2").run((context) -> {
MeterRegistry registry = getInitializedMeterRegistry(context);
assertThat(registry.get("http.client.requests").meters()).hasSize(2);
TestObservationRegistry registry = getInitializedRegistry(context);
// TODO check size is 2
TestObservationRegistryAssert.assertThat(registry).hasObservationWithNameEqualTo("http.client.requests");
assertThat(output).contains("Reached the maximum number of URI tags for 'http.client.requests'.")
.contains("Are you using 'uriVariables'?");
});
@ -87,30 +98,17 @@ class WebClientMetricsConfigurationTests {
@Test
void shouldNotDenyNorLogIfMaxUrisIsNotReached(CapturedOutput output) {
this.contextRunner.withPropertyValues("management.metrics.web.client.max-uri-tags=5").run((context) -> {
MeterRegistry registry = getInitializedMeterRegistry(context);
assertThat(registry.get("http.client.requests").meters()).hasSize(3);
TestObservationRegistry registry = getInitializedRegistry(context);
// TODO check size is 3
TestObservationRegistryAssert.assertThat(registry).hasObservationWithNameEqualTo("http.client.requests");
assertThat(output).doesNotContain("Reached the maximum number of URI tags for 'http.client.requests'.")
.doesNotContain("Are you using 'uriVariables'?");
});
}
@Test
void autoTimeRequestsCanBeConfigured() {
this.contextRunner.withPropertyValues("management.metrics.web.client.request.autotime.enabled=true",
"management.metrics.web.client.request.autotime.percentiles=0.5,0.7",
"management.metrics.web.client.request.autotime.percentiles-histogram=true").run((context) -> {
MeterRegistry registry = getInitializedMeterRegistry(context);
Timer timer = registry.get("http.client.requests").timer();
HistogramSnapshot snapshot = timer.takeSnapshot();
assertThat(snapshot.percentileValues()).hasSize(2);
assertThat(snapshot.percentileValues()[0].percentile()).isEqualTo(0.5);
assertThat(snapshot.percentileValues()[1].percentile()).isEqualTo(0.7);
});
}
private MeterRegistry getInitializedMeterRegistry(AssertableApplicationContext context) {
private TestObservationRegistry getInitializedRegistry(AssertableApplicationContext context) {
WebClient webClient = mockWebClient(context.getBean(WebClient.Builder.class));
MeterRegistry registry = context.getBean(MeterRegistry.class);
TestObservationRegistry registry = context.getBean(TestObservationRegistry.class);
for (int i = 0; i < 3; i++) {
webClient.get().uri("https://example.org/projects/" + i).retrieve().toBodilessEntity()
.block(Duration.ofSeconds(30));
@ -118,12 +116,13 @@ class WebClientMetricsConfigurationTests {
return registry;
}
private void validateWebClient(WebClient.Builder builder, MeterRegistry registry) {
private void validateWebClient(WebClient.Builder builder, TestObservationRegistry registry) {
WebClient webClient = mockWebClient(builder);
assertThat(registry.find("http.client.requests").meter()).isNull();
TestObservationRegistryAssert.assertThat(registry).doesNotHaveAnyObservation();
webClient.get().uri("https://example.org/projects/{project}", "spring-boot").retrieve().toBodilessEntity()
.block(Duration.ofSeconds(30));
assertThat(registry.find("http.client.requests").tags("uri", "/projects/{project}").meter()).isNotNull();
TestObservationRegistryAssert.assertThat(registry).hasObservationWithNameEqualTo("http.client.requests").that()
.hasLowCardinalityKeyValue("uri", "https://example.org/projects/{project}");
}
private WebClient mockWebClient(WebClient.Builder builder) {

@ -21,7 +21,7 @@ import zipkin2.reporter.urlconnection.URLConnectionSender;
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.web.client.HttpClientMetricsAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.web.client.HttpClientObservationsAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.tracing.BraveAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration;
@ -64,7 +64,7 @@ class ZipkinAutoConfigurationIntegrationTests {
return runner
.withConfiguration(AutoConfigurations.of(MicrometerTracingAutoConfiguration.class,
ObservationAutoConfiguration.class, BraveAutoConfiguration.class, ZipkinAutoConfiguration.class,
HttpClientMetricsAutoConfiguration.class, MetricsAutoConfiguration.class,
HttpClientObservationsAutoConfiguration.class, MetricsAutoConfiguration.class,
SimpleMetricsExportAutoConfiguration.class))
.withClassLoader(new FilteredClassLoader(URLConnectionSender.class));
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2022 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.
@ -29,7 +29,10 @@ import org.springframework.web.reactive.function.client.ClientResponse;
* @author Brian Clozel
* @author Nishant Raut
* @since 2.1.0
* @deprecated since 3.0.0 for removal in 3.2.0 in favor of
* {@link org.springframework.web.reactive.function.client.DefaultClientObservationConvention}
*/
@Deprecated
public class DefaultWebClientExchangeTagsProvider implements WebClientExchangeTagsProvider {
@Override

@ -1,60 +0,0 @@
/*
* Copyright 2012-2020 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
*
* https://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.actuate.metrics.AutoTimer;
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
* @param autoTimer the auto-timers to apply or {@code null} to disable auto-timing
* @since 2.2.0
*/
public MetricsWebClientCustomizer(MeterRegistry meterRegistry, WebClientExchangeTagsProvider tagProvider,
String metricName, AutoTimer autoTimer) {
this.filterFunction = new MetricsWebClientFilterFunction(meterRegistry, tagProvider, metricName, autoTimer);
}
@Override
public void customize(WebClient.Builder webClientBuilder) {
webClientBuilder.filters((filterFunctions) -> {
if (!filterFunctions.contains(this.filterFunction)) {
filterFunctions.add(0, this.filterFunction);
}
});
}
}

@ -1,120 +0,0 @@
/*
* Copyright 2012-2022 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
*
* https://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 java.util.concurrent.atomic.AtomicBoolean;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import reactor.core.publisher.SignalType;
import reactor.util.context.Context;
import reactor.util.context.ContextView;
import org.springframework.boot.actuate.metrics.AutoTimer;
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
* @author Tadaya Tsuyukubo
* @author Scott Frederick
* @since 2.1.0
*/
public class MetricsWebClientFilterFunction implements ExchangeFilterFunction {
private static final String METRICS_WEBCLIENT_START_TIME = MetricsWebClientFilterFunction.class.getName()
+ ".START_TIME";
private static final Log logger = LogFactory.getLog(MetricsWebClientFilterFunction.class);
private final MeterRegistry meterRegistry;
private final WebClientExchangeTagsProvider tagProvider;
private final String metricName;
private final AutoTimer autoTimer;
/**
* Create a new {@code MetricsWebClientFilterFunction}.
* @param meterRegistry the registry to which metrics are recorded
* @param tagProvider provider for metrics tags
* @param metricName name of the metric to record
* @param autoTimer the auto-timer configuration or {@code null} to disable
* @since 2.2.0
*/
public MetricsWebClientFilterFunction(MeterRegistry meterRegistry, WebClientExchangeTagsProvider tagProvider,
String metricName, AutoTimer autoTimer) {
this.meterRegistry = meterRegistry;
this.tagProvider = tagProvider;
this.metricName = metricName;
this.autoTimer = (autoTimer != null) ? autoTimer : AutoTimer.DISABLED;
}
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
if (!this.autoTimer.isEnabled()) {
return next.exchange(request);
}
return next.exchange(request).as((responseMono) -> instrumentResponse(request, responseMono))
.contextWrite(this::putStartTime);
}
private Mono<ClientResponse> instrumentResponse(ClientRequest request, Mono<ClientResponse> responseMono) {
final AtomicBoolean responseReceived = new AtomicBoolean();
return Mono.deferContextual((ctx) -> responseMono.doOnEach((signal) -> {
if (signal.isOnNext() || signal.isOnError()) {
responseReceived.set(true);
recordTimer(request, signal.get(), signal.getThrowable(), getStartTime(ctx));
}
}).doFinally((signalType) -> {
if (!responseReceived.get() && SignalType.CANCEL.equals(signalType)) {
recordTimer(request, null, null, getStartTime(ctx));
}
}));
}
private void recordTimer(ClientRequest request, ClientResponse response, Throwable error, Long startTime) {
try {
Iterable<Tag> tags = this.tagProvider.tags(request, response, error);
this.autoTimer.builder(this.metricName).tags(tags).description("Timer of WebClient operation")
.register(this.meterRegistry).record(System.nanoTime() - startTime, TimeUnit.NANOSECONDS);
}
catch (Exception ex) {
logger.warn("Failed to record timer metrics", ex);
// Allow request-response exchange to continue, unaffected by metrics problem
}
}
private Long getStartTime(ContextView context) {
return context.get(METRICS_WEBCLIENT_START_TIME);
}
private Context putStartTime(Context context) {
return context.put(METRICS_WEBCLIENT_START_TIME, System.nanoTime());
}
}

@ -0,0 +1,56 @@
/*
* Copyright 2012-2022 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
*
* https://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.observation.ObservationRegistry;
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
import org.springframework.web.reactive.function.client.ClientObservationConvention;
import org.springframework.web.reactive.function.client.WebClient;
/**
* {@link WebClientCustomizer} that configures the {@link WebClient} to record request
* observations.
*
* @author Brian Clozel
* @since 3.0.0
*/
public class ObservationWebClientCustomizer implements WebClientCustomizer {
private final ObservationRegistry observationRegistry;
private final ClientObservationConvention observationConvention;
/**
* Create a new {@code ObservationWebClientCustomizer} that will configure the
* {@code Observation} setup on the client.
* @param observationRegistry the registry to publish observations to
* @param observationConvention the convention to use to populate observations
*/
public ObservationWebClientCustomizer(ObservationRegistry observationRegistry,
ClientObservationConvention observationConvention) {
this.observationRegistry = observationRegistry;
this.observationConvention = observationConvention;
}
@Override
public void customize(WebClient.Builder webClientBuilder) {
webClientBuilder.observationRegistry(this.observationRegistry)
.observationConvention(this.observationConvention);
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2022 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.
@ -27,8 +27,11 @@ import org.springframework.web.reactive.function.client.ClientResponse;
*
* @author Brian Clozel
* @since 2.1.0
* @deprecated since 3.0.0 for removal in 3.2.0 in favor of
* {@link org.springframework.web.reactive.function.client.ClientObservationConvention}
*/
@FunctionalInterface
@Deprecated
public interface WebClientExchangeTagsProvider {
/**

@ -39,6 +39,7 @@ import static org.mockito.Mockito.mock;
* @author Brian Clozel
* @author Nishant Raut
*/
@SuppressWarnings("deprecation")
class DefaultWebClientExchangeTagsProviderTests {
private static final String URI_TEMPLATE_ATTRIBUTE = WebClient.class.getName() + ".uriTemplate";

@ -1,64 +0,0 @@
/*
* Copyright 2012-2021 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
*
* https://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.jupiter.api.BeforeEach;
import org.junit.jupiter.api.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
*/
class MetricsWebClientCustomizerTests {
private MetricsWebClientCustomizer customizer;
private WebClient.Builder clientBuilder;
@BeforeEach
void setup() {
this.customizer = new MetricsWebClientCustomizer(mock(MeterRegistry.class),
mock(WebClientExchangeTagsProvider.class), "test", null);
this.clientBuilder = WebClient.builder();
}
@Test
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
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).singleElement().isInstanceOf(MetricsWebClientFilterFunction.class));
}
}

@ -1,194 +0,0 @@
/*
* Copyright 2012-2022 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
*
* https://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 java.time.Duration;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.MockClock;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.search.MeterNotFoundException;
import io.micrometer.core.instrument.simple.SimpleConfig;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.boot.actuate.metrics.AutoTimer;
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.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link MetricsWebClientFilterFunction}
*
* @author Brian Clozel
* @author Scott Frederick
*/
class MetricsWebClientFilterFunctionTests {
private static final String URI_TEMPLATE_ATTRIBUTE = WebClient.class.getName() + ".uriTemplate";
private MeterRegistry registry;
private MetricsWebClientFilterFunction filterFunction;
private final FaultyTagsProvider tagsProvider = new FaultyTagsProvider();
private ClientResponse response;
private ExchangeFunction exchange;
@BeforeEach
void setup() {
this.registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, new MockClock());
this.filterFunction = new MetricsWebClientFilterFunction(this.registry, this.tagsProvider,
"http.client.requests", AutoTimer.ENABLED);
this.response = mock(ClientResponse.class);
this.exchange = (r) -> Mono.just(this.response);
}
@Test
void filterShouldRecordTimer() {
ClientRequest request = ClientRequest
.create(HttpMethod.GET, URI.create("https://example.com/projects/spring-boot")).build();
given(this.response.statusCode()).willReturn(HttpStatus.OK);
this.filterFunction.filter(request, this.exchange).block(Duration.ofSeconds(5));
assertThat(this.registry.get("http.client.requests")
.tags("method", "GET", "uri", "/projects/spring-boot", "status", "200").timer().count()).isEqualTo(1);
}
@Test
void filterWhenUriTemplatePresentShouldRecordTimer() {
ClientRequest request = ClientRequest
.create(HttpMethod.GET, URI.create("https://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(Duration.ofSeconds(5));
assertThat(this.registry.get("http.client.requests")
.tags("method", "GET", "uri", "/projects/{project}", "status", "200").timer().count()).isEqualTo(1);
}
@Test
void filterWhenIoExceptionThrownShouldRecordTimer() {
ClientRequest request = ClientRequest
.create(HttpMethod.GET, URI.create("https://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(Duration.ofSeconds(5));
assertThat(this.registry.get("http.client.requests")
.tags("method", "GET", "uri", "/projects/spring-boot", "status", "IO_ERROR").timer().count())
.isEqualTo(1);
}
@Test
void filterWhenExceptionThrownShouldRecordTimer() {
ClientRequest request = ClientRequest
.create(HttpMethod.GET, URI.create("https://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(Duration.ofSeconds(5));
assertThat(this.registry.get("http.client.requests")
.tags("method", "GET", "uri", "/projects/spring-boot", "status", "CLIENT_ERROR").timer().count())
.isEqualTo(1);
}
@Test
void filterWhenCancelThrownShouldRecordTimer() {
ClientRequest request = ClientRequest
.create(HttpMethod.GET, URI.create("https://example.com/projects/spring-boot")).build();
given(this.response.statusCode()).willReturn(HttpStatus.OK);
Mono<ClientResponse> filter = this.filterFunction.filter(request, this.exchange);
StepVerifier.create(filter).thenCancel().verify(Duration.ofSeconds(5));
assertThat(this.registry.get("http.client.requests")
.tags("method", "GET", "uri", "/projects/spring-boot", "status", "CLIENT_ERROR").timer().count())
.isEqualTo(1);
assertThatThrownBy(() -> this.registry.get("http.client.requests")
.tags("method", "GET", "uri", "/projects/spring-boot", "status", "200").timer())
.isInstanceOf(MeterNotFoundException.class);
}
@Test
void filterWhenCancelAfterResponseThrownShouldNotRecordTimer() {
ClientRequest request = ClientRequest
.create(HttpMethod.GET, URI.create("https://example.com/projects/spring-boot")).build();
given(this.response.statusCode()).willReturn(HttpStatus.OK);
Mono<ClientResponse> filter = this.filterFunction.filter(request, this.exchange);
StepVerifier.create(filter).expectNextCount(1).thenCancel().verify(Duration.ofSeconds(5));
assertThat(this.registry.get("http.client.requests")
.tags("method", "GET", "uri", "/projects/spring-boot", "status", "200").timer().count()).isEqualTo(1);
assertThatThrownBy(() -> this.registry.get("http.client.requests")
.tags("method", "GET", "uri", "/projects/spring-boot", "status", "CLIENT_ERROR").timer())
.isInstanceOf(MeterNotFoundException.class);
}
@Test
void filterWhenExceptionAndRetryShouldNotAccumulateRecordTime() {
ClientRequest request = ClientRequest
.create(HttpMethod.GET, URI.create("https://example.com/projects/spring-boot")).build();
ExchangeFunction exchange = (r) -> Mono.error(new IllegalArgumentException())
.delaySubscription(Duration.ofMillis(1000)).cast(ClientResponse.class);
this.filterFunction.filter(request, exchange).retry(1)
.onErrorResume(IllegalArgumentException.class, (t) -> Mono.empty()).block(Duration.ofSeconds(5));
Timer timer = this.registry.get("http.client.requests")
.tags("method", "GET", "uri", "/projects/spring-boot", "status", "CLIENT_ERROR").timer();
assertThat(timer.count()).isEqualTo(2);
assertThat(timer.max(TimeUnit.MILLISECONDS)).isLessThan(2000);
}
@Test
void whenMetricsRecordingFailsThenFilteringSucceeds() {
ClientRequest request = ClientRequest
.create(HttpMethod.GET, URI.create("https://example.com/projects/spring-boot")).build();
given(this.response.statusCode()).willReturn(HttpStatus.OK);
this.tagsProvider.failOnce();
this.filterFunction.filter(request, this.exchange).block(Duration.ofSeconds(5));
}
static class FaultyTagsProvider extends DefaultWebClientExchangeTagsProvider {
private final AtomicBoolean fail = new AtomicBoolean(false);
@Override
public Iterable<Tag> tags(ClientRequest request, ClientResponse response, Throwable throwable) {
if (this.fail.compareAndSet(true, false)) {
throw new RuntimeException();
}
return super.tags(request, response, throwable);
}
void failOnce() {
this.fail.set(true);
}
}
}

@ -0,0 +1,60 @@
/*
* Copyright 2012-2022 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
*
* https://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.observation.ObservationConvention;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistry;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.reactive.function.client.ClientObservationConvention;
import org.springframework.web.reactive.function.client.DefaultClientObservationConvention;
import org.springframework.web.reactive.function.client.WebClient;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ObservationWebClientCustomizer}
*
* @author Brian Clozel
*/
@SuppressWarnings("deprecation")
class ObservationWebClientCustomizerTests {
private static final String TEST_METRIC_NAME = "http.test.metric.name";
private TestObservationRegistry observationRegistry = TestObservationRegistry.create();
private ClientObservationConvention observationConvention = new DefaultClientObservationConvention(
TEST_METRIC_NAME);
private ObservationWebClientCustomizer customizer = new ObservationWebClientCustomizer(this.observationRegistry,
this.observationConvention);
private WebClient.Builder clientBuilder = WebClient.builder();
@Test
void shouldCustomizeObservationConfiguration() {
this.customizer.customize(this.clientBuilder);
assertThat((ObservationRegistry) ReflectionTestUtils.getField(this.clientBuilder, "observationRegistry"))
.isEqualTo(this.observationRegistry);
assertThat((ObservationConvention<?>) ReflectionTestUtils.getField(this.clientBuilder, "observationConvention"))
.isInstanceOf(DefaultClientObservationConvention.class).extracting("name").isEqualTo(TEST_METRIC_NAME);
}
}
Loading…
Cancel
Save