From 8b4a20d6cd9ba0a79063f9cbc12d1c74c87f3e66 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 30 Sep 2022 11:18:07 +0200 Subject: [PATCH] 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 --- .../metrics/MetricsProperties.java | 10 - .../ClientObservationConventionAdapter.java | 86 ++++++++ ...pClientObservationsAutoConfiguration.java} | 42 ++-- .../RestTemplateObservationConfiguration.java | 5 +- ...=> WebClientObservationConfiguration.java} | 37 ++-- ...itional-spring-configuration-metadata.json | 18 +- ...ot.autoconfigure.AutoConfiguration.imports | 2 +- .../metrics/test/MetricsIntegrationTests.java | 4 +- ...ientObservationConventionAdapterTests.java | 89 ++++++++ ...TemplateObservationConfigurationTests.java | 4 +- ...bClientObservationConfigurationTests.java} | 61 +++--- ...pkinAutoConfigurationIntegrationTests.java | 4 +- .../DefaultWebClientExchangeTagsProvider.java | 5 +- .../client/MetricsWebClientCustomizer.java | 60 ------ .../MetricsWebClientFilterFunction.java | 120 ----------- .../ObservationWebClientCustomizer.java | 56 +++++ .../client/WebClientExchangeTagsProvider.java | 5 +- ...ultWebClientExchangeTagsProviderTests.java | 1 + .../MetricsWebClientCustomizerTests.java | 64 ------ .../MetricsWebClientFilterFunctionTests.java | 194 ------------------ .../ObservationWebClientCustomizerTests.java | 60 ++++++ 21 files changed, 400 insertions(+), 527 deletions(-) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/ClientObservationConventionAdapter.java rename spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/{HttpClientMetricsAutoConfiguration.java => HttpClientObservationsAutoConfiguration.java} (65%) rename spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/{WebClientMetricsConfiguration.java => WebClientObservationConfiguration.java} (50%) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/ClientObservationConventionAdapterTests.java rename spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/{WebClientMetricsConfigurationTests.java => WebClientObservationConfigurationTests.java} (69%) delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientCustomizer.java delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunction.java create mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/ObservationWebClientCustomizer.java delete mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientCustomizerTests.java delete mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunctionTests.java create mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/ObservationWebClientCustomizerTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java index 4f60fb5375..141be9b6ea 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsProperties.java @@ -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; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/ClientObservationConventionAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/ClientObservationConventionAdapter.java new file mode 100644 index 0000000000..16b076bb0a --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/ClientObservationConventionAdapter.java @@ -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 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; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/HttpClientMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/HttpClientObservationsAutoConfiguration.java similarity index 65% rename from spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/HttpClientMetricsAutoConfiguration.java rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/HttpClientObservationsAutoConfiguration.java index 644fb1fef0..81ea628a32 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/HttpClientMetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/HttpClientObservationsAutoConfiguration.java @@ -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); + } + } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/RestTemplateObservationConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/RestTemplateObservationConfiguration.java index b9eb9620ae..d23e53a30c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/RestTemplateObservationConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/RestTemplateObservationConfiguration.java @@ -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 { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientObservationConfiguration.java similarity index 50% rename from spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfiguration.java rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientObservationConfiguration.java index 994f7b25db..a09ff52530 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientObservationConfiguration.java @@ -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 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); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 3af7c2ab55..0cf1d1e133 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -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", diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index cf47b7bb35..1ba70ac93f 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -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 diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/test/MetricsIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/test/MetricsIntegrationTests.java index ec5deaeb0b..02846519ab 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/test/MetricsIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/test/MetricsIntegrationTests.java @@ -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, diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/ClientObservationConventionAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/ClientObservationConventionAdapterTests.java new file mode 100644 index 0000000000..786a68d2a5 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/ClientObservationConventionAdapterTests.java @@ -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 { + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/RestTemplateObservationConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/RestTemplateObservationConfigurationTests.java index f7e6eafef0..dbb9e079ea 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/RestTemplateObservationConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/RestTemplateObservationConfigurationTests.java @@ -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)); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientObservationConfigurationTests.java similarity index 69% rename from spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfigurationTests.java rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientObservationConfigurationTests.java index a81c2d7ed5..ca3dc1bbe3 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientMetricsConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/client/WebClientObservationConfigurationTests.java @@ -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) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfigurationIntegrationTests.java index c6aeaf336a..3e69b31a83 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/zipkin/ZipkinAutoConfigurationIntegrationTests.java @@ -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)); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProvider.java index 6ea9f0c012..2fde367ae6 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProvider.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProvider.java @@ -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 diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientCustomizer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientCustomizer.java deleted file mode 100644 index 5b4be59ba1..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientCustomizer.java +++ /dev/null @@ -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); - } - }); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunction.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunction.java deleted file mode 100644 index 17f258ffb5..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunction.java +++ /dev/null @@ -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 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 instrumentResponse(ClientRequest request, Mono 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 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()); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/ObservationWebClientCustomizer.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/ObservationWebClientCustomizer.java new file mode 100644 index 0000000000..af85ad0839 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/ObservationWebClientCustomizer.java @@ -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); + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsProvider.java index 6dc127956a..0440a48f67 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsProvider.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/client/WebClientExchangeTagsProvider.java @@ -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 { /** diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProviderTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProviderTests.java index d01b7d9cb7..b8758ff941 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProviderTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/DefaultWebClientExchangeTagsProviderTests.java @@ -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"; diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientCustomizerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientCustomizerTests.java deleted file mode 100644 index cb25ba9e27..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientCustomizerTests.java +++ /dev/null @@ -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)); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunctionTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunctionTests.java deleted file mode 100644 index ad4ae96aa1..0000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/MetricsWebClientFilterFunctionTests.java +++ /dev/null @@ -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 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 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 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); - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/ObservationWebClientCustomizerTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/ObservationWebClientCustomizerTests.java new file mode 100644 index 0000000000..442708b5fa --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/client/ObservationWebClientCustomizerTests.java @@ -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); + } + +}