From 685fa900f8f5ea5b14bba14f3eb982015786bee9 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 20 Oct 2022 15:24:47 +0200 Subject: [PATCH] Auto-configure Observation instrumentation for WebFlux Prior to this commit, Spring Boot would offer a specific Metrics instrumentation for WebFlux applications through a `WebFilter` and custom Tag providers. As of Spring Framework 6.0, the Observation instrumentation is done directly in WebFlux, also with a `WebFilter`. While this allows both metrics and traces, some features cannot be supported in the same way with this new infrastructure. The former `WebFilter` has been removed and the Tagging infrastructure deprecated in favor of custom Observation conventions. This commit provides an adapter layer so that developers can refactor their custom tagging solution to the convention way, during the deprecation phase, without losing any feature. Closes gh-32539 --- .../WebFluxMetricsAutoConfiguration.java | 85 ------ ...lientHttpObservationConventionAdapter.java | 6 - ...erRequestObservationConventionAdapter.java | 68 +++++ .../WebFluxObservationAutoConfiguration.java | 116 +++++++ .../web/reactive/package-info.java | 6 +- ...ot.autoconfigure.AutoConfiguration.imports | 2 +- .../metrics/test/MetricsIntegrationTests.java | 4 +- .../WebFluxMetricsAutoConfigurationTests.java | 159 ---------- ...uestObservationConventionAdapterTests.java | 64 ++++ ...FluxObservationAutoConfigurationTests.java | 108 +++++++ .../CancelledServerWebExchangeException.java | 29 -- .../server/DefaultWebFluxTagsProvider.java | 4 + .../web/reactive/server/MetricsWebFilter.java | 128 -------- .../web/reactive/server/WebFluxTags.java | 6 +- .../server/WebFluxTagsContributor.java | 3 + .../reactive/server/WebFluxTagsProvider.java | 3 + .../DefaultWebFluxTagsProviderTests.java | 1 + .../server/MetricsWebFilterTests.java | 283 ------------------ .../web/reactive/server/WebFluxTagsTests.java | 7 +- 19 files changed, 378 insertions(+), 704 deletions(-) delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/ServerRequestObservationConventionAdapter.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java rename spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/{metrics => observation}/web/reactive/package-info.java (74%) delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/ServerRequestObservationConventionAdapterTests.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/CancelledServerWebExchangeException.java delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/MetricsWebFilter.java delete mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/MetricsWebFilterTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfiguration.java deleted file mode 100644 index 051b1f97a96..00000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfiguration.java +++ /dev/null @@ -1,85 +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.autoconfigure.metrics.web.reactive; - -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.config.MeterFilter; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties; -import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties.Web.Server.ServerRequest; -import org.springframework.boot.actuate.autoconfigure.metrics.OnlyOnceLoggingDenyMeterFilter; -import org.springframework.boot.actuate.autoconfigure.metrics.PropertiesAutoTimer; -import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; -import org.springframework.boot.actuate.metrics.web.reactive.server.DefaultWebFluxTagsProvider; -import org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter; -import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsContributor; -import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsProvider; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; -import org.springframework.context.annotation.Bean; -import org.springframework.core.annotation.Order; - -/** - * {@link EnableAutoConfiguration Auto-configuration} for instrumentation of Spring - * WebFlux applications. - * - * @author Jon Schneider - * @author Dmytro Nosan - * @since 2.0.0 - */ -@AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, - SimpleMetricsExportAutoConfiguration.class }) -@ConditionalOnBean(MeterRegistry.class) -@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) -@SuppressWarnings("removal") -public class WebFluxMetricsAutoConfiguration { - - private final MetricsProperties properties; - - public WebFluxMetricsAutoConfiguration(MetricsProperties properties) { - this.properties = properties; - } - - @Bean - @ConditionalOnMissingBean(WebFluxTagsProvider.class) - public DefaultWebFluxTagsProvider webFluxTagsProvider(ObjectProvider contributors) { - return new DefaultWebFluxTagsProvider(true, contributors.orderedStream().toList()); - } - - @Bean - public MetricsWebFilter webfluxMetrics(MeterRegistry registry, WebFluxTagsProvider tagConfigurer) { - ServerRequest request = this.properties.getWeb().getServer().getRequest(); - return new MetricsWebFilter(registry, tagConfigurer, request.getMetricName(), new PropertiesAutoTimer(null)); - } - - @Bean - @Order(0) - public MeterFilter metricsHttpServerUriTagFilter() { - String metricName = this.properties.getWeb().getServer().getRequest().getMetricName(); - MeterFilter filter = new OnlyOnceLoggingDenyMeterFilter( - () -> String.format("Reached the maximum number of URI tags for '%s'.", metricName)); - return MeterFilter.maximumAllowableTags(metricName, "uri", this.properties.getWeb().getServer().getMaxUriTags(), - filter); - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/ClientHttpObservationConventionAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/ClientHttpObservationConventionAdapter.java index d08b9bd1c56..9f9d5e70633 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/ClientHttpObservationConventionAdapter.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/ClientHttpObservationConventionAdapter.java @@ -18,7 +18,6 @@ package org.springframework.boot.actuate.autoconfigure.observation.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.client.RestTemplateExchangeTagsProvider; import org.springframework.http.client.observation.ClientRequestObservationContext; @@ -42,11 +41,6 @@ class ClientHttpObservationConventionAdapter implements ClientRequestObservation this.tagsProvider = tagsProvider; } - @Override - public boolean supportsContext(Observation.Context context) { - return context instanceof ClientRequestObservationContext; - } - @Override @SuppressWarnings("deprecation") public KeyValues getLowCardinalityKeyValues(ClientRequestObservationContext context) { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/ServerRequestObservationConventionAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/ServerRequestObservationConventionAdapter.java new file mode 100644 index 00000000000..93df517940c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/ServerRequestObservationConventionAdapter.java @@ -0,0 +1,68 @@ +/* + * 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.observation.web.reactive; + +import java.util.List; + +import io.micrometer.common.KeyValues; +import io.micrometer.core.instrument.Tag; + +import org.springframework.boot.actuate.metrics.web.reactive.server.DefaultWebFluxTagsProvider; +import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsContributor; +import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsProvider; +import org.springframework.http.observation.reactive.ServerRequestObservationContext; +import org.springframework.http.observation.reactive.ServerRequestObservationConvention; + +/** + * Adapter class that applies {@link WebFluxTagsProvider} tags as a + * {@link ServerRequestObservationConvention}. + * + * @author Brian Clozel + */ +@SuppressWarnings("removal") +class ServerRequestObservationConventionAdapter implements ServerRequestObservationConvention { + + private final String name; + + private final WebFluxTagsProvider tagsProvider; + + ServerRequestObservationConventionAdapter(String name, WebFluxTagsProvider tagsProvider) { + this.name = name; + this.tagsProvider = tagsProvider; + } + + ServerRequestObservationConventionAdapter(String name, List contributors) { + this.name = name; + this.tagsProvider = new DefaultWebFluxTagsProvider(contributors); + } + + @Override + public String getName() { + return this.name; + } + + @Override + public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) { + KeyValues keyValues = KeyValues.empty(); + Iterable tags = this.tagsProvider.httpRequestTags(context.getServerWebExchange(), context.getError()); + for (Tag tag : tags) { + keyValues = keyValues.and(tag.getKey(), tag.getValue()); + } + return keyValues; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java new file mode 100644 index 00000000000..b5343633b05 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java @@ -0,0 +1,116 @@ +/* + * 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.observation.web.reactive; + +import java.util.List; + +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.beans.factory.ObjectProvider; +import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties; +import org.springframework.boot.actuate.autoconfigure.metrics.OnlyOnceLoggingDenyMeterFilter; +import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties; +import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsContributor; +import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.http.observation.reactive.DefaultServerRequestObservationConvention; +import org.springframework.http.observation.reactive.ServerRequestObservationConvention; +import org.springframework.web.filter.reactive.ServerHttpObservationFilter; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for instrumentation of Spring + * WebFlux applications. + * + * @author Brian Clozel + * @author Jon Schneider + * @author Dmytro Nosan + * @since 3.0.0 + */ +@AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, + SimpleMetricsExportAutoConfiguration.class, ObservationAutoConfiguration.class }) +@ConditionalOnClass(Observation.class) +@ConditionalOnBean(ObservationRegistry.class) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) +@EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class }) +@SuppressWarnings("removal") +public class WebFluxObservationAutoConfiguration { + + private final MetricsProperties metricsProperties; + + private final ObservationProperties observationProperties; + + public WebFluxObservationAutoConfiguration(MetricsProperties metricsProperties, + ObservationProperties observationProperties) { + this.metricsProperties = metricsProperties; + this.observationProperties = observationProperties; + } + + @Bean + @ConditionalOnMissingBean + public ServerHttpObservationFilter webfluxObservationFilter(ObservationRegistry registry, + ObjectProvider tagConfigurer, + ObjectProvider contributorsProvider) { + + String observationName = this.observationProperties.getHttp().getServer().getRequests().getName(); + String metricName = this.metricsProperties.getWeb().getServer().getRequest().getMetricName(); + String name = (observationName != null) ? observationName : metricName; + WebFluxTagsProvider tagsProvider = tagConfigurer.getIfAvailable(); + List tagsContributors = contributorsProvider.orderedStream().toList(); + ServerRequestObservationConvention convention = new DefaultServerRequestObservationConvention(name); + if (tagsProvider != null) { + convention = new ServerRequestObservationConventionAdapter(name, tagsProvider); + } + else if (!tagsContributors.isEmpty()) { + convention = new ServerRequestObservationConventionAdapter(name, tagsContributors); + } + return new ServerHttpObservationFilter(registry, convention); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(MeterRegistry.class) + @ConditionalOnBean(MeterRegistry.class) + static class MeterFilterConfiguration { + + @Bean + @Order(0) + MeterFilter metricsHttpServerUriTagFilter(MetricsProperties properties) { + String metricName = properties.getWeb().getServer().getRequest().getMetricName(); + MeterFilter filter = new OnlyOnceLoggingDenyMeterFilter( + () -> String.format("Reached the maximum number of URI tags for '%s'.", metricName)); + return MeterFilter.maximumAllowableTags(metricName, "uri", properties.getWeb().getServer().getMaxUriTags(), + filter); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/package-info.java similarity index 74% rename from spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/package-info.java rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/package-info.java index 4849faffa23..e7770c5fba2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/package-info.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/package-info.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. @@ -15,6 +15,6 @@ */ /** - * Auto-configuration for WebFlux actuator metrics. + * Auto-configuration for WebFlux actuator observations. */ -package org.springframework.boot.actuate.autoconfigure.metrics.web.reactive; +package org.springframework.boot.actuate.autoconfigure.observation.web.reactive; 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 967582a86da..74ec09657fc 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 @@ -79,7 +79,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.startup.StartupTimeMetric org.springframework.boot.actuate.autoconfigure.metrics.task.TaskExecutorMetricsAutoConfiguration org.springframework.boot.actuate.autoconfigure.observation.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.observation.web.reactive.WebFluxObservationAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.web.tomcat.TomcatMetricsAutoConfiguration org.springframework.boot.actuate.autoconfigure.data.mongo.MongoHealthContributorAutoConfiguration org.springframework.boot.actuate.autoconfigure.data.mongo.MongoReactiveHealthContributorAutoConfiguration 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 922a5e113e5..1062a3bafb6 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,9 +41,9 @@ 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.reactive.WebFluxMetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.observation.web.client.HttpClientObservationsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.observation.web.reactive.WebFluxObservationAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.observation.web.servlet.WebMvcObservationAutoConfiguration; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; @@ -141,7 +141,7 @@ class MetricsIntegrationTests { SystemMetricsAutoConfiguration.class, RabbitMetricsAutoConfiguration.class, CacheMetricsAutoConfiguration.class, DataSourcePoolMetricsAutoConfiguration.class, HibernateMetricsAutoConfiguration.class, HttpClientObservationsAutoConfiguration.class, - WebFluxMetricsAutoConfiguration.class, WebMvcObservationAutoConfiguration.class, + WebFluxObservationAutoConfiguration.class, WebMvcObservationAutoConfiguration.class, JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, RestTemplateAutoConfiguration.class, WebMvcAutoConfiguration.class, DispatcherServletAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class }) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfigurationTests.java deleted file mode 100644 index 14291de5107..00000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/reactive/WebFluxMetricsAutoConfigurationTests.java +++ /dev/null @@ -1,159 +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.autoconfigure.metrics.web.reactive; - -import io.micrometer.core.instrument.MeterRegistry; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; -import org.springframework.boot.actuate.autoconfigure.metrics.web.TestController; -import org.springframework.boot.actuate.metrics.web.reactive.server.DefaultWebFluxTagsProvider; -import org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter; -import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsContributor; -import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsProvider; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; -import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext; -import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; -import org.springframework.boot.test.system.CapturedOutput; -import org.springframework.boot.test.system.OutputCaptureExtension; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.test.web.reactive.server.WebTestClient; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link WebFluxMetricsAutoConfiguration} - * - * @author Brian Clozel - * @author Dmytro Nosan - * @author Madhura Bhave - */ -@ExtendWith(OutputCaptureExtension.class) -@Disabled("until gh-32539 is fixed") -class WebFluxMetricsAutoConfigurationTests { - - private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() - .with(MetricsRun.simple()).withConfiguration(AutoConfigurations.of(WebFluxMetricsAutoConfiguration.class)); - - @Test - void shouldProvideWebFluxMetricsBeans() { - this.contextRunner.run((context) -> { - assertThat(context).getBeans(MetricsWebFilter.class).hasSize(1); - assertThat(context).getBeans(DefaultWebFluxTagsProvider.class).hasSize(1); - assertThat(context.getBean(DefaultWebFluxTagsProvider.class)).extracting("ignoreTrailingSlash") - .isEqualTo(true); - }); - } - - @Test - void tagsProviderWhenIgnoreTrailingSlashIsFalse() { - this.contextRunner.withPropertyValues("management.metrics.web.server.request.ignore-trailing-slash=false") - .run((context) -> { - assertThat(context).hasSingleBean(DefaultWebFluxTagsProvider.class); - assertThat(context.getBean(DefaultWebFluxTagsProvider.class)).extracting("ignoreTrailingSlash") - .isEqualTo(false); - }); - } - - @Test - void shouldNotOverrideCustomTagsProvider() { - this.contextRunner.withUserConfiguration(CustomWebFluxTagsProviderConfig.class) - .run((context) -> assertThat(context).getBeans(WebFluxTagsProvider.class).hasSize(1) - .containsKey("customWebFluxTagsProvider")); - } - - @Test - void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) { - this.contextRunner.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class)) - .withUserConfiguration(TestController.class) - .withPropertyValues("management.metrics.web.server.max-uri-tags=2").run((context) -> { - MeterRegistry registry = getInitializedMeterRegistry(context); - assertThat(registry.get("http.server.requests").meters()).hasSize(2); - assertThat(output).contains("Reached the maximum number of URI tags for 'http.server.requests'"); - }); - } - - @Test - void shouldNotDenyNorLogIfMaxUrisIsNotReached(CapturedOutput output) { - this.contextRunner.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class)) - .withUserConfiguration(TestController.class) - .withPropertyValues("management.metrics.web.server.max-uri-tags=5").run((context) -> { - MeterRegistry registry = getInitializedMeterRegistry(context); - assertThat(registry.get("http.server.requests").meters()).hasSize(3); - assertThat(output) - .doesNotContain("Reached the maximum number of URI tags for 'http.server.requests'"); - }); - } - - @Test - void metricsAreNotRecordedIfAutoTimeRequestsIsDisabled() { - this.contextRunner.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class)) - .withUserConfiguration(TestController.class) - .withPropertyValues("management.metrics.web.server.request.autotime.enabled=false").run((context) -> { - MeterRegistry registry = getInitializedMeterRegistry(context); - assertThat(registry.find("http.server.requests").meter()).isNull(); - }); - } - - @Test - void whenTagContributorsAreDefinedThenTagsProviderUsesThem() { - this.contextRunner.withUserConfiguration(TagsContributorsConfiguration.class).run((context) -> { - assertThat(context).hasSingleBean(DefaultWebFluxTagsProvider.class); - assertThat(context.getBean(DefaultWebFluxTagsProvider.class)).extracting("contributors").asList() - .hasSize(2); - }); - } - - private MeterRegistry getInitializedMeterRegistry(AssertableReactiveWebApplicationContext context) { - WebTestClient webTestClient = WebTestClient.bindToApplicationContext(context).build(); - for (int i = 0; i < 3; i++) { - webTestClient.get().uri("/test" + i).exchange().expectStatus().isOk(); - } - return context.getBean(MeterRegistry.class); - } - - @Configuration(proxyBeanMethods = false) - static class CustomWebFluxTagsProviderConfig { - - @Bean - WebFluxTagsProvider customWebFluxTagsProvider() { - return mock(WebFluxTagsProvider.class); - } - - } - - @Configuration(proxyBeanMethods = false) - static class TagsContributorsConfiguration { - - @Bean - WebFluxTagsContributor tagContributorOne() { - return mock(WebFluxTagsContributor.class); - } - - @Bean - WebFluxTagsContributor tagContributorTwo() { - return mock(WebFluxTagsContributor.class); - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/ServerRequestObservationConventionAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/ServerRequestObservationConventionAdapterTests.java new file mode 100644 index 00000000000..8f1c79c89d2 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/ServerRequestObservationConventionAdapterTests.java @@ -0,0 +1,64 @@ +/* + * 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.observation.web.reactive; + +import io.micrometer.common.KeyValue; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.metrics.web.reactive.server.DefaultWebFluxTagsProvider; +import org.springframework.http.observation.reactive.ServerRequestObservationContext; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.util.pattern.PathPatternParser; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ServerRequestObservationConventionAdapter}. + * + * @author Brian Clozel + */ +@SuppressWarnings("removal") +class ServerRequestObservationConventionAdapterTests { + + private static final String TEST_METRIC_NAME = "test.metric.name"; + + private final MockServerHttpRequest request = MockServerHttpRequest.get("/resource/test").build(); + + private final MockServerWebExchange serverWebExchange = MockServerWebExchange.builder(this.request).build(); + + private final ServerRequestObservationContext context = new ServerRequestObservationContext(this.serverWebExchange); + + private final ServerRequestObservationConventionAdapter convention = new ServerRequestObservationConventionAdapter( + TEST_METRIC_NAME, new DefaultWebFluxTagsProvider()); + + @Test + void shouldUseConfiguredName() { + assertThat(this.convention.getName()).isEqualTo(TEST_METRIC_NAME); + } + + @Test + void shouldPushTagsAsLowCardinalityKeyValues() { + this.serverWebExchange.getAttributes().put(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, + PathPatternParser.defaultInstance.parse("/resource/{name}")); + assertThat(this.convention.getLowCardinalityKeyValues(this.context)).contains(KeyValue.of("status", "200"), + KeyValue.of("outcome", "SUCCESS"), KeyValue.of("uri", "/resource/{name}"), + KeyValue.of("method", "GET")); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java new file mode 100644 index 00000000000..e5a7fadf17c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java @@ -0,0 +1,108 @@ +/* + * 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.observation.web.reactive; + +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +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.server.DefaultWebFluxTagsProvider; +import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsContributor; +import org.springframework.boot.actuate.metrics.web.reactive.server.WebFluxTagsProvider; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.reactive.ServerHttpObservationFilter; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link WebFluxObservationAutoConfiguration} + * + * @author Brian Clozel + * @author Dmytro Nosan + * @author Madhura Bhave + */ +@ExtendWith(OutputCaptureExtension.class) +@SuppressWarnings("removal") +class WebFluxObservationAutoConfigurationTests { + + private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() + .with(MetricsRun.simple()).withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.class, + WebFluxObservationAutoConfiguration.class)); + + @Test + void shouldProvideWebFluxObservationFilter() { + this.contextRunner.run((context) -> assertThat(context).hasSingleBean(ServerHttpObservationFilter.class)); + } + + @Test + void shouldUseConventionAdapterWhenCustomTagsProvider() { + this.contextRunner.withUserConfiguration(CustomTagsProviderConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(ServerHttpObservationFilter.class); + assertThat(context).hasSingleBean(WebFluxTagsProvider.class); + assertThat(context).getBean(ServerHttpObservationFilter.class).extracting("observationConvention") + .isInstanceOf(ServerRequestObservationConventionAdapter.class); + }); + } + + @Test + void shouldUseConventionAdapterWhenCustomTagsContributor() { + this.contextRunner.withUserConfiguration(CustomTagsContributorConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(ServerHttpObservationFilter.class); + assertThat(context).hasSingleBean(WebFluxTagsContributor.class); + assertThat(context).getBean(ServerHttpObservationFilter.class).extracting("observationConvention") + .isInstanceOf(ServerRequestObservationConventionAdapter.class); + }); + } + + @Configuration(proxyBeanMethods = false) + static class CustomTagsProviderConfiguration { + + @Bean + WebFluxTagsProvider tagsProvider() { + return new DefaultWebFluxTagsProvider(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class CustomTagsContributorConfiguration { + + @Bean + WebFluxTagsContributor tagsContributor() { + return new CustomTagsContributor(); + } + + } + + static class CustomTagsContributor implements WebFluxTagsContributor { + + @Override + public Iterable httpRequestTags(ServerWebExchange exchange, Throwable ex) { + return Tags.of("custom", "testvalue"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/CancelledServerWebExchangeException.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/CancelledServerWebExchangeException.java deleted file mode 100644 index b0e6c40c7d5..00000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/CancelledServerWebExchangeException.java +++ /dev/null @@ -1,29 +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.server; - -/** - * Runtime exception that materializes a {@link reactor.core.publisher.SignalType#CANCEL - * cancel signal} for the WebFlux server metrics instrumentation. - * - * @author Brian Clozel - * @since 2.5.0 - * @see MetricsWebFilter - */ -public class CancelledServerWebExchangeException extends RuntimeException { - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/DefaultWebFluxTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/DefaultWebFluxTagsProvider.java index c3407a3da73..74deb59e472 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/DefaultWebFluxTagsProvider.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/DefaultWebFluxTagsProvider.java @@ -30,7 +30,11 @@ import org.springframework.web.server.ServerWebExchange; * @author Jon Schneider * @author Andy Wilkinson * @since 2.0.0 + * @deprecated since 3.0.0 for removal in 3.2.0 in favor of + * {@link org.springframework.http.observation.reactive.ServerRequestObservationConvention} */ +@Deprecated(since = "3.0.0", forRemoval = true) +@SuppressWarnings("removal") public class DefaultWebFluxTagsProvider implements WebFluxTagsProvider { private final boolean ignoreTrailingSlash; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/MetricsWebFilter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/MetricsWebFilter.java deleted file mode 100644 index f3428763772..00000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/MetricsWebFilter.java +++ /dev/null @@ -1,128 +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.server; - -import java.util.Collections; -import java.util.Set; -import java.util.concurrent.TimeUnit; - -import io.micrometer.core.annotation.Timed; -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 org.reactivestreams.Publisher; -import reactor.core.publisher.Mono; - -import org.springframework.boot.actuate.metrics.AutoTimer; -import org.springframework.boot.actuate.metrics.annotation.TimedAnnotations; -import org.springframework.boot.web.reactive.error.ErrorAttributes; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.http.server.reactive.ServerHttpResponse; -import org.springframework.web.method.HandlerMethod; -import org.springframework.web.reactive.HandlerMapping; -import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.WebFilter; -import org.springframework.web.server.WebFilterChain; - -/** - * Intercepts incoming HTTP requests handled by Spring WebFlux handlers and records - * metrics about execution time and results. - * - * @author Jon Schneider - * @author Brian Clozel - * @since 2.0.0 - */ -@Order(Ordered.HIGHEST_PRECEDENCE + 1) -public class MetricsWebFilter implements WebFilter { - - private static Log logger = LogFactory.getLog(MetricsWebFilter.class); - - private final MeterRegistry registry; - - private final WebFluxTagsProvider tagsProvider; - - private final String metricName; - - private final AutoTimer autoTimer; - - /** - * Create a new {@code MetricsWebFilter}. - * @param registry the registry to which metrics are recorded - * @param tagsProvider provider for metrics tags - * @param metricName name of the metric to record - * @param autoTimer the auto-timers to apply or {@code null} to disable auto-timing - * @since 2.2.0 - */ - public MetricsWebFilter(MeterRegistry registry, WebFluxTagsProvider tagsProvider, String metricName, - AutoTimer autoTimer) { - this.registry = registry; - this.tagsProvider = tagsProvider; - this.metricName = metricName; - this.autoTimer = (autoTimer != null) ? autoTimer : AutoTimer.DISABLED; - } - - @Override - public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { - return chain.filter(exchange).transformDeferred((call) -> filter(exchange, call)); - } - - private Publisher filter(ServerWebExchange exchange, Mono call) { - long start = System.nanoTime(); - return call.doOnEach((signal) -> onTerminalSignal(exchange, signal.getThrowable(), start)) - .doOnCancel(() -> onTerminalSignal(exchange, new CancelledServerWebExchangeException(), start)); - } - - private void onTerminalSignal(ServerWebExchange exchange, Throwable cause, long start) { - ServerHttpResponse response = exchange.getResponse(); - if (response.isCommitted() || cause instanceof CancelledServerWebExchangeException) { - record(exchange, cause, start); - } - else { - response.beforeCommit(() -> { - record(exchange, cause, start); - return Mono.empty(); - }); - } - } - - private void record(ServerWebExchange exchange, Throwable cause, long start) { - try { - cause = (cause != null) ? cause : exchange.getAttribute(ErrorAttributes.ERROR_ATTRIBUTE); - Object handler = exchange.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE); - Set annotations = getTimedAnnotations(handler); - Iterable tags = this.tagsProvider.httpRequestTags(exchange, cause); - long duration = System.nanoTime() - start; - AutoTimer.apply(this.autoTimer, this.metricName, annotations, - (builder) -> builder.description("Duration of HTTP server request handling").tags(tags) - .register(this.registry).record(duration, TimeUnit.NANOSECONDS)); - } - catch (Exception ex) { - logger.warn("Failed to record timer metrics", ex); - // Allow exchange to continue, unaffected by metrics problem - } - } - - private Set getTimedAnnotations(Object handler) { - if (handler instanceof HandlerMethod handlerMethod) { - return TimedAnnotations.get(handlerMethod.getMethod(), handlerMethod.getBeanType()); - } - return Collections.emptySet(); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTags.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTags.java index 7b55fff6b48..756dd7660ab 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTags.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTags.java @@ -40,7 +40,10 @@ import org.springframework.web.util.pattern.PathPattern; * @author Michael McFadyen * @author Brian Clozel * @since 2.0.0 + * @deprecated since 3.0.0 for removal in 3.2.0 in favor of + * {@link org.springframework.http.observation.reactive.ServerRequestObservationConvention} */ +@Deprecated(since = "3.0.0", forRemoval = true) public final class WebFluxTags { private static final Tag URI_NOT_FOUND = Tag.of("uri", "NOT_FOUND"); @@ -176,8 +179,7 @@ public final class WebFluxTags { */ public static Tag outcome(ServerWebExchange exchange, Throwable exception) { if (exception != null) { - if (exception instanceof CancelledServerWebExchangeException - || DISCONNECTED_CLIENT_EXCEPTIONS.contains(exception.getClass().getSimpleName())) { + if (DISCONNECTED_CLIENT_EXCEPTIONS.contains(exception.getClass().getSimpleName())) { return Outcome.UNKNOWN.asTag(); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsContributor.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsContributor.java index a0ff28daa39..a5a745a328e 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsContributor.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsContributor.java @@ -26,8 +26,11 @@ import org.springframework.web.server.ServerWebExchange; * * @author Andy Wilkinson * @since 2.3.0 + * @deprecated since 3.0.0 for removal in 3.2.0 in favor of + * {@link org.springframework.http.observation.reactive.ServerRequestObservationConvention} */ @FunctionalInterface +@Deprecated(since = "3.0.0", forRemoval = true) public interface WebFluxTagsContributor { /** diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsProvider.java index 7ccd79e36e4..b6b2963c513 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsProvider.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsProvider.java @@ -26,8 +26,11 @@ import org.springframework.web.server.ServerWebExchange; * @author Jon Schneider * @author Andy Wilkinson * @since 2.0.0 + * @deprecated since 3.0.0 for removal in 3.2.0 in favor of + * {@link org.springframework.http.observation.reactive.ServerRequestObservationConvention} */ @FunctionalInterface +@Deprecated(since = "3.0.0", forRemoval = true) public interface WebFluxTagsProvider { /** diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/DefaultWebFluxTagsProviderTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/DefaultWebFluxTagsProviderTests.java index c1541223ea1..4298a989777 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/DefaultWebFluxTagsProviderTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/DefaultWebFluxTagsProviderTests.java @@ -37,6 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Andy Wilkinson */ +@SuppressWarnings("removal") class DefaultWebFluxTagsProviderTests { @Test diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/MetricsWebFilterTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/MetricsWebFilterTests.java deleted file mode 100644 index 267279dcec9..00000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/MetricsWebFilterTests.java +++ /dev/null @@ -1,283 +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.server; - -import java.io.EOFException; -import java.time.Duration; -import java.util.concurrent.atomic.AtomicBoolean; - -import io.micrometer.core.annotation.Timed; -import io.micrometer.core.instrument.MockClock; -import io.micrometer.core.instrument.Tag; -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.boot.web.reactive.error.ErrorAttributes; -import org.springframework.mock.http.server.reactive.MockServerHttpRequest; -import org.springframework.mock.web.server.MockServerWebExchange; -import org.springframework.util.ReflectionUtils; -import org.springframework.web.method.HandlerMethod; -import org.springframework.web.reactive.HandlerMapping; -import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.util.pattern.PathPatternParser; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link MetricsWebFilter} - * - * @author Brian Clozel - * @author Madhura Bhave - */ -class MetricsWebFilterTests { - - private static final String REQUEST_METRICS_NAME = "http.server.requests"; - - private static final String REQUEST_METRICS_NAME_PERCENTILE = REQUEST_METRICS_NAME + ".percentile"; - - private final FaultyWebFluxTagsProvider tagsProvider = new FaultyWebFluxTagsProvider(); - - private SimpleMeterRegistry registry; - - private MetricsWebFilter webFilter; - - @BeforeEach - void setup() { - MockClock clock = new MockClock(); - this.registry = new SimpleMeterRegistry(SimpleConfig.DEFAULT, clock); - this.webFilter = new MetricsWebFilter(this.registry, this.tagsProvider, REQUEST_METRICS_NAME, - AutoTimer.ENABLED); - } - - @Test - void filterAddsTagsToRegistry() { - MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}"); - this.webFilter.filter(exchange, (serverWebExchange) -> exchange.getResponse().setComplete()) - .block(Duration.ofSeconds(30)); - assertMetricsContainsTag("uri", "/projects/{project}"); - assertMetricsContainsTag("status", "200"); - } - - @Test - void filterAddsTagsToRegistryForExceptions() { - MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}"); - this.webFilter.filter(exchange, (serverWebExchange) -> Mono.error(new IllegalStateException("test error"))) - .onErrorResume((t) -> { - exchange.getResponse().setRawStatusCode(500); - return exchange.getResponse().setComplete(); - }).block(Duration.ofSeconds(30)); - assertMetricsContainsTag("uri", "/projects/{project}"); - assertMetricsContainsTag("status", "500"); - assertMetricsContainsTag("exception", "IllegalStateException"); - } - - @Test - void filterAddsNonEmptyTagsToRegistryForAnonymousExceptions() { - final Exception anonymous = new Exception("test error") { - }; - - MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}"); - this.webFilter.filter(exchange, (serverWebExchange) -> Mono.error(anonymous)).onErrorResume((t) -> { - exchange.getResponse().setRawStatusCode(500); - return exchange.getResponse().setComplete(); - }).block(Duration.ofSeconds(30)); - assertMetricsContainsTag("uri", "/projects/{project}"); - assertMetricsContainsTag("status", "500"); - assertMetricsContainsTag("exception", anonymous.getClass().getName()); - } - - @Test - void filterAddsTagsToRegistryForHandledExceptions() { - MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}"); - this.webFilter.filter(exchange, (serverWebExchange) -> { - exchange.getAttributes().put(ErrorAttributes.ERROR_ATTRIBUTE, new IllegalStateException("test error")); - return exchange.getResponse().setComplete(); - }).block(Duration.ofSeconds(30)); - assertMetricsContainsTag("uri", "/projects/{project}"); - assertMetricsContainsTag("status", "200"); - assertMetricsContainsTag("exception", "IllegalStateException"); - } - - @Test - void filterAddsTagsToRegistryForExceptionsAndCommittedResponse() { - MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}"); - this.webFilter.filter(exchange, (serverWebExchange) -> { - exchange.getResponse().setRawStatusCode(500); - return exchange.getResponse().setComplete().then(Mono.error(new IllegalStateException("test error"))); - }).onErrorResume((t) -> Mono.empty()).block(Duration.ofSeconds(30)); - assertMetricsContainsTag("uri", "/projects/{project}"); - assertMetricsContainsTag("status", "500"); - } - - @Test - void trailingSlashShouldNotRecordDuplicateMetrics() { - MockServerWebExchange exchange1 = createExchange("/projects/spring-boot", "/projects/{project}"); - MockServerWebExchange exchange2 = createExchange("/projects/spring-boot", "/projects/{project}/"); - this.webFilter.filter(exchange1, (serverWebExchange) -> exchange1.getResponse().setComplete()) - .block(Duration.ofSeconds(30)); - this.webFilter.filter(exchange2, (serverWebExchange) -> exchange2.getResponse().setComplete()) - .block(Duration.ofSeconds(30)); - assertThat(this.registry.get(REQUEST_METRICS_NAME).tag("uri", "/projects/{project}").timer().count()) - .isEqualTo(2); - assertThat(this.registry.get(REQUEST_METRICS_NAME).tag("status", "200").timer().count()).isEqualTo(2); - } - - @Test - void cancelledConnectionsShouldProduceMetrics() { - MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}"); - Mono processing = this.webFilter.filter(exchange, - (serverWebExchange) -> exchange.getResponse().setComplete()); - StepVerifier.create(processing).thenCancel().verify(Duration.ofSeconds(5)); - assertMetricsContainsTag("uri", "/projects/{project}"); - assertMetricsContainsTag("status", "200"); - assertMetricsContainsTag("outcome", "UNKNOWN"); - } - - @Test - void disconnectedExceptionShouldProduceMetrics() { - MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}"); - Mono processing = this.webFilter - .filter(exchange, (serverWebExchange) -> Mono.error(new EOFException("Disconnected"))) - .onErrorResume((t) -> { - exchange.getResponse().setRawStatusCode(500); - return exchange.getResponse().setComplete(); - }); - StepVerifier.create(processing).expectComplete().verify(Duration.ofSeconds(5)); - assertMetricsContainsTag("uri", "/projects/{project}"); - assertMetricsContainsTag("status", "500"); - assertMetricsContainsTag("outcome", "UNKNOWN"); - } - - @Test - void filterAddsStandardTags() { - MockServerWebExchange exchange = createTimedHandlerMethodExchange("timed"); - this.webFilter.filter(exchange, (serverWebExchange) -> exchange.getResponse().setComplete()) - .block(Duration.ofSeconds(30)); - assertMetricsContainsTag("uri", "/projects/{project}"); - assertMetricsContainsTag("status", "200"); - } - - @Test - void filterAddsExtraTags() { - MockServerWebExchange exchange = createTimedHandlerMethodExchange("timedExtraTags"); - this.webFilter.filter(exchange, (serverWebExchange) -> exchange.getResponse().setComplete()) - .block(Duration.ofSeconds(30)); - assertMetricsContainsTag("uri", "/projects/{project}"); - assertMetricsContainsTag("status", "200"); - assertMetricsContainsTag("tag1", "value1"); - assertMetricsContainsTag("tag2", "value2"); - } - - @Test - void filterAddsExtraTagsAndException() { - MockServerWebExchange exchange = createTimedHandlerMethodExchange("timedExtraTags"); - this.webFilter.filter(exchange, (serverWebExchange) -> Mono.error(new IllegalStateException("test error"))) - .onErrorResume((ex) -> { - exchange.getResponse().setRawStatusCode(500); - return exchange.getResponse().setComplete(); - }).block(Duration.ofSeconds(30)); - assertMetricsContainsTag("uri", "/projects/{project}"); - assertMetricsContainsTag("status", "500"); - assertMetricsContainsTag("exception", "IllegalStateException"); - assertMetricsContainsTag("tag1", "value1"); - assertMetricsContainsTag("tag2", "value2"); - } - - @Test - void filterAddsPercentileMeters() { - MockServerWebExchange exchange = createTimedHandlerMethodExchange("timedPercentiles"); - this.webFilter.filter(exchange, (serverWebExchange) -> exchange.getResponse().setComplete()) - .block(Duration.ofSeconds(30)); - assertMetricsContainsTag("uri", "/projects/{project}"); - assertMetricsContainsTag("status", "200"); - assertThat(this.registry.get(REQUEST_METRICS_NAME_PERCENTILE).tag("phi", "0.95").gauge().value()).isNotZero(); - assertThat(this.registry.get(REQUEST_METRICS_NAME_PERCENTILE).tag("phi", "0.5").gauge().value()).isNotZero(); - } - - @Test - void whenMetricsRecordingFailsThenExchangeFilteringSucceeds() { - MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}"); - this.tagsProvider.failOnce(); - this.webFilter.filter(exchange, (serverWebExchange) -> exchange.getResponse().setComplete()) - .block(Duration.ofSeconds(30)); - } - - private MockServerWebExchange createTimedHandlerMethodExchange(String methodName) { - MockServerWebExchange exchange = createExchange("/projects/spring-boot", "/projects/{project}"); - exchange.getAttributes().put(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE, - new HandlerMethod(this, ReflectionUtils.findMethod(Handlers.class, methodName))); - return exchange; - } - - private MockServerWebExchange createExchange(String path, String pathPattern) { - PathPatternParser parser = new PathPatternParser(); - MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get(path).build()); - exchange.getAttributes().put(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, parser.parse(pathPattern)); - return exchange; - } - - private void assertMetricsContainsTag(String tagKey, String tagValue) { - assertThat(this.registry.get(REQUEST_METRICS_NAME).tag(tagKey, tagValue).timer().count()).isEqualTo(1); - } - - static class Handlers { - - @Timed - Mono timed() { - return Mono.just("test"); - } - - @Timed(extraTags = { "tag1", "value1", "tag2", "value2" }) - Mono timedExtraTags() { - return Mono.just("test"); - } - - @Timed(percentiles = { 0.5, 0.95 }) - Mono timedPercentiles() { - return Mono.just("test"); - } - - } - - class FaultyWebFluxTagsProvider extends DefaultWebFluxTagsProvider { - - private final AtomicBoolean fail = new AtomicBoolean(); - - FaultyWebFluxTagsProvider() { - super(true); - } - - @Override - public Iterable httpRequestTags(ServerWebExchange exchange, Throwable exception) { - if (this.fail.compareAndSet(true, false)) { - throw new RuntimeException(); - } - return super.httpRequestTags(exchange, exception); - } - - 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/server/WebFluxTagsTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsTests.java index c82b1a22e3d..ff741118080 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/reactive/server/WebFluxTagsTests.java @@ -44,6 +44,7 @@ import static org.mockito.Mockito.mock; * @author Madhura Bhave * @author Stephane Nicoll */ +@SuppressWarnings("removal") class WebFluxTagsTests { private MockServerWebExchange exchange; @@ -215,10 +216,4 @@ class WebFluxTagsTests { assertThat(tag.getValue()).isEqualTo("UNKNOWN"); } - @Test - void outcomeTagIsUnknownWhenExceptionIsCancelledExchange() { - Tag tag = WebFluxTags.outcome(this.exchange, new CancelledServerWebExchangeException()); - assertThat(tag.getValue()).isEqualTo("UNKNOWN"); - } - }