From b2fe807d47b68de048d63687e9b0d6f3bcefd6d5 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 19 Oct 2022 19:33:31 +0200 Subject: [PATCH] Move MVC metrics to Observation auto-configuration This commit moves the entire Metrics auto-configuration for Spring MVC to the new `Observation` API and the instrumentation contributed in Spring Framework. Closes gh-32538 --- .../metrics/MetricsProperties.java | 27 +- .../JerseyServerMetricsAutoConfiguration.java | 6 +- .../WebFluxMetricsAutoConfiguration.java | 7 +- .../WebMvcMetricsAutoConfiguration.java | 131 ---- .../observation/ObservationProperties.java | 34 ++ ...erRequestObservationConventionAdapter.java | 79 +++ .../WebMvcObservationAutoConfiguration.java | 121 ++++ .../web/servlet/package-info.java | 4 +- ...itional-spring-configuration-metadata.json | 26 +- ...ot.autoconfigure.AutoConfiguration.imports | 2 +- ...eyServerMetricsAutoConfigurationTests.java | 9 +- .../metrics/test/MetricsIntegrationTests.java | 18 +- .../WebFluxMetricsAutoConfigurationTests.java | 2 + ...uestObservationConventionAdapterTests.java | 108 ++++ ...MvcObservationAutoConfigurationTests.java} | 137 ++--- .../servlet/DefaultWebMvcTagsProvider.java | 4 + .../LongTaskTimingHandlerInterceptor.java | 165 ----- .../web/servlet/WebMvcMetricsFilter.java | 190 ------ .../metrics/web/servlet/WebMvcTags.java | 3 + .../web/servlet/WebMvcTagsContributor.java | 3 + .../web/servlet/WebMvcTagsProvider.java | 3 + .../DefaultWebMvcTagsProviderTests.java | 1 + .../endpoint/web/servlet/WebMvcTagsTests.java | 1 + .../web/servlet/FaultyWebMvcTagsProvider.java | 60 -- ...LongTaskTimingHandlerInterceptorTests.java | 216 ------- .../WebMvcMetricsFilterAutoTimedTests.java | 120 ---- .../web/servlet/WebMvcMetricsFilterTests.java | 573 ------------------ .../WebMvcMetricsIntegrationTests.java | 163 ----- .../src/docs/asciidoc/actuator/metrics.adoc | 15 +- 29 files changed, 455 insertions(+), 1773 deletions(-) delete mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/servlet/WebMvcMetricsAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/ServerRequestObservationConventionAdapter.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java rename spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/{metrics => observation}/web/servlet/package-info.java (82%) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/ServerRequestObservationConventionAdapterTests.java rename spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/{metrics/web/servlet/WebMvcMetricsAutoConfigurationTests.java => observation/web/servlet/WebMvcObservationAutoConfigurationTests.java} (60%) delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/LongTaskTimingHandlerInterceptor.java delete mode 100644 spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsFilter.java delete mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/FaultyWebMvcTagsProvider.java delete mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/LongTaskTimingHandlerInterceptorTests.java delete mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsFilterAutoTimedTests.java delete mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsFilterTests.java delete mode 100644 spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsIntegrationTests.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 eea391d8a4f..3999d164717 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 @@ -194,37 +194,18 @@ public class MetricsProperties { */ private String metricName = "http.server.requests"; - /** - * Whether the trailing slash should be ignored when recording metrics. - */ - private boolean ignoreTrailingSlash = true; - - /** - * Auto-timed request settings. - */ - @NestedConfigurationProperty - private final AutoTimeProperties autotime = new AutoTimeProperties(); - - public AutoTimeProperties getAutotime() { - return this.autotime; - } - + @Deprecated(since = "3.0.0", forRemoval = true) + @DeprecatedConfigurationProperty(replacement = "management.observations.http.server.requests.name") public String getMetricName() { return this.metricName; } + @Deprecated(since = "3.0.0", forRemoval = true) + @DeprecatedConfigurationProperty(replacement = "management.observations.http.server.requests.name") public void setMetricName(String metricName) { this.metricName = metricName; } - public boolean isIgnoreTrailingSlash() { - return this.ignoreTrailingSlash; - } - - public void setIgnoreTrailingSlash(boolean ignoreTrailingSlash) { - this.ignoreTrailingSlash = ignoreTrailingSlash; - } - } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfiguration.java index 382d71daf91..fdb018f2276 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfiguration.java @@ -57,6 +57,7 @@ import org.springframework.core.annotation.Order; @ConditionalOnClass({ ResourceConfig.class, MetricsApplicationEventListener.class }) @ConditionalOnBean({ MeterRegistry.class, ResourceConfig.class }) @EnableConfigurationProperties(MetricsProperties.class) +@SuppressWarnings("removal") public class JerseyServerMetricsAutoConfiguration { private final MetricsProperties properties; @@ -75,9 +76,8 @@ public class JerseyServerMetricsAutoConfiguration { public ResourceConfigCustomizer jerseyServerMetricsResourceConfigCustomizer(MeterRegistry meterRegistry, JerseyTagsProvider tagsProvider) { Server server = this.properties.getWeb().getServer(); - return (config) -> config.register( - new MetricsApplicationEventListener(meterRegistry, tagsProvider, server.getRequest().getMetricName(), - server.getRequest().getAutotime().isEnabled(), new AnnotationUtilsAnnotationFinder())); + return (config) -> config.register(new MetricsApplicationEventListener(meterRegistry, tagsProvider, + server.getRequest().getMetricName(), true, new AnnotationUtilsAnnotationFinder())); } @Bean 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 index 80c2152ef30..051b1f97a96 100644 --- 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 @@ -51,6 +51,7 @@ import org.springframework.core.annotation.Order; SimpleMetricsExportAutoConfiguration.class }) @ConditionalOnBean(MeterRegistry.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) +@SuppressWarnings("removal") public class WebFluxMetricsAutoConfiguration { private final MetricsProperties properties; @@ -62,15 +63,13 @@ public class WebFluxMetricsAutoConfiguration { @Bean @ConditionalOnMissingBean(WebFluxTagsProvider.class) public DefaultWebFluxTagsProvider webFluxTagsProvider(ObjectProvider contributors) { - return new DefaultWebFluxTagsProvider(this.properties.getWeb().getServer().getRequest().isIgnoreTrailingSlash(), - contributors.orderedStream().toList()); + 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(request.getAutotime())); + return new MetricsWebFilter(registry, tagConfigurer, request.getMetricName(), new PropertiesAutoTimer(null)); } @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/servlet/WebMvcMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/servlet/WebMvcMetricsAutoConfiguration.java deleted file mode 100644 index c05a9f9d954..00000000000 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/servlet/WebMvcMetricsAutoConfiguration.java +++ /dev/null @@ -1,131 +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.servlet; - -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.config.MeterFilter; -import jakarta.servlet.DispatcherType; - -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.servlet.DefaultWebMvcTagsProvider; -import org.springframework.boot.actuate.metrics.web.servlet.LongTaskTimingHandlerInterceptor; -import org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter; -import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsContributor; -import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsProvider; -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.autoconfigure.web.servlet.ConditionalOnMissingFilterBean; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.web.servlet.DispatcherServlet; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -/** - * {@link EnableAutoConfiguration Auto-configuration} for instrumentation of Spring Web - * MVC servlet-based request mappings. - * - * @author Jon Schneider - * @author Dmytro Nosan - * @since 2.0.0 - */ -@AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, - SimpleMetricsExportAutoConfiguration.class }) -@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) -@ConditionalOnClass(DispatcherServlet.class) -@ConditionalOnBean(MeterRegistry.class) -@EnableConfigurationProperties(MetricsProperties.class) -public class WebMvcMetricsAutoConfiguration { - - private final MetricsProperties properties; - - public WebMvcMetricsAutoConfiguration(MetricsProperties properties) { - this.properties = properties; - } - - @Bean - @ConditionalOnMissingBean(WebMvcTagsProvider.class) - public DefaultWebMvcTagsProvider webMvcTagsProvider(ObjectProvider contributors) { - return new DefaultWebMvcTagsProvider(this.properties.getWeb().getServer().getRequest().isIgnoreTrailingSlash(), - contributors.orderedStream().toList()); - } - - @Bean - @ConditionalOnMissingFilterBean - public FilterRegistrationBean webMvcMetricsFilter(MeterRegistry registry, - WebMvcTagsProvider tagsProvider) { - ServerRequest request = this.properties.getWeb().getServer().getRequest(); - WebMvcMetricsFilter filter = new WebMvcMetricsFilter(registry, tagsProvider, request.getMetricName(), - new PropertiesAutoTimer(request.getAutotime())); - FilterRegistrationBean registration = new FilterRegistrationBean<>(filter); - registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1); - registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC); - return registration; - } - - @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); - } - - @Bean - public MetricsWebMvcConfigurer metricsWebMvcConfigurer(MeterRegistry meterRegistry, - WebMvcTagsProvider tagsProvider) { - return new MetricsWebMvcConfigurer(meterRegistry, tagsProvider); - } - - /** - * {@link WebMvcConfigurer} to add metrics interceptors. - */ - static class MetricsWebMvcConfigurer implements WebMvcConfigurer { - - private final MeterRegistry meterRegistry; - - private final WebMvcTagsProvider tagsProvider; - - MetricsWebMvcConfigurer(MeterRegistry meterRegistry, WebMvcTagsProvider tagsProvider) { - this.meterRegistry = meterRegistry; - this.tagsProvider = tagsProvider; - } - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new LongTaskTimingHandlerInterceptor(this.meterRegistry, this.tagsProvider)); - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java index 2124c9855e3..e4668d7f4b5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/ObservationProperties.java @@ -38,10 +38,16 @@ public class ObservationProperties { private final Client client = new Client(); + private final Server server = new Server(); + public Client getClient() { return this.client; } + public Server getServer() { + return this.server; + } + public static class Client { private final ClientRequests requests = new ClientRequests(); @@ -70,6 +76,34 @@ public class ObservationProperties { } + public static class Server { + + private final ServerRequests requests = new ServerRequests(); + + public ServerRequests getRequests() { + return this.requests; + } + + public static class ServerRequests { + + /** + * Name of the observation for server requests. If empty, will use the + * default "http.server.requests". + */ + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + } + + } + } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/ServerRequestObservationConventionAdapter.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/ServerRequestObservationConventionAdapter.java new file mode 100644 index 00000000000..8a3591ac900 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/ServerRequestObservationConventionAdapter.java @@ -0,0 +1,79 @@ +/* + * 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.servlet; + +import java.util.List; + +import io.micrometer.common.KeyValues; +import io.micrometer.core.instrument.Tag; +import io.micrometer.observation.Observation; + +import org.springframework.boot.actuate.metrics.web.servlet.DefaultWebMvcTagsProvider; +import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsContributor; +import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsProvider; +import org.springframework.http.observation.ServerRequestObservationContext; +import org.springframework.http.observation.ServerRequestObservationConvention; +import org.springframework.util.Assert; +import org.springframework.web.servlet.HandlerMapping; + +/** + * Adapter class that applies {@link WebMvcTagsProvider} tags as a + * {@link ServerRequestObservationConvention}. + * + * @author Brian Clozel + */ +@SuppressWarnings({ "deprecation", "removal" }) +class ServerRequestObservationConventionAdapter implements ServerRequestObservationConvention { + + private final String observationName; + + private final WebMvcTagsProvider tagsProvider; + + ServerRequestObservationConventionAdapter(String observationName, WebMvcTagsProvider tagsProvider, + List contributors) { + Assert.state((tagsProvider != null) || (contributors != null), + "adapter should adapt to a WebMvcTagsProvider or a list of contributors"); + this.observationName = observationName; + this.tagsProvider = (tagsProvider != null) ? tagsProvider : new DefaultWebMvcTagsProvider(contributors); + } + + @Override + public String getName() { + return this.observationName; + } + + @Override + public boolean supportsContext(Observation.Context context) { + return context instanceof ServerRequestObservationContext; + } + + @Override + public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) { + KeyValues keyValues = KeyValues.empty(); + Iterable tags = this.tagsProvider.getTags(context.getCarrier(), context.getResponse(), getHandler(context), + context.getError()); + for (Tag tag : tags) { + keyValues = keyValues.and(tag.getKey(), tag.getValue()); + } + return keyValues; + } + + private Object getHandler(ServerRequestObservationContext context) { + return context.getCarrier().getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java new file mode 100644 index 00000000000..3a17a20c895 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java @@ -0,0 +1,121 @@ +/* + * 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.servlet; + +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 jakarta.servlet.DispatcherType; + +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.servlet.WebMvcTagsContributor; +import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsProvider; +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.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.web.servlet.ConditionalOnMissingFilterBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.observation.DefaultServerRequestObservationConvention; +import org.springframework.http.observation.ServerRequestObservationConvention; +import org.springframework.web.filter.ServerHttpObservationFilter; +import org.springframework.web.servlet.DispatcherServlet; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for instrumentation of Spring Web + * MVC servlet-based request mappings. + * + * @author Brian Clozel + * @author Jon Schneider + * @author Dmytro Nosan + * @since 3.0.0 + */ +@AutoConfiguration(after = { MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, + SimpleMetricsExportAutoConfiguration.class, ObservationAutoConfiguration.class }) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +@ConditionalOnClass({ DispatcherServlet.class, Observation.class }) +@ConditionalOnBean(ObservationRegistry.class) +@EnableConfigurationProperties({ MetricsProperties.class, ObservationProperties.class }) +@SuppressWarnings("removal") +public class WebMvcObservationAutoConfiguration { + + private final MetricsProperties metricsProperties; + + private final ObservationProperties observationProperties; + + public WebMvcObservationAutoConfiguration(ObservationProperties observationProperties, + MetricsProperties metricsProperties) { + this.observationProperties = observationProperties; + this.metricsProperties = metricsProperties; + } + + @Bean + @ConditionalOnMissingFilterBean + public FilterRegistrationBean webMvcObservationFilter(ObservationRegistry registry, + ObjectProvider customTagsProvider, + ObjectProvider contributorsProvider) { + + String observationName = this.observationProperties.getHttp().getServer().getRequests().getName(); + String metricName = this.metricsProperties.getWeb().getServer().getRequest().getMetricName(); + String name = (observationName != null) ? observationName : metricName; + ServerRequestObservationConvention convention = new DefaultServerRequestObservationConvention(name); + WebMvcTagsProvider tagsProvider = customTagsProvider.getIfAvailable(); + List contributors = contributorsProvider.orderedStream().toList(); + if (tagsProvider != null || contributors.size() > 0) { + convention = new ServerRequestObservationConventionAdapter(name, tagsProvider, contributors); + } + ServerHttpObservationFilter filter = new ServerHttpObservationFilter(registry, convention); + FilterRegistrationBean registration = new FilterRegistrationBean<>(filter); + registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1); + registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC); + return registration; + } + + @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/servlet/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/package-info.java similarity index 82% rename from spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/servlet/package-info.java rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/package-info.java index fbfbefb7fc0..fda593804c0 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/web/servlet/package-info.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/package-info.java @@ -15,6 +15,6 @@ */ /** - * Auto-configuration for Spring MVC actuator metrics. + * Auto-configuration for Spring MVC observation support. */ -package org.springframework.boot.actuate.autoconfigure.metrics.web.servlet; +package org.springframework.boot.actuate.autoconfigure.observation.web.servlet; 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 0cf1d1e133d..85188e5535b 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 @@ -1947,16 +1947,36 @@ { "name": "management.metrics.web.server.request.autotime.enabled", "description": "Whether to automatically time web server requests.", - "defaultValue": true + "defaultValue": true, + "deprecation": { + "level": "error", + "reason": "Should be applied at the ObservationRegistry level." + } }, { "name": "management.metrics.web.server.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.server.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.server.request.ignore-trailing-slash", + "type": "java.lang.Boolean", + "deprecation": { + "level": "error", + "reason": "Not needed anymore, direct instrumentation in Spring MVC." + } }, { "name": "management.metrics.web.server.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 2bdd69ff79b..f5084d63b51 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 @@ -80,12 +80,12 @@ org.springframework.boot.actuate.autoconfigure.metrics.task.TaskExecutorMetricsA 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 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 org.springframework.boot.actuate.autoconfigure.neo4j.Neo4jHealthContributorAutoConfiguration org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration +org.springframework.boot.actuate.autoconfigure.observation.web.servlet.WebMvcObservationAutoConfiguration org.springframework.boot.actuate.autoconfigure.quartz.QuartzEndpointAutoConfiguration org.springframework.boot.actuate.autoconfigure.r2dbc.ConnectionFactoryHealthContributorAutoConfiguration org.springframework.boot.actuate.autoconfigure.data.redis.RedisHealthContributorAutoConfiguration diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfigurationTests.java index 56ec7a79a16..561f96be667 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/jersey/JerseyServerMetricsAutoConfigurationTests.java @@ -34,6 +34,7 @@ import org.junit.jupiter.api.Test; 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.test.MetricsRun; +import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration; import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer; @@ -61,10 +62,10 @@ class JerseyServerMetricsAutoConfigurationTests { .withConfiguration(AutoConfigurations.of(JerseyServerMetricsAutoConfiguration.class)); private final WebApplicationContextRunner webContextRunner = new WebApplicationContextRunner( - AnnotationConfigServletWebServerApplicationContext::new) - .withConfiguration(AutoConfigurations.of(JerseyAutoConfiguration.class, - JerseyServerMetricsAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class, - SimpleMetricsExportAutoConfiguration.class, MetricsAutoConfiguration.class)) + AnnotationConfigServletWebServerApplicationContext::new).withConfiguration( + AutoConfigurations.of(JerseyAutoConfiguration.class, JerseyServerMetricsAutoConfiguration.class, + ServletWebServerFactoryAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class, + ObservationAutoConfiguration.class, MetricsAutoConfiguration.class)) .withUserConfiguration(ResourceConfiguration.class).withPropertyValues("server.port:0"); @Test 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 02846519ab6..788c62ac733 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 @@ -43,9 +43,8 @@ import org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoo import org.springframework.boot.actuate.autoconfigure.metrics.orm.jpa.HibernateMetricsAutoConfiguration; 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; -import org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter; +import org.springframework.boot.actuate.autoconfigure.observation.web.servlet.WebMvcObservationAutoConfiguration; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; @@ -70,6 +69,7 @@ import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; +import org.springframework.web.filter.ServerHttpObservationFilter; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.waitAtMost; @@ -128,9 +128,9 @@ class MetricsIntegrationTests { void metricsFilterRegisteredForAsyncDispatches() { Map filterRegistrations = this.context .getBeansOfType(FilterRegistrationBean.class); - assertThat(filterRegistrations).containsKey("webMvcMetricsFilter"); - FilterRegistrationBean registration = filterRegistrations.get("webMvcMetricsFilter"); - assertThat(registration.getFilter()).isInstanceOf(WebMvcMetricsFilter.class); + assertThat(filterRegistrations).containsKey("webMvcObservationFilter"); + FilterRegistrationBean registration = filterRegistrations.get("webMvcObservationFilter"); + assertThat(registration.getFilter()).isInstanceOf(ServerHttpObservationFilter.class); assertThat((Set) ReflectionTestUtils.getField(registration, "dispatcherTypes")) .containsExactlyInAnyOrder(DispatcherType.REQUEST, DispatcherType.ASYNC); } @@ -141,10 +141,10 @@ class MetricsIntegrationTests { SystemMetricsAutoConfiguration.class, RabbitMetricsAutoConfiguration.class, CacheMetricsAutoConfiguration.class, DataSourcePoolMetricsAutoConfiguration.class, HibernateMetricsAutoConfiguration.class, HttpClientObservationsAutoConfiguration.class, - WebFluxMetricsAutoConfiguration.class, WebMvcMetricsAutoConfiguration.class, JacksonAutoConfiguration.class, - HttpMessageConvertersAutoConfiguration.class, RestTemplateAutoConfiguration.class, - WebMvcAutoConfiguration.class, DispatcherServletAutoConfiguration.class, - ServletWebServerFactoryAutoConfiguration.class }) + WebFluxMetricsAutoConfiguration.class, WebMvcObservationAutoConfiguration.class, + JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, + RestTemplateAutoConfiguration.class, WebMvcAutoConfiguration.class, + DispatcherServletAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class }) @Import(PersonController.class) static class MetricsApp { 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 index 45352944a9f..14291de5107 100644 --- 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 @@ -17,6 +17,7 @@ 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; @@ -47,6 +48,7 @@ import static org.mockito.Mockito.mock; * @author Madhura Bhave */ @ExtendWith(OutputCaptureExtension.class) +@Disabled("until gh-32539 is fixed") class WebFluxMetricsAutoConfigurationTests { private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/ServerRequestObservationConventionAdapterTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/ServerRequestObservationConventionAdapterTests.java new file mode 100644 index 00000000000..018d67daa14 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/ServerRequestObservationConventionAdapterTests.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.servlet; + +import java.util.Collections; +import java.util.List; + +import io.micrometer.common.KeyValue; +import io.micrometer.core.instrument.Tag; +import io.micrometer.observation.Observation; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.metrics.web.servlet.DefaultWebMvcTagsProvider; +import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsContributor; +import org.springframework.http.observation.ServerRequestObservationContext; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.web.servlet.HandlerMapping; + +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 ServerRequestObservationConventionAdapter convention = new ServerRequestObservationConventionAdapter( + TEST_METRIC_NAME, new DefaultWebMvcTagsProvider(), Collections.emptyList()); + + private MockHttpServletRequest request = new MockHttpServletRequest("GET", "/resource/test"); + + private MockHttpServletResponse response = new MockHttpServletResponse(); + + private ServerRequestObservationContext context = new ServerRequestObservationContext(this.request, this.response); + + @Test + void customNameIsUsed() { + assertThat(this.convention.getName()).isEqualTo(TEST_METRIC_NAME); + } + + @Test + void onlySupportServerRequestObservationContext() { + assertThat(this.convention.supportsContext(this.context)).isTrue(); + assertThat(this.convention.supportsContext(new OtherContext())).isFalse(); + } + + @Test + void pushTagsAsLowCardinalityKeyValues() { + this.request.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, "/resource/{name}"); + this.context.setPathPattern("/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")); + } + + @Test + void doesNotPushAnyHighCardinalityKeyValue() { + assertThat(this.convention.getHighCardinalityKeyValues(this.context)).isEmpty(); + } + + @Test + void pushTagsFromContributors() { + ServerRequestObservationConventionAdapter convention = new ServerRequestObservationConventionAdapter( + TEST_METRIC_NAME, null, List.of(new CustomWebMvcContributor())); + assertThat(convention.getLowCardinalityKeyValues(this.context)).contains(KeyValue.of("custom", "value")); + } + + static class OtherContext extends Observation.Context { + + } + + static class CustomWebMvcContributor implements WebMvcTagsContributor { + + @Override + public Iterable getTags(HttpServletRequest request, HttpServletResponse response, Object handler, + Throwable exception) { + return List.of(Tag.of("custom", "value")); + } + + @Override + public Iterable getLongRequestTags(HttpServletRequest request, Object handler) { + return Collections.emptyList(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/servlet/WebMvcMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java similarity index 60% rename from spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/servlet/WebMvcMetricsAutoConfigurationTests.java rename to spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java index 82e0d7c1dc9..cd53d141001 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/web/servlet/WebMvcMetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java @@ -14,17 +14,14 @@ * limitations under the License. */ -package org.springframework.boot.actuate.autoconfigure.metrics.web.servlet; +package org.springframework.boot.actuate.autoconfigure.observation.web.servlet; -import java.util.Collection; import java.util.Collections; import java.util.EnumSet; -import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tag; -import io.micrometer.core.instrument.Timer; -import io.micrometer.core.instrument.distribution.HistogramSnapshot; +import io.micrometer.observation.tck.TestObservationRegistry; import jakarta.servlet.DispatcherType; import jakarta.servlet.Filter; import jakarta.servlet.http.HttpServletRequest; @@ -35,9 +32,8 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; import org.springframework.boot.actuate.autoconfigure.metrics.web.TestController; +import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; import org.springframework.boot.actuate.metrics.web.servlet.DefaultWebMvcTagsProvider; -import org.springframework.boot.actuate.metrics.web.servlet.LongTaskTimingHandlerInterceptor; -import org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter; import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsContributor; import org.springframework.boot.actuate.metrics.web.servlet.WebMvcTagsProvider; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -53,14 +49,14 @@ import org.springframework.core.Ordered; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.filter.ServerHttpObservationFilter; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** - * Tests for {@link WebMvcMetricsAutoConfiguration}. + * Tests for {@link WebMvcObservationAutoConfiguration}. * * @author Andy Wilkinson * @author Dmytro Nosan @@ -69,45 +65,36 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. * @author Chanhyeong LEE */ @ExtendWith(OutputCaptureExtension.class) -class WebMvcMetricsAutoConfigurationTests { +@SuppressWarnings("removal") +class WebMvcObservationAutoConfigurationTests { private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() - .with(MetricsRun.simple()).withConfiguration(AutoConfigurations.of(WebMvcMetricsAutoConfiguration.class)); + .with(MetricsRun.simple()).withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(WebMvcObservationAutoConfiguration.class)); @Test void backsOffWhenMeterRegistryIsMissing() { - new WebApplicationContextRunner().withConfiguration(AutoConfigurations.of(WebMvcMetricsAutoConfiguration.class)) - .run((context) -> assertThat(context).doesNotHaveBean(WebMvcTagsProvider.class)); + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(WebMvcObservationAutoConfiguration.class)) + .run((context) -> assertThat(context).doesNotHaveBean(FilterRegistrationBean.class)); } @Test - void definesTagsProviderAndFilterWhenMeterRegistryIsPresent() { + void definesFilterWhenRegistryIsPresent() { this.contextRunner.run((context) -> { - assertThat(context).hasSingleBean(DefaultWebMvcTagsProvider.class); - assertThat(context.getBean(DefaultWebMvcTagsProvider.class)).extracting("ignoreTrailingSlash") - .isEqualTo(true); + assertThat(context).doesNotHaveBean(DefaultWebMvcTagsProvider.class); assertThat(context).hasSingleBean(FilterRegistrationBean.class); assertThat(context.getBean(FilterRegistrationBean.class).getFilter()) - .isInstanceOf(WebMvcMetricsFilter.class); + .isInstanceOf(ServerHttpObservationFilter.class); }); } @Test - void tagsProviderWhenIgnoreTrailingSlashIsFalse() { - this.contextRunner.withPropertyValues("management.metrics.web.server.request.ignore-trailing-slash=false") - .run((context) -> { - assertThat(context).hasSingleBean(DefaultWebMvcTagsProvider.class); - assertThat(context.getBean(DefaultWebMvcTagsProvider.class)).extracting("ignoreTrailingSlash") - .isEqualTo(false); - }); - } - - @Test - void tagsProviderBacksOff() { - this.contextRunner.withUserConfiguration(TagsProviderConfiguration.class).run((context) -> { - assertThat(context).doesNotHaveBean(DefaultWebMvcTagsProvider.class); - assertThat(context).hasSingleBean(TestWebMvcTagsProvider.class); - }); + void adapterConventionWhenTagsProviderPresent() { + this.contextRunner.withUserConfiguration(TagsProviderConfiguration.class) + .run((context) -> assertThat(context.getBean(FilterRegistrationBean.class).getFilter()) + .extracting("observationConvention") + .isInstanceOf(ServerRequestObservationConventionAdapter.class)); } @Test @@ -121,41 +108,42 @@ class WebMvcMetricsAutoConfigurationTests { } @Test - void filterRegistrationBacksOffWithAnotherWebMvcMetricsFilterRegistration() { - this.contextRunner.withUserConfiguration(TestWebMvcMetricsFilterRegistrationConfiguration.class) + void filterRegistrationBacksOffWithAnotherServerHttpObservationFilterRegistration() { + this.contextRunner.withUserConfiguration(TestServerHttpObservationFilterRegistrationConfiguration.class) .run((context) -> { assertThat(context).hasSingleBean(FilterRegistrationBean.class); assertThat(context.getBean(FilterRegistrationBean.class)) - .isSameAs(context.getBean("testWebMvcMetricsFilter")); + .isSameAs(context.getBean("testServerHttpObservationFilter")); }); } @Test - void filterRegistrationBacksOffWithAnotherWebMvcMetricsFilter() { - this.contextRunner.withUserConfiguration(TestWebMvcMetricsFilterConfiguration.class) + void filterRegistrationBacksOffWithAnotherServerHttpObservationFilter() { + this.contextRunner.withUserConfiguration(TestServerHttpObservationFilterConfiguration.class) .run((context) -> assertThat(context).doesNotHaveBean(FilterRegistrationBean.class) - .hasSingleBean(WebMvcMetricsFilter.class)); + .hasSingleBean(ServerHttpObservationFilter.class)); } @Test void filterRegistrationDoesNotBackOffWithOtherFilterRegistration() { this.contextRunner.withUserConfiguration(TestFilterRegistrationConfiguration.class) - .run((context) -> assertThat(context).hasBean("testFilter").hasBean("webMvcMetricsFilter")); + .run((context) -> assertThat(context).hasBean("testFilter").hasBean("webMvcObservationFilter")); } @Test void filterRegistrationDoesNotBackOffWithOtherFilter() { this.contextRunner.withUserConfiguration(TestFilterConfiguration.class) - .run((context) -> assertThat(context).hasBean("testFilter").hasBean("webMvcMetricsFilter")); + .run((context) -> assertThat(context).hasBean("testFilter").hasBean("webMvcObservationFilter")); } @Test void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) { this.contextRunner.withUserConfiguration(TestController.class) - .withConfiguration(AutoConfigurations.of(MetricsAutoConfiguration.class, WebMvcAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(MetricsAutoConfiguration.class, + ObservationAutoConfiguration.class, WebMvcAutoConfiguration.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(registry.get("http.server.requests").meters().size()).isLessThanOrEqualTo(2); assertThat(output).contains("Reached the maximum number of URI tags for 'http.server.requests'"); }); } @@ -163,7 +151,8 @@ class WebMvcMetricsAutoConfigurationTests { @Test void shouldNotDenyNorLogIfMaxUrisIsNotReached(CapturedOutput output) { this.contextRunner.withUserConfiguration(TestController.class) - .withConfiguration(AutoConfigurations.of(MetricsAutoConfiguration.class, WebMvcAutoConfiguration.class)) + .withConfiguration(AutoConfigurations.of(MetricsAutoConfiguration.class, + ObservationAutoConfiguration.class, WebMvcAutoConfiguration.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); @@ -172,52 +161,12 @@ class WebMvcMetricsAutoConfigurationTests { }); } - @Test - void autoTimeRequestsCanBeConfigured() { - this.contextRunner.withUserConfiguration(TestController.class) - .withConfiguration(AutoConfigurations.of(MetricsAutoConfiguration.class, WebMvcAutoConfiguration.class)) - .withPropertyValues("management.metrics.web.server.request.autotime.enabled=true", - "management.metrics.web.server.request.autotime.percentiles=0.5,0.7", - "management.metrics.web.server.request.autotime.percentiles-histogram=true") - .run((context) -> { - MeterRegistry registry = getInitializedMeterRegistry(context); - Timer timer = registry.get("http.server.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); - }); - } - - @Test - void timerWorksWithTimedAnnotationsWhenAutoTimeRequestsIsFalse() { - this.contextRunner.withUserConfiguration(TestController.class) - .withConfiguration(AutoConfigurations.of(MetricsAutoConfiguration.class, WebMvcAutoConfiguration.class)) - .withPropertyValues("management.metrics.web.server.request.autotime.enabled=false").run((context) -> { - MeterRegistry registry = getInitializedMeterRegistry(context, "/test3"); - Collection meters = registry.get("http.server.requests").meters(); - assertThat(meters).hasSize(1); - Meter meter = meters.iterator().next(); - assertThat(meter.getId().getTag("uri")).isEqualTo("/test3"); - }); - } - - @Test - @SuppressWarnings("rawtypes") - void longTaskTimingInterceptorIsRegistered() { - this.contextRunner.withUserConfiguration(TestController.class) - .withConfiguration(AutoConfigurations.of(MetricsAutoConfiguration.class, WebMvcAutoConfiguration.class)) - .run((context) -> assertThat(context.getBean(RequestMappingHandlerMapping.class)) - .extracting("interceptors").asList().extracting((item) -> (Class) item.getClass()) - .contains(LongTaskTimingHandlerInterceptor.class)); - } - @Test void whenTagContributorsAreDefinedThenTagsProviderUsesThem() { - this.contextRunner.withUserConfiguration(TagsContributorsConfiguration.class).run((context) -> { - assertThat(context).hasSingleBean(DefaultWebMvcTagsProvider.class); - assertThat(context.getBean(DefaultWebMvcTagsProvider.class)).extracting("contributors").asList().hasSize(2); - }); + this.contextRunner.withUserConfiguration(TagsContributorsConfiguration.class) + .run((context) -> assertThat(context.getBean(FilterRegistrationBean.class).getFilter()) + .extracting("observationConvention") + .isInstanceOf(ServerRequestObservationConventionAdapter.class)); } private MeterRegistry getInitializedMeterRegistry(AssertableWebApplicationContext context) throws Exception { @@ -228,7 +177,7 @@ class WebMvcMetricsAutoConfigurationTests { throws Exception { assertThat(context).hasSingleBean(FilterRegistrationBean.class); Filter filter = context.getBean(FilterRegistrationBean.class).getFilter(); - assertThat(filter).isInstanceOf(WebMvcMetricsFilter.class); + assertThat(filter).isInstanceOf(ServerHttpObservationFilter.class); MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context).addFilters(filter).build(); for (String url : urls) { mockMvc.perform(MockMvcRequestBuilders.get(url)).andExpect(status().isOk()); @@ -277,22 +226,22 @@ class WebMvcMetricsAutoConfigurationTests { } @Configuration(proxyBeanMethods = false) - static class TestWebMvcMetricsFilterRegistrationConfiguration { + static class TestServerHttpObservationFilterRegistrationConfiguration { @Bean @SuppressWarnings("unchecked") - FilterRegistrationBean testWebMvcMetricsFilter() { + FilterRegistrationBean testServerHttpObservationFilter() { return mock(FilterRegistrationBean.class); } } @Configuration(proxyBeanMethods = false) - static class TestWebMvcMetricsFilterConfiguration { + static class TestServerHttpObservationFilterConfiguration { @Bean - WebMvcMetricsFilter testWebMvcMetricsFilter() { - return new WebMvcMetricsFilter(null, null, null, null); + ServerHttpObservationFilter testServerHttpObservationFilter() { + return new ServerHttpObservationFilter(TestObservationRegistry.create()); } } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/DefaultWebMvcTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/DefaultWebMvcTagsProvider.java index 9a303bdbd05..150aa938970 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/DefaultWebMvcTagsProvider.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/DefaultWebMvcTagsProvider.java @@ -29,7 +29,11 @@ import jakarta.servlet.http.HttpServletResponse; * * @author Jon Schneider * @since 2.0.0 + * @deprecated since 3.0.0 for removal in 3.2.0 in favor of + * {@link org.springframework.http.observation.ServerRequestObservationConvention} */ +@Deprecated(since = "3.0.0", forRemoval = true) +@SuppressWarnings("removal") public class DefaultWebMvcTagsProvider implements WebMvcTagsProvider { private final boolean ignoreTrailingSlash; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/LongTaskTimingHandlerInterceptor.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/LongTaskTimingHandlerInterceptor.java deleted file mode 100644 index 38e53a927a9..00000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/LongTaskTimingHandlerInterceptor.java +++ /dev/null @@ -1,165 +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.servlet; - -import java.lang.reflect.AnnotatedElement; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Set; - -import io.micrometer.core.annotation.Timed; -import io.micrometer.core.instrument.LongTaskTimer; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Tag; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.core.annotation.MergedAnnotationCollectors; -import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.web.method.HandlerMethod; -import org.springframework.web.servlet.HandlerInterceptor; - -/** - * A {@link HandlerInterceptor} that supports Micrometer's long task timers configured on - * a handler using {@link Timed @Timed} with {@link Timed#longTask() longTask} set to - * {@code true}. - * - * @author Andy Wilkinson - * @since 2.0.7 - */ -public class LongTaskTimingHandlerInterceptor implements HandlerInterceptor { - - private static final Log logger = LogFactory.getLog(LongTaskTimingHandlerInterceptor.class); - - private final MeterRegistry registry; - - private final WebMvcTagsProvider tagsProvider; - - /** - * Creates a new {@code LongTaskTimingHandlerInterceptor} that will create - * {@link LongTaskTimer LongTaskTimers} using the given registry. Timers will be - * tagged using the given {@code tagsProvider}. - * @param registry the registry - * @param tagsProvider the tags provider - */ - public LongTaskTimingHandlerInterceptor(MeterRegistry registry, WebMvcTagsProvider tagsProvider) { - this.registry = registry; - this.tagsProvider = tagsProvider; - } - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) - throws Exception { - LongTaskTimingContext timingContext = LongTaskTimingContext.get(request); - if (timingContext == null) { - startAndAttachTimingContext(request, handler); - } - return true; - } - - @Override - public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) - throws Exception { - if (!request.isAsyncStarted()) { - stopLongTaskTimers(LongTaskTimingContext.get(request)); - } - } - - private void startAndAttachTimingContext(HttpServletRequest request, Object handler) { - Set annotations = getTimedAnnotations(handler); - Collection longTaskTimerSamples = getLongTaskTimerSamples(request, handler, annotations); - LongTaskTimingContext timingContext = new LongTaskTimingContext(longTaskTimerSamples); - timingContext.attachTo(request); - } - - private Collection getLongTaskTimerSamples(HttpServletRequest request, Object handler, - Set annotations) { - List samples = new ArrayList<>(); - try { - annotations.stream().filter(Timed::longTask).forEach((annotation) -> { - Iterable tags = this.tagsProvider.getLongRequestTags(request, handler); - LongTaskTimer.Builder builder = LongTaskTimer.builder(annotation).tags(tags); - LongTaskTimer timer = builder.register(this.registry); - samples.add(timer.start()); - }); - } - catch (Exception ex) { - logger.warn("Failed to start long task timers", ex); - // Allow request-response exchange to continue, unaffected by metrics problem - } - return samples; - } - - private Set getTimedAnnotations(Object handler) { - if (handler instanceof HandlerMethod handlerMethod) { - return getTimedAnnotations(handlerMethod); - } - return Collections.emptySet(); - } - - private Set getTimedAnnotations(HandlerMethod handler) { - Set timed = findTimedAnnotations(handler.getMethod()); - if (timed.isEmpty()) { - return findTimedAnnotations(handler.getBeanType()); - } - return timed; - } - - private Set findTimedAnnotations(AnnotatedElement element) { - return MergedAnnotations.from(element).stream(Timed.class) - .collect(MergedAnnotationCollectors.toAnnotationSet()); - } - - private void stopLongTaskTimers(LongTaskTimingContext timingContext) { - for (LongTaskTimer.Sample sample : timingContext.getLongTaskTimerSamples()) { - sample.stop(); - } - } - - /** - * Context object attached to a request to retain information across the multiple - * interceptor calls that happen with async requests. - */ - static class LongTaskTimingContext { - - private static final String ATTRIBUTE = LongTaskTimingContext.class.getName(); - - private final Collection longTaskTimerSamples; - - LongTaskTimingContext(Collection longTaskTimerSamples) { - this.longTaskTimerSamples = longTaskTimerSamples; - } - - Collection getLongTaskTimerSamples() { - return this.longTaskTimerSamples; - } - - void attachTo(HttpServletRequest request) { - request.setAttribute(ATTRIBUTE, this); - } - - static LongTaskTimingContext get(HttpServletRequest request) { - return (LongTaskTimingContext) request.getAttribute(ATTRIBUTE); - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsFilter.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsFilter.java deleted file mode 100644 index dc744b4dc12..00000000000 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsFilter.java +++ /dev/null @@ -1,190 +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.servlet; - -import java.io.IOException; -import java.util.Collections; -import java.util.Set; - -import io.micrometer.core.annotation.Timed; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.Timer; -import io.micrometer.core.instrument.Timer.Builder; -import io.micrometer.core.instrument.Timer.Sample; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.boot.actuate.metrics.AutoTimer; -import org.springframework.boot.actuate.metrics.annotation.TimedAnnotations; -import org.springframework.boot.web.servlet.error.ErrorAttributes; -import org.springframework.http.HttpStatus; -import org.springframework.web.filter.OncePerRequestFilter; -import org.springframework.web.method.HandlerMethod; -import org.springframework.web.servlet.DispatcherServlet; -import org.springframework.web.servlet.HandlerMapping; - -/** - * Intercepts incoming HTTP requests handled by Spring MVC handlers and records metrics - * about execution time and results. - * - * @author Jon Schneider - * @author Phillip Webb - * @author Chanhyeong LEE - * @since 2.0.0 - */ -public class WebMvcMetricsFilter extends OncePerRequestFilter { - - private static final Log logger = LogFactory.getLog(WebMvcMetricsFilter.class); - - private final MeterRegistry registry; - - private final WebMvcTagsProvider tagsProvider; - - private final String metricName; - - private final AutoTimer autoTimer; - - /** - * Create a new {@link WebMvcMetricsFilter} instance. - * @param registry the meter registry - * @param tagsProvider the tags provider - * @param metricName the metric name - * @param autoTimer the auto-timers to apply or {@code null} to disable auto-timing - * @since 2.2.0 - */ - public WebMvcMetricsFilter(MeterRegistry registry, WebMvcTagsProvider tagsProvider, String metricName, - AutoTimer autoTimer) { - this.registry = registry; - this.tagsProvider = tagsProvider; - this.metricName = metricName; - this.autoTimer = autoTimer; - } - - @Override - protected boolean shouldNotFilterAsyncDispatch() { - return false; - } - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { - TimingContext timingContext = TimingContext.get(request); - if (timingContext == null) { - timingContext = startAndAttachTimingContext(request); - } - try { - filterChain.doFilter(request, response); - if (!request.isAsyncStarted()) { - // Only record when async processing has finished or never been started. - // If async was started by something further down the chain we wait - // until the second filter invocation (but we'll be using the - // TimingContext that was attached to the first) - Throwable exception = fetchException(request); - record(timingContext, request, response, exception); - } - } - catch (Exception ex) { - response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); - record(timingContext, request, response, unwrapServletException(ex)); - throw ex; - } - } - - private Throwable unwrapServletException(Throwable ex) { - return (ex instanceof ServletException) ? ex.getCause() : ex; - } - - private TimingContext startAndAttachTimingContext(HttpServletRequest request) { - Timer.Sample timerSample = Timer.start(this.registry); - TimingContext timingContext = new TimingContext(timerSample); - timingContext.attachTo(request); - return timingContext; - } - - private Throwable fetchException(HttpServletRequest request) { - Throwable exception = (Throwable) request.getAttribute(ErrorAttributes.ERROR_ATTRIBUTE); - if (exception == null) { - exception = (Throwable) request.getAttribute(DispatcherServlet.EXCEPTION_ATTRIBUTE); - } - return exception; - } - - private void record(TimingContext timingContext, HttpServletRequest request, HttpServletResponse response, - Throwable exception) { - try { - Object handler = getHandler(request); - Set annotations = getTimedAnnotations(handler); - Timer.Sample timerSample = timingContext.getTimerSample(); - AutoTimer.apply(this.autoTimer, this.metricName, annotations, (builder) -> timerSample - .stop(getTimer(builder, handler, request, response, exception).register(this.registry))); - } - catch (Exception ex) { - logger.warn("Failed to record timer metrics", ex); - // Allow request-response exchange to continue, unaffected by metrics problem - } - } - - private Object getHandler(HttpServletRequest request) { - return request.getAttribute(HandlerMapping.BEST_MATCHING_HANDLER_ATTRIBUTE); - } - - private Set getTimedAnnotations(Object handler) { - if (handler instanceof HandlerMethod handlerMethod) { - return TimedAnnotations.get(handlerMethod.getMethod(), handlerMethod.getBeanType()); - } - return Collections.emptySet(); - } - - private Timer.Builder getTimer(Builder builder, Object handler, HttpServletRequest request, - HttpServletResponse response, Throwable exception) { - return builder.description("Duration of HTTP server request handling") - .tags(this.tagsProvider.getTags(request, response, handler, exception)); - } - - /** - * Context object attached to a request to retain information across the multiple - * filter calls that happen with async requests. - */ - private static class TimingContext { - - private static final String ATTRIBUTE = TimingContext.class.getName(); - - private final Timer.Sample timerSample; - - TimingContext(Sample timerSample) { - this.timerSample = timerSample; - } - - Timer.Sample getTimerSample() { - return this.timerSample; - } - - void attachTo(HttpServletRequest request) { - request.setAttribute(ATTRIBUTE, this); - } - - static TimingContext get(HttpServletRequest request) { - return (TimingContext) request.getAttribute(ATTRIBUTE); - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTags.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTags.java index 2df49961e24..8f4f3ef09b5 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTags.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTags.java @@ -37,7 +37,10 @@ import org.springframework.web.util.pattern.PathPattern; * @author Brian Clozel * @author Michael McFadyen * @since 2.0.0 + * @deprecated since 3.0.0 for removal in 3.2.0 in favor of + * {@link org.springframework.http.observation.ServerRequestObservationConvention} */ +@Deprecated(since = "3.0.0", forRemoval = true) public final class WebMvcTags { private static final String DATA_REST_PATH_PATTERN_ATTRIBUTE = "org.springframework.data.rest.webmvc.RepositoryRestHandlerMapping.EFFECTIVE_REPOSITORY_RESOURCE_LOOKUP_PATH"; diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTagsContributor.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTagsContributor.java index fe16c9749c7..3355c22e266 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTagsContributor.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTagsContributor.java @@ -27,7 +27,10 @@ import jakarta.servlet.http.HttpServletResponse; * * @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.ServerRequestObservationConvention} */ +@Deprecated(since = "3.0.0", forRemoval = true) public interface WebMvcTagsContributor { /** diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTagsProvider.java index e09d4d4baa1..630af5076f1 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTagsProvider.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcTagsProvider.java @@ -27,7 +27,10 @@ import jakarta.servlet.http.HttpServletResponse; * @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.ServerRequestObservationConvention} */ +@Deprecated(since = "3.0.0", forRemoval = true) public interface WebMvcTagsProvider { /** diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/DefaultWebMvcTagsProviderTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/DefaultWebMvcTagsProviderTests.java index 2066e8a7425..ab2a3cc3a67 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/DefaultWebMvcTagsProviderTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/DefaultWebMvcTagsProviderTests.java @@ -40,6 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Andy Wilkinson */ +@SuppressWarnings("removal") class DefaultWebMvcTagsProviderTests { @Test diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcTagsTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcTagsTests.java index 36ba42db687..03ef6502a29 100644 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcTagsTests.java +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/web/servlet/WebMvcTagsTests.java @@ -34,6 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Brian Clozel * @author Michael McFadyen */ +@SuppressWarnings("removal") class WebMvcTagsTests { private final MockHttpServletRequest request = new MockHttpServletRequest(); diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/FaultyWebMvcTagsProvider.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/FaultyWebMvcTagsProvider.java deleted file mode 100644 index 8541ce66762..00000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/FaultyWebMvcTagsProvider.java +++ /dev/null @@ -1,60 +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.servlet; - -import java.util.concurrent.atomic.AtomicBoolean; - -import io.micrometer.core.instrument.Tag; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -/** - * {@link WebMvcTagsProvider} used for testing that can be configured to fail when getting - * tags or long task tags. - * - * @author Andy Wilkinson - */ -class FaultyWebMvcTagsProvider extends DefaultWebMvcTagsProvider { - - private final AtomicBoolean fail = new AtomicBoolean(); - - FaultyWebMvcTagsProvider() { - super(true); - } - - @Override - public Iterable getTags(HttpServletRequest request, HttpServletResponse response, Object handler, - Throwable exception) { - if (this.fail.compareAndSet(true, false)) { - throw new RuntimeException(); - } - return super.getTags(request, response, handler, exception); - } - - @Override - public Iterable getLongRequestTags(HttpServletRequest request, Object handler) { - if (this.fail.compareAndSet(true, false)) { - throw new RuntimeException(); - } - return super.getLongRequestTags(request, handler); - } - - void failOnce() { - this.fail.set(true); - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/LongTaskTimingHandlerInterceptorTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/LongTaskTimingHandlerInterceptorTests.java deleted file mode 100644 index b479f310e84..00000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/LongTaskTimingHandlerInterceptorTests.java +++ /dev/null @@ -1,216 +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.servlet; - -import java.util.concurrent.Callable; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CyclicBarrier; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - -import io.micrometer.core.annotation.Timed; -import io.micrometer.core.instrument.Clock; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.MockClock; -import io.micrometer.core.instrument.simple.SimpleConfig; -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import jakarta.servlet.ServletException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.fail; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -/** - * Tests for {@link LongTaskTimingHandlerInterceptor}. - * - * @author Andy Wilkinson - */ -@ExtendWith(SpringExtension.class) -@WebAppConfiguration -class LongTaskTimingHandlerInterceptorTests { - - @Autowired - private SimpleMeterRegistry registry; - - @Autowired - private WebApplicationContext context; - - @Autowired - private CyclicBarrier callableBarrier; - - @Autowired - private FaultyWebMvcTagsProvider tagsProvider; - - private MockMvc mvc; - - @BeforeEach - void setUpMockMvc() { - this.mvc = MockMvcBuilders.webAppContextSetup(this.context).build(); - } - - @Test - void asyncRequestThatThrowsUncheckedException() throws Exception { - MvcResult result = this.mvc.perform(get("/api/c1/completableFutureException")) - .andExpect(request().asyncStarted()).andReturn(); - assertThat(this.registry.get("my.long.request.exception").longTaskTimer().activeTasks()).isEqualTo(1); - assertThatExceptionOfType(ServletException.class).isThrownBy(() -> this.mvc.perform(asyncDispatch(result))) - .withRootCauseInstanceOf(RuntimeException.class); - assertThat(this.registry.get("my.long.request.exception").longTaskTimer().activeTasks()).isEqualTo(0); - } - - @Test - void asyncCallableRequest() throws Exception { - AtomicReference result = new AtomicReference<>(); - Thread backgroundRequest = new Thread(() -> { - try { - result.set( - this.mvc.perform(get("/api/c1/callable/10")).andExpect(request().asyncStarted()).andReturn()); - } - catch (Exception ex) { - fail("Failed to execute async request", ex); - } - }); - backgroundRequest.start(); - this.callableBarrier.await(); - assertThat(this.registry.get("my.long.request").tags("region", "test").longTaskTimer().activeTasks()) - .isEqualTo(1); - this.callableBarrier.await(); - backgroundRequest.join(); - this.mvc.perform(asyncDispatch(result.get())).andExpect(status().isOk()); - assertThat(this.registry.get("my.long.request").tags("region", "test").longTaskTimer().activeTasks()) - .isEqualTo(0); - } - - @Test - void whenMetricsRecordingFailsResponseIsUnaffected() throws Exception { - this.tagsProvider.failOnce(); - AtomicReference result = new AtomicReference<>(); - Thread backgroundRequest = new Thread(() -> { - try { - result.set( - this.mvc.perform(get("/api/c1/callable/10")).andExpect(request().asyncStarted()).andReturn()); - } - catch (Exception ex) { - fail("Failed to execute async request", ex); - } - }); - backgroundRequest.start(); - this.callableBarrier.await(10, TimeUnit.SECONDS); - this.callableBarrier.await(10, TimeUnit.SECONDS); - backgroundRequest.join(); - this.mvc.perform(asyncDispatch(result.get())).andExpect(status().isOk()); - } - - @Configuration(proxyBeanMethods = false) - @EnableWebMvc - @Import(Controller1.class) - static class MetricsInterceptorConfiguration { - - @Bean - Clock micrometerClock() { - return new MockClock(); - } - - @Bean - SimpleMeterRegistry simple(Clock clock) { - return new SimpleMeterRegistry(SimpleConfig.DEFAULT, clock); - } - - @Bean - CyclicBarrier callableBarrier() { - return new CyclicBarrier(2); - } - - @Bean - FaultyWebMvcTagsProvider webMvcTagsProvider() { - return new FaultyWebMvcTagsProvider(); - } - - @Bean - WebMvcConfigurer handlerInterceptorConfigurer(MeterRegistry meterRegistry, WebMvcTagsProvider tagsProvider) { - return new WebMvcConfigurer() { - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new LongTaskTimingHandlerInterceptor(meterRegistry, tagsProvider)); - } - - }; - } - - } - - @RestController - @RequestMapping("/api/c1") - static class Controller1 { - - @Autowired - private CyclicBarrier callableBarrier; - - @Timed - @Timed(value = "my.long.request", extraTags = { "region", "test" }, longTask = true) - @GetMapping("/callable/{id}") - Callable asyncCallable(@PathVariable Long id) throws Exception { - this.callableBarrier.await(); - return () -> { - try { - this.callableBarrier.await(); - } - catch (InterruptedException ex) { - throw new RuntimeException(ex); - } - return id.toString(); - }; - } - - @Timed - @Timed(value = "my.long.request.exception", longTask = true) - @GetMapping("/completableFutureException") - CompletableFuture asyncCompletableFutureException() { - return CompletableFuture.supplyAsync(() -> { - throw new RuntimeException("boom"); - }); - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsFilterAutoTimedTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsFilterAutoTimedTests.java deleted file mode 100644 index 8c49eef7016..00000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsFilterAutoTimedTests.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2012-2019 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.servlet; - -import io.micrometer.core.instrument.Clock; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.MockClock; -import io.micrometer.core.instrument.Timer; -import io.micrometer.core.instrument.distribution.HistogramSnapshot; -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 org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -/** - * Test for {@link WebMvcMetricsFilter} with auto-timed enabled. - * - * @author Jon Schneider - * @author Tadaya Tsuyukubo - */ -@ExtendWith(SpringExtension.class) -@WebAppConfiguration -class WebMvcMetricsFilterAutoTimedTests { - - @Autowired - private MeterRegistry registry; - - @Autowired - private WebApplicationContext context; - - private MockMvc mvc; - - @Autowired - private WebMvcMetricsFilter filter; - - @BeforeEach - void setupMockMvc() { - this.mvc = MockMvcBuilders.webAppContextSetup(this.context).addFilters(this.filter).build(); - } - - @Test - void metricsCanBeAutoTimed() throws Exception { - this.mvc.perform(get("/api/10")).andExpect(status().isOk()); - Timer timer = this.registry.get("http.server.requests").tags("status", "200").timer(); - assertThat(timer.count()).isEqualTo(1L); - HistogramSnapshot snapshot = timer.takeSnapshot(); - assertThat(snapshot.percentileValues()).hasSize(2); - assertThat(snapshot.percentileValues()[0].percentile()).isEqualTo(0.5); - assertThat(snapshot.percentileValues()[1].percentile()).isEqualTo(0.95); - } - - @Configuration(proxyBeanMethods = false) - @EnableWebMvc - @Import({ Controller.class }) - static class TestConfiguration { - - @Bean - MockClock clock() { - return new MockClock(); - } - - @Bean - MeterRegistry meterRegistry(Clock clock) { - return new SimpleMeterRegistry(SimpleConfig.DEFAULT, clock); - } - - @Bean - WebMvcMetricsFilter webMetricsFilter(WebApplicationContext context, MeterRegistry registry) { - return new WebMvcMetricsFilter(registry, new DefaultWebMvcTagsProvider(), "http.server.requests", - (builder) -> builder.publishPercentiles(0.5, 0.95).publishPercentileHistogram(true)); - } - - } - - @RestController - @RequestMapping("/api") - static class Controller { - - @GetMapping("/{id}") - String successful(@PathVariable Long id) { - return id.toString(); - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsFilterTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsFilterTests.java deleted file mode 100644 index cbbdd7240c8..00000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsFilterTests.java +++ /dev/null @@ -1,573 +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.servlet; - -import java.io.IOException; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import java.time.Duration; -import java.util.Collection; -import java.util.concurrent.BrokenBarrierException; -import java.util.concurrent.Callable; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CyclicBarrier; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - -import io.micrometer.core.annotation.Timed; -import io.micrometer.core.instrument.Clock; -import io.micrometer.core.instrument.Meter; -import io.micrometer.core.instrument.Meter.Id; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.MockClock; -import io.micrometer.core.instrument.Tag; -import io.micrometer.core.instrument.composite.CompositeMeterRegistry; -import io.micrometer.core.instrument.config.MeterFilter; -import io.micrometer.core.instrument.config.MeterFilterReply; -import io.micrometer.core.instrument.simple.SimpleConfig; -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import io.micrometer.prometheus.PrometheusConfig; -import io.micrometer.prometheus.PrometheusMeterRegistry; -import io.prometheus.client.CollectorRegistry; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.actuate.metrics.AutoTimer; -import org.springframework.boot.web.servlet.error.ErrorAttributes; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.Primary; -import org.springframework.http.HttpStatus; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.MvcResult; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.filter.OncePerRequestFilter; -import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter; -import org.springframework.web.util.pattern.PathPatternParser; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatIOException; -import static org.assertj.core.api.Assertions.fail; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -/** - * Tests for {@link WebMvcMetricsFilter}. - * - * @author Jon Schneider - */ -@ExtendWith(SpringExtension.class) -@WebAppConfiguration -class WebMvcMetricsFilterTests { - - @Autowired - private SimpleMeterRegistry registry; - - @Autowired - private PrometheusMeterRegistry prometheusRegistry; - - @Autowired - private WebApplicationContext context; - - @Autowired - private WebMvcMetricsFilter filter; - - private MockMvc mvc; - - @Autowired - @Qualifier("callableBarrier") - private CyclicBarrier callableBarrier; - - @Autowired - @Qualifier("completableFutureBarrier") - private CyclicBarrier completableFutureBarrier; - - @Autowired - private FaultyWebMvcTagsProvider tagsProvider; - - @BeforeEach - void setupMockMvc() { - this.mvc = MockMvcBuilders.webAppContextSetup(this.context).addFilters(this.filter, new CustomBehaviorFilter()) - .build(); - } - - @Test - void timedMethod() throws Exception { - this.mvc.perform(get("/api/c1/10")).andExpect(status().isOk()); - assertThat(this.registry.get("http.server.requests") - .tags("status", "200", "uri", "/api/c1/{id}", "public", "true").timer().count()).isEqualTo(1); - } - - @Test - void subclassedTimedMethod() throws Exception { - this.mvc.perform(get("/api/c1/metaTimed/10")).andExpect(status().isOk()); - assertThat(this.registry.get("http.server.requests").tags("status", "200", "uri", "/api/c1/metaTimed/{id}") - .timer().count()).isEqualTo(1L); - } - - @Test - void untimedMethod() throws Exception { - this.mvc.perform(get("/api/c1/untimed/10")).andExpect(status().isOk()); - assertThat(this.registry.find("http.server.requests").tags("uri", "/api/c1/untimed/10").timer()).isNull(); - } - - @Test - void timedControllerClass() throws Exception { - this.mvc.perform(get("/api/c2/10")).andExpect(status().isOk()); - assertThat(this.registry.get("http.server.requests").tags("status", "200").timer().count()).isEqualTo(1L); - } - - @Test - void badClientRequest() throws Exception { - this.mvc.perform(get("/api/c1/oops")).andExpect(status().is4xxClientError()); - assertThat(this.registry.get("http.server.requests").tags("status", "400").timer().count()).isEqualTo(1L); - } - - @Test - void redirectRequest() throws Exception { - this.mvc.perform(get("/api/redirect").header(CustomBehaviorFilter.TEST_STATUS_HEADER, "302")) - .andExpect(status().is3xxRedirection()); - assertThat(this.registry.get("http.server.requests").tags("uri", "REDIRECTION").tags("status", "302").timer()) - .isNotNull(); - } - - @Test - void notFoundRequest() throws Exception { - this.mvc.perform(get("/api/not/found").header(CustomBehaviorFilter.TEST_STATUS_HEADER, "404")) - .andExpect(status().is4xxClientError()); - assertThat(this.registry.get("http.server.requests").tags("uri", "NOT_FOUND").tags("status", "404").timer()) - .isNotNull(); - } - - @Test - void unhandledError() { - assertThatCode(() -> this.mvc.perform(get("/api/c1/unhandledError/10"))) - .hasRootCauseInstanceOf(RuntimeException.class); - assertThat(this.registry.get("http.server.requests").tags("exception", "RuntimeException").timer().count()) - .isEqualTo(1L); - } - - @Test - void unhandledServletException() { - assertThatCode(() -> this.mvc - .perform(get("/api/filterError").header(CustomBehaviorFilter.TEST_SERVLET_EXCEPTION_HEADER, "throw"))) - .isInstanceOf(ServletException.class); - Id meterId = this.registry.get("http.server.requests").tags("exception", "IllegalStateException").timer() - .getId(); - assertThat(meterId.getTag("status")).isEqualTo("500"); - } - - @Test - void streamingError() throws Exception { - MvcResult result = this.mvc.perform(get("/api/c1/streamingError")).andExpect(request().asyncStarted()) - .andReturn(); - assertThatIOException().isThrownBy(() -> this.mvc.perform(asyncDispatch(result)).andReturn()); - Id meterId = this.registry.get("http.server.requests").tags("exception", "IOException").timer().getId(); - // Response is committed before error occurs so status is 200 (OK) - assertThat(meterId.getTag("status")).isEqualTo("200"); - } - - @Test - void whenMetricsRecordingFailsResponseIsUnaffected() throws Exception { - this.tagsProvider.failOnce(); - this.mvc.perform(get("/api/c1/10")).andExpect(status().isOk()); - } - - @Test - void anonymousError() { - try { - this.mvc.perform(get("/api/c1/anonymousError/10")); - } - catch (Throwable ignore) { - } - Id meterId = this.registry.get("http.server.requests").tag("uri", "/api/c1/anonymousError/{id}").timer() - .getId(); - assertThat(meterId.getTag("exception")).endsWith("$1"); - assertThat(meterId.getTag("status")).isEqualTo("500"); - } - - @Test - void asyncCallableRequest() throws Exception { - AtomicReference result = new AtomicReference<>(); - Thread backgroundRequest = new Thread(() -> { - try { - result.set( - this.mvc.perform(get("/api/c1/callable/10")).andExpect(request().asyncStarted()).andReturn()); - } - catch (Exception ex) { - fail("Failed to execute async request", ex); - } - }); - backgroundRequest.start(); - assertThat(this.registry.find("http.server.requests").tags("uri", "/api/c1/async").timer()) - .describedAs("Request isn't prematurely recorded as complete").isNull(); - // once the mapping completes, we can gather information about status, etc. - this.callableBarrier.await(); - MockClock.clock(this.registry).add(Duration.ofSeconds(2)); - this.callableBarrier.await(); - backgroundRequest.join(); - this.mvc.perform(asyncDispatch(result.get())).andExpect(status().isOk()); - assertThat(this.registry.get("http.server.requests").tags("status", "200").tags("uri", "/api/c1/callable/{id}") - .timer().totalTime(TimeUnit.SECONDS)).isEqualTo(2L); - } - - @Test - void asyncRequestThatThrowsUncheckedException() throws Exception { - MvcResult result = this.mvc.perform(get("/api/c1/completableFutureException")) - .andExpect(request().asyncStarted()).andReturn(); - assertThatExceptionOfType(ServletException.class).isThrownBy(() -> this.mvc.perform(asyncDispatch(result))) - .withRootCauseInstanceOf(RuntimeException.class); - assertThat(this.registry.get("http.server.requests").tags("uri", "/api/c1/completableFutureException").timer() - .count()).isEqualTo(1); - } - - @Test - void asyncCompletableFutureRequest() throws Exception { - AtomicReference result = new AtomicReference<>(); - Thread backgroundRequest = new Thread(() -> { - try { - result.set(this.mvc.perform(get("/api/c1/completableFuture/{id}", 1)) - .andExpect(request().asyncStarted()).andReturn()); - } - catch (Exception ex) { - fail("Failed to execute async request", ex); - } - }); - backgroundRequest.start(); - this.completableFutureBarrier.await(); - MockClock.clock(this.registry).add(Duration.ofSeconds(2)); - this.completableFutureBarrier.await(); - backgroundRequest.join(); - this.mvc.perform(asyncDispatch(result.get())).andExpect(status().isOk()); - assertThat(this.registry.get("http.server.requests").tags("uri", "/api/c1/completableFuture/{id}").timer() - .totalTime(TimeUnit.SECONDS)).isEqualTo(2); - } - - @Test - void endpointThrowsError() throws Exception { - this.mvc.perform(get("/api/c1/error/10")).andExpect(status().is4xxClientError()); - assertThat(this.registry.get("http.server.requests").tags("status", "422", "exception", "IllegalStateException") - .timer().count()).isEqualTo(1L); - } - - @Test - void regexBasedRequestMapping() throws Exception { - this.mvc.perform(get("/api/c1/regex/.abc")).andExpect(status().isOk()); - assertThat( - this.registry.get("http.server.requests").tags("uri", "/api/c1/regex/{id:\\.[a-z]+}").timer().count()) - .isEqualTo(1L); - } - - @Test - void recordQuantiles() throws Exception { - this.mvc.perform(get("/api/c1/percentiles/10")).andExpect(status().isOk()); - assertThat(this.prometheusRegistry.scrape()).contains("quantile=\"0.5\""); - assertThat(this.prometheusRegistry.scrape()).contains("quantile=\"0.95\""); - } - - @Test - void recordHistogram() throws Exception { - this.mvc.perform(get("/api/c1/histogram/10")).andExpect(status().isOk()); - assertThat(this.prometheusRegistry.scrape()).contains("le=\"0.001\""); - assertThat(this.prometheusRegistry.scrape()).contains("le=\"30.0\""); - } - - @Test - void trailingSlashShouldNotRecordDuplicateMetrics() throws Exception { - this.mvc.perform(get("/api/c1/simple/10")).andExpect(status().isOk()); - this.mvc.perform(get("/api/c1/simple/10/")).andExpect(status().isOk()); - assertThat(this.registry.get("http.server.requests").tags("status", "200", "uri", "/api/c1/simple/{id}").timer() - .count()).isEqualTo(2); - } - - @Target({ ElementType.METHOD }) - @Retention(RetentionPolicy.RUNTIME) - @Timed(percentiles = 0.95) - @interface Timed95 { - - } - - @Configuration(proxyBeanMethods = false) - @EnableWebMvc - @Import({ Controller1.class, Controller2.class }) - static class MetricsFilterApp implements WebMvcConfigurer { - - @Bean - Clock micrometerClock() { - return new MockClock(); - } - - @Primary - @Bean - MeterRegistry meterRegistry(Collection registries, Clock clock) { - CompositeMeterRegistry composite = new CompositeMeterRegistry(clock); - registries.forEach(composite::add); - return composite; - } - - @Bean - SimpleMeterRegistry simple(Clock clock) { - return new SimpleMeterRegistry(SimpleConfig.DEFAULT, clock); - } - - @Bean - PrometheusMeterRegistry prometheus(Clock clock) { - PrometheusMeterRegistry r = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT, new CollectorRegistry(), - clock); - r.config().meterFilter(new MeterFilter() { - @Override - public MeterFilterReply accept(Meter.Id id) { - for (Tag tag : id.getTags()) { - if (tag.getKey().equals("uri") - && (tag.getValue().contains("histogram") || tag.getValue().contains("percentiles"))) { - return MeterFilterReply.ACCEPT; - } - } - return MeterFilterReply.DENY; - } - }); - return r; - } - - @Bean - CustomBehaviorFilter customBehaviorFilter() { - return new CustomBehaviorFilter(); - } - - @Bean(name = "callableBarrier") - CyclicBarrier callableBarrier() { - return new CyclicBarrier(2); - } - - @Bean(name = "completableFutureBarrier") - CyclicBarrier completableFutureBarrier() { - return new CyclicBarrier(2); - } - - @Bean - WebMvcMetricsFilter webMetricsFilter(MeterRegistry registry, FaultyWebMvcTagsProvider tagsProvider, - WebApplicationContext ctx) { - return new WebMvcMetricsFilter(registry, tagsProvider, "http.server.requests", AutoTimer.ENABLED); - } - - @Bean - FaultyWebMvcTagsProvider faultyWebMvcTagsProvider() { - return new FaultyWebMvcTagsProvider(); - } - - @Override - @SuppressWarnings("deprecation") - public void configurePathMatch(PathMatchConfigurer configurer) { - PathPatternParser pathPatternParser = new PathPatternParser(); - pathPatternParser.setMatchOptionalTrailingSeparator(true); - configurer.setPatternParser(pathPatternParser); - } - - } - - @RestController - @RequestMapping("/api/c1") - static class Controller1 { - - @Autowired - @Qualifier("callableBarrier") - private CyclicBarrier callableBarrier; - - @Autowired - @Qualifier("completableFutureBarrier") - private CyclicBarrier completableFutureBarrier; - - @Timed(extraTags = { "public", "true" }) - @GetMapping("/{id}") - String successfulWithExtraTags(@PathVariable Long id) { - return id.toString(); - } - - @GetMapping("/simple/{id}") - String simpleMapping(@PathVariable Long id) { - return id.toString(); - } - - @Timed - @Timed(value = "my.long.request", extraTags = { "region", "test" }, longTask = true) - @GetMapping("/callable/{id}") - Callable asyncCallable(@PathVariable Long id) throws Exception { - this.callableBarrier.await(); - return () -> { - try { - this.callableBarrier.await(); - } - catch (InterruptedException ex) { - throw new RuntimeException(ex); - } - return id.toString(); - }; - } - - @Timed - @GetMapping("/completableFuture/{id}") - CompletableFuture asyncCompletableFuture(@PathVariable Long id) throws Exception { - this.completableFutureBarrier.await(); - return CompletableFuture.supplyAsync(() -> { - try { - this.completableFutureBarrier.await(); - } - catch (InterruptedException | BrokenBarrierException ex) { - throw new RuntimeException(ex); - } - return id.toString(); - }); - } - - @Timed - @Timed(value = "my.long.request.exception", longTask = true) - @GetMapping("/completableFutureException") - CompletableFuture asyncCompletableFutureException() { - return CompletableFuture.supplyAsync(() -> { - throw new RuntimeException("boom"); - }); - } - - @GetMapping("/untimed/{id}") - String successfulButUntimed(@PathVariable Long id) { - return id.toString(); - } - - @Timed - @GetMapping("/error/{id}") - String alwaysThrowsException(@PathVariable Long id) { - throw new IllegalStateException("Boom on " + id + "!"); - } - - @Timed - @GetMapping("/anonymousError/{id}") - String alwaysThrowsAnonymousException(@PathVariable Long id) throws Exception { - throw new Exception("this exception won't have a simple class name") { - }; - } - - @Timed - @GetMapping("/unhandledError/{id}") - String alwaysThrowsUnhandledException(@PathVariable Long id) { - throw new RuntimeException("Boom on " + id + "!"); - } - - @GetMapping("/streamingError") - ResponseBodyEmitter streamingError() throws IOException { - ResponseBodyEmitter emitter = new ResponseBodyEmitter(); - emitter.send("some data"); - emitter.send("some more data"); - emitter.completeWithError(new IOException("error while writing to the response")); - return emitter; - } - - @Timed - @GetMapping("/regex/{id:\\.[a-z]+}") - String successfulRegex(@PathVariable String id) { - return id; - } - - @Timed(percentiles = { 0.50, 0.95 }) - @GetMapping("/percentiles/{id}") - String percentiles(@PathVariable String id) { - return id; - } - - @Timed(histogram = true) - @GetMapping("/histogram/{id}") - String histogram(@PathVariable String id) { - return id; - } - - @Timed95 - @GetMapping("/metaTimed/{id}") - String meta(@PathVariable String id) { - return id; - } - - @ExceptionHandler(IllegalStateException.class) - @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) - ModelAndView defaultErrorHandler(HttpServletRequest request, Exception e) { - // this is done by ErrorAttributes implementations - request.setAttribute(ErrorAttributes.ERROR_ATTRIBUTE, e); - return new ModelAndView("myerror"); - } - - } - - @RestController - @Timed - @RequestMapping("/api/c2") - static class Controller2 { - - @GetMapping("/{id}") - String successful(@PathVariable Long id) { - return id.toString(); - } - - } - - static class CustomBehaviorFilter extends OncePerRequestFilter { - - static final String TEST_STATUS_HEADER = "x-test-status"; - - static final String TEST_SERVLET_EXCEPTION_HEADER = "x-test-servlet-exception"; - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - String misbehaveStatus = request.getHeader(TEST_STATUS_HEADER); - if (misbehaveStatus != null) { - response.setStatus(Integer.parseInt(misbehaveStatus)); - return; - } - if (request.getHeader(TEST_SERVLET_EXCEPTION_HEADER) != null) { - throw new ServletException(new IllegalStateException()); - } - filterChain.doFilter(request, response); - } - - } - -} diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsIntegrationTests.java deleted file mode 100644 index b4cb6b969a0..00000000000 --- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/web/servlet/WebMvcMetricsIntegrationTests.java +++ /dev/null @@ -1,163 +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.servlet; - -import io.micrometer.core.annotation.Timed; -import io.micrometer.core.instrument.Clock; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.MockClock; -import io.micrometer.core.instrument.simple.SimpleConfig; -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import jakarta.servlet.ServletException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.actuate.metrics.AutoTimer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.test.context.TestPropertySource; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -/** - * Tests for {@link WebMvcMetricsFilter} in the presence of a custom exception handler. - * - * @author Jon Schneider - */ -@ExtendWith(SpringExtension.class) -@WebAppConfiguration -@TestPropertySource(properties = "security.ignored=/**") -class WebMvcMetricsIntegrationTests { - - @Autowired - private WebApplicationContext context; - - @Autowired - private SimpleMeterRegistry registry; - - @Autowired - private WebMvcMetricsFilter filter; - - private MockMvc mvc; - - @BeforeEach - void setupMockMvc() { - this.mvc = MockMvcBuilders.webAppContextSetup(this.context).addFilters(this.filter).build(); - } - - @Test - void handledExceptionIsRecordedInMetricTag() throws Exception { - this.mvc.perform(get("/api/handledError")).andExpect(status().is5xxServerError()); - assertThat(this.registry.get("http.server.requests").tags("exception", "Exception1", "status", "500").timer() - .count()).isEqualTo(1L); - } - - @Test - void rethrownExceptionIsRecordedInMetricTag() { - assertThatExceptionOfType(ServletException.class) - .isThrownBy(() -> this.mvc.perform(get("/api/rethrownError")).andReturn()); - assertThat(this.registry.get("http.server.requests").tags("exception", "Exception2", "status", "500").timer() - .count()).isEqualTo(1L); - } - - @Configuration(proxyBeanMethods = false) - @EnableWebMvc - static class TestConfiguration { - - @Bean - MockClock clock() { - return new MockClock(); - } - - @Bean - MeterRegistry meterRegistry(Clock clock) { - return new SimpleMeterRegistry(SimpleConfig.DEFAULT, clock); - } - - @Bean - WebMvcMetricsFilter webMetricsFilter(MeterRegistry registry, WebApplicationContext ctx) { - return new WebMvcMetricsFilter(registry, new DefaultWebMvcTagsProvider(), "http.server.requests", - AutoTimer.ENABLED); - } - - @Configuration(proxyBeanMethods = false) - @RestController - @RequestMapping("/api") - @Timed - static class Controller1 { - - @Bean - CustomExceptionHandler controllerAdvice() { - return new CustomExceptionHandler(); - } - - @GetMapping("/handledError") - String handledError() { - throw new Exception1(); - } - - @GetMapping("/rethrownError") - String rethrownError() { - throw new Exception2(); - } - - } - - } - - static class Exception1 extends RuntimeException { - - } - - static class Exception2 extends RuntimeException { - - } - - @ControllerAdvice - static class CustomExceptionHandler { - - @ExceptionHandler - ResponseEntity handleError(Exception1 ex) { - return new ResponseEntity<>("this is a custom exception body", HttpStatus.INTERNAL_SERVER_ERROR); - } - - @ExceptionHandler - ResponseEntity rethrowError(Exception2 ex) { - throw ex; - } - - } - -} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc index f1cc7b10e4e..7109bdd2c79 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc @@ -742,10 +742,7 @@ Metrics are tagged by the name of the executor, which is derived from the bean n ==== Spring MVC Metrics Auto-configuration enables the instrumentation of all requests handled by Spring MVC controllers and functional handlers. By default, metrics are generated with the name, `http.server.requests`. -You can customize the name by setting the configprop:management.metrics.web.server.request.metric-name[] property. - -`@Timed` annotations are supported on `@Controller` classes and `@RequestMapping` methods (see <> for details). -If you do not want to record metrics for all Spring MVC requests, you can set configprop:management.metrics.web.server.request.autotime.enabled[] to `false` and exclusively use `@Timed` annotations instead. +You can customize the name by setting the configprop:management.observations.http.server.requests.name[] property. By default, Spring MVC related metrics are tagged with the following information: @@ -784,10 +781,7 @@ To customize the filter, provide a `@Bean` that implements `FilterRegistrationBe ==== Spring WebFlux Metrics Auto-configuration enables the instrumentation of all requests handled by Spring WebFlux controllers and functional handlers. By default, metrics are generated with the name, `http.server.requests`. -You can customize the name by setting the configprop:management.metrics.web.server.request.metric-name[] property. - -`@Timed` annotations are supported on `@Controller` classes and `@RequestMapping` methods (see <> for details). -If you do not want to record metrics for all Spring WebFlux requests, you can set configprop:management.metrics.web.server.request.autotime.enabled[] to `false` and exclusively use `@Timed` annotations instead. +You can customize the name by setting the configprop:management.observations.http.server.requests.name[] property. By default, WebFlux related metrics are tagged with the following information: @@ -823,10 +817,7 @@ Applications can opt in and record exceptions by <> for details). -If you do not want to record metrics for all Jersey requests, you can set configprop:management.metrics.web.server.request.autotime.enabled[] to `false` and exclusively use `@Timed` annotations instead. +You can customize the name by setting the configprop:management.observations.http.server.requests.name[] property. By default, Jersey server metrics are tagged with the following information: