From 07766c436cb89128eac5389d33f76329b758d8df Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Tue, 22 Nov 2022 20:44:05 +0100 Subject: [PATCH] Apply user-provided ObservationConventions in auto-configurations Prior to this commit, we would advise developers, as migration path from Spring Boot 2.0-x metrics, to create `GlobalObservationConvention` beans for the observations they want to customize (observation name or key values). `GlobalObservationConvention` are currently applied **in addition** to the chosen convention in some cases, so this does not work well with this migration path. Instead, instrumentations always provide a default convention but also a way to configure a custom convention for their observations. Spring Boot should inject custom convention beans in the relevant auto-configurations. Fixes gh-33285 --- .../GraphQlObservationAutoConfiguration.java | 11 ++++-- .../RestTemplateObservationConfiguration.java | 30 ++++++++++++--- .../WebClientObservationConfiguration.java | 34 +++++++++++++---- .../WebFluxObservationAutoConfiguration.java | 10 ++++- .../WebMvcObservationAutoConfiguration.java | 23 +++++++++--- ...phQlObservationAutoConfigurationTests.java | 37 +++++++++++++++++++ ...TemplateObservationConfigurationTests.java | 35 +++++++++++++++++- ...ebClientObservationConfigurationTests.java | 36 ++++++++++++++++++ ...FluxObservationAutoConfigurationTests.java | 24 ++++++++++++ ...bMvcObservationAutoConfigurationTests.java | 22 +++++++++++ .../src/docs/asciidoc/actuator/metrics.adoc | 4 +- 11 files changed, 238 insertions(+), 28 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/graphql/GraphQlObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/graphql/GraphQlObservationAutoConfiguration.java index 0885ad5f42..6ef41b4246 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/graphql/GraphQlObservationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/graphql/GraphQlObservationAutoConfiguration.java @@ -20,6 +20,7 @@ import graphql.GraphQL; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -28,6 +29,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.graphql.execution.GraphQlSource; +import org.springframework.graphql.observation.DataFetcherObservationConvention; +import org.springframework.graphql.observation.ExecutionRequestObservationConvention; import org.springframework.graphql.observation.GraphQlObservationInstrumentation; /** @@ -45,9 +48,11 @@ public class GraphQlObservationAutoConfiguration { @Bean @ConditionalOnMissingBean - public GraphQlObservationInstrumentation graphQlObservationInstrumentation( - ObservationRegistry observationRegistry) { - return new GraphQlObservationInstrumentation(observationRegistry); + public GraphQlObservationInstrumentation graphQlObservationInstrumentation(ObservationRegistry observationRegistry, + ObjectProvider executionConvention, + ObjectProvider dataFetcherConvention) { + return new GraphQlObservationInstrumentation(observationRegistry, executionConvention.getIfAvailable(), + dataFetcherConvention.getIfAvailable()); } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfiguration.java index 184b9ef698..0f62f2849b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfiguration.java @@ -45,16 +45,34 @@ class RestTemplateObservationConfiguration { @Bean ObservationRestTemplateCustomizer observationRestTemplateCustomizer(ObservationRegistry observationRegistry, + ObjectProvider customConvention, ObservationProperties observationProperties, MetricsProperties metricsProperties, ObjectProvider optionalTagsProvider) { + String name = observationName(observationProperties, metricsProperties); + ClientRequestObservationConvention observationConvention = createConvention(customConvention.getIfAvailable(), + name, optionalTagsProvider.getIfAvailable()); + return new ObservationRestTemplateCustomizer(observationRegistry, observationConvention); + } + + private static String observationName(ObservationProperties observationProperties, + MetricsProperties metricsProperties) { String metricName = metricsProperties.getWeb().getClient().getRequest().getMetricName(); String observationName = observationProperties.getHttp().getClient().getRequests().getName(); - String name = (observationName != null) ? observationName : metricName; - RestTemplateExchangeTagsProvider tagsProvider = optionalTagsProvider.getIfAvailable(); - ClientRequestObservationConvention observationConvention = (tagsProvider != null) - ? new ClientHttpObservationConventionAdapter(name, tagsProvider) - : new DefaultClientRequestObservationConvention(name); - return new ObservationRestTemplateCustomizer(observationRegistry, observationConvention); + return (observationName != null) ? observationName : metricName; + } + + private static ClientRequestObservationConvention createConvention( + ClientRequestObservationConvention customConvention, String name, + RestTemplateExchangeTagsProvider tagsProvider) { + if (customConvention != null) { + return customConvention; + } + else if (tagsProvider != null) { + return new ClientHttpObservationConventionAdapter(name, tagsProvider); + } + else { + return new DefaultClientRequestObservationConvention(name); + } } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/WebClientObservationConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/WebClientObservationConfiguration.java index 3b1ccf6377..ce531912e1 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/WebClientObservationConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/WebClientObservationConfiguration.java @@ -42,16 +42,34 @@ class WebClientObservationConfiguration { @Bean ObservationWebClientCustomizer observationWebClientCustomizer(ObservationRegistry observationRegistry, - ObservationProperties observationProperties, - ObjectProvider optionalTagsProvider, MetricsProperties metricsProperties) { + ObjectProvider customConvention, + ObservationProperties observationProperties, ObjectProvider tagsProvider, + MetricsProperties metricsProperties) { + String name = observationName(observationProperties, metricsProperties); + ClientRequestObservationConvention observationConvention = createConvention(customConvention.getIfAvailable(), + tagsProvider.getIfAvailable(), name); + return new ObservationWebClientCustomizer(observationRegistry, observationConvention); + } + + private static ClientRequestObservationConvention createConvention( + ClientRequestObservationConvention customConvention, WebClientExchangeTagsProvider tagsProvider, + String name) { + if (customConvention != null) { + return customConvention; + } + else if (tagsProvider != null) { + return new ClientObservationConventionAdapter(name, tagsProvider); + } + else { + return new DefaultClientRequestObservationConvention(name); + } + } + + private static String observationName(ObservationProperties observationProperties, + MetricsProperties metricsProperties) { String metricName = metricsProperties.getWeb().getClient().getRequest().getMetricName(); String observationName = observationProperties.getHttp().getClient().getRequests().getName(); - String name = (observationName != null) ? observationName : metricName; - WebClientExchangeTagsProvider tagsProvider = optionalTagsProvider.getIfAvailable(); - ClientRequestObservationConvention observationConvention = (tagsProvider != null) - ? new ClientObservationConventionAdapter(name, tagsProvider) - : new DefaultClientRequestObservationConvention(name); - return new ObservationWebClientCustomizer(observationRegistry, observationConvention); + return (observationName != null) ? observationName : metricName; } } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java index 85318606c4..27860e3b1b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java @@ -78,6 +78,7 @@ public class WebFluxObservationAutoConfiguration { @Bean @ConditionalOnMissingBean public ServerHttpObservationFilter webfluxObservationFilter(ObservationRegistry registry, + ObjectProvider customConvention, ObjectProvider tagConfigurer, ObjectProvider contributorsProvider) { String observationName = this.observationProperties.getHttp().getServer().getRequests().getName(); @@ -85,12 +86,17 @@ public class WebFluxObservationAutoConfiguration { String name = (observationName != null) ? observationName : metricName; WebFluxTagsProvider tagsProvider = tagConfigurer.getIfAvailable(); List tagsContributors = contributorsProvider.orderedStream().toList(); - ServerRequestObservationConvention convention = createConvention(name, tagsProvider, tagsContributors); + ServerRequestObservationConvention convention = createConvention(customConvention.getIfAvailable(), name, + tagsProvider, tagsContributors); return new ServerHttpObservationFilter(registry, convention); } - private ServerRequestObservationConvention createConvention(String name, WebFluxTagsProvider tagsProvider, + private static ServerRequestObservationConvention createConvention( + ServerRequestObservationConvention customConvention, String name, WebFluxTagsProvider tagsProvider, List tagsContributors) { + if (customConvention != null) { + return customConvention; + } if (tagsProvider != null) { return new ServerRequestObservationConventionAdapter(name, tagsProvider); } 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 index f20f75db0c..5a70728ac1 100644 --- 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 @@ -82,15 +82,12 @@ public class WebMvcObservationAutoConfiguration { @Bean @ConditionalOnMissingFilterBean public FilterRegistrationBean webMvcObservationFilter(ObservationRegistry registry, + ObjectProvider customConvention, ObjectProvider customTagsProvider, ObjectProvider contributorsProvider) { String name = httpRequestsMetricName(this.observationProperties, this.metricsProperties); - 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); - } + ServerRequestObservationConvention convention = createConvention(customConvention.getIfAvailable(), name, + customTagsProvider.getIfAvailable(), contributorsProvider.orderedStream().toList()); ServerHttpObservationFilter filter = new ServerHttpObservationFilter(registry, convention); FilterRegistrationBean registration = new FilterRegistrationBean<>(filter); registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1); @@ -98,6 +95,20 @@ public class WebMvcObservationAutoConfiguration { return registration; } + private static ServerRequestObservationConvention createConvention( + ServerRequestObservationConvention customConvention, String name, WebMvcTagsProvider tagsProvider, + List contributors) { + if (customConvention != null) { + return customConvention; + } + else if (tagsProvider != null || contributors.size() > 0) { + return new ServerRequestObservationConventionAdapter(name, tagsProvider, contributors); + } + else { + return new DefaultServerRequestObservationConvention(name); + } + } + private static String httpRequestsMetricName(ObservationProperties observationProperties, MetricsProperties metricsProperties) { String observationName = observationProperties.getHttp().getServer().getRequests().getName(); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/graphql/GraphQlObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/graphql/GraphQlObservationAutoConfigurationTests.java index 326a8e2adf..cba3b691d9 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/graphql/GraphQlObservationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/graphql/GraphQlObservationAutoConfigurationTests.java @@ -24,6 +24,8 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.graphql.observation.DefaultDataFetcherObservationConvention; +import org.springframework.graphql.observation.DefaultExecutionRequestObservationConvention; import org.springframework.graphql.observation.GraphQlObservationInstrumentation; import static org.assertj.core.api.Assertions.assertThat; @@ -58,6 +60,18 @@ class GraphQlObservationAutoConfigurationTests { .hasBean("customInstrumentation")); } + @Test + void instrumentationUsesCustomConventionsIfAvailable() { + this.contextRunner.withUserConfiguration(CustomConventionsConfiguration.class).run((context) -> { + GraphQlObservationInstrumentation instrumentation = context + .getBean(GraphQlObservationInstrumentation.class); + assertThat(instrumentation).extracting("requestObservationConvention") + .isInstanceOf(CustomExecutionRequestObservationConvention.class); + assertThat(instrumentation).extracting("dataFetcherObservationConvention") + .isInstanceOf(CustomDataFetcherObservationConvention.class); + }); + } + @Configuration(proxyBeanMethods = false) static class InstrumentationConfiguration { @@ -68,4 +82,27 @@ class GraphQlObservationAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class CustomConventionsConfiguration { + + @Bean + CustomExecutionRequestObservationConvention customExecutionConvention() { + return new CustomExecutionRequestObservationConvention(); + } + + @Bean + CustomDataFetcherObservationConvention customDataFetcherConvention() { + return new CustomDataFetcherObservationConvention(); + } + + } + + static class CustomExecutionRequestObservationConvention extends DefaultExecutionRequestObservationConvention { + + } + + static class CustomDataFetcherObservationConvention extends DefaultDataFetcherObservationConvention { + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfigurationTests.java index 3727f47d11..a9b34f4c62 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfigurationTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.actuate.autoconfigure.observation.web.client; +import io.micrometer.common.KeyValues; import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Tags; import io.micrometer.observation.ObservationRegistry; @@ -41,6 +42,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpRequest; import org.springframework.http.HttpStatus; import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.client.observation.ClientRequestObservationContext; +import org.springframework.http.client.observation.DefaultClientRequestObservationConvention; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.client.RestTemplate; @@ -116,6 +119,17 @@ class RestTemplateObservationConfigurationTests { }); } + @Test + void restTemplateCreatedWithBuilderUsesCustomConvention() { + this.contextRunner.withUserConfiguration(CustomConvention.class).run((context) -> { + RestTemplate restTemplate = buildRestTemplate(context); + restTemplate.getForEntity("/projects/{project}", Void.class, "spring-boot"); + TestObservationRegistry registry = context.getBean(TestObservationRegistry.class); + TestObservationRegistryAssert.assertThat(registry).hasObservationWithNameEqualTo("http.client.requests") + .that().hasLowCardinalityKeyValue("project", "spring-boot"); + }); + } + @Test void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) { this.contextRunner.with(MetricsRun.simple()).withPropertyValues("management.metrics.web.client.max-uri-tags=2") @@ -153,7 +167,7 @@ class RestTemplateObservationConfigurationTests { return restTemplate; } - @Configuration + @Configuration(proxyBeanMethods = false) static class CustomTagsConfiguration { @Bean @@ -173,4 +187,23 @@ class RestTemplateObservationConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class CustomConventionConfiguration { + + @Bean + CustomConvention customConvention() { + return new CustomConvention(); + } + + } + + static class CustomConvention extends DefaultClientRequestObservationConvention { + + @Override + public KeyValues getLowCardinalityKeyValues(ClientRequestObservationContext context) { + return super.getLowCardinalityKeyValues(context).and("project", "spring-boot"); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/WebClientObservationConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/WebClientObservationConfigurationTests.java index f07b3f41c1..c9a5cd8f8a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/WebClientObservationConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/WebClientObservationConfigurationTests.java @@ -18,6 +18,7 @@ package org.springframework.boot.actuate.autoconfigure.observation.web.client; import java.time.Duration; +import io.micrometer.common.KeyValues; import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.tck.TestObservationRegistry; import io.micrometer.observation.tck.TestObservationRegistryAssert; @@ -41,6 +42,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.mock.http.client.reactive.MockClientHttpResponse; +import org.springframework.web.reactive.function.client.ClientRequestObservationContext; +import org.springframework.web.reactive.function.client.DefaultClientRequestObservationConvention; import org.springframework.web.reactive.function.client.WebClient; import static org.assertj.core.api.Assertions.assertThat; @@ -84,6 +87,20 @@ class WebClientObservationConfigurationTests { .getBeans(WebClientExchangeTagsProvider.class).hasSize(1).containsKey("customTagsProvider")); } + @Test + void shouldUseCustomConventionIfAvailable() { + this.contextRunner.withUserConfiguration(CustomConvention.class).run((context) -> { + TestObservationRegistry registry = context.getBean(TestObservationRegistry.class); + WebClient.Builder builder = context.getBean(WebClient.Builder.class); + WebClient webClient = mockWebClient(builder); + TestObservationRegistryAssert.assertThat(registry).doesNotHaveAnyObservation(); + webClient.get().uri("https://example.org/projects/{project}", "spring-boot").retrieve().toBodilessEntity() + .block(Duration.ofSeconds(30)); + TestObservationRegistryAssert.assertThat(registry).hasObservationWithNameEqualTo("http.client.requests") + .that().hasLowCardinalityKeyValue("project", "spring-boot"); + }); + } + @Test void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) { this.contextRunner.withPropertyValues("management.metrics.web.client.max-uri-tags=2").run((context) -> { @@ -141,4 +158,23 @@ class WebClientObservationConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class CustomConventionConfig { + + @Bean + CustomConvention customConvention() { + return new CustomConvention(); + } + + } + + static class CustomConvention extends DefaultClientRequestObservationConvention { + + @Override + public KeyValues getLowCardinalityKeyValues(ClientRequestObservationContext context) { + return super.getLowCardinalityKeyValues(context).and("project", "spring-boot"); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java index b71a80a66e..d2cfe8c4d2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfigurationTests.java @@ -37,6 +37,7 @@ import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.filter.reactive.ServerHttpObservationFilter; import org.springframework.web.server.ServerWebExchange; @@ -83,6 +84,15 @@ class WebFluxObservationAutoConfigurationTests { }); } + @Test + void shouldUseCustomConventionWhenAvailable() { + this.contextRunner.withUserConfiguration(CustomConventionConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(ServerHttpObservationFilter.class); + assertThat(context).getBean(ServerHttpObservationFilter.class).extracting("observationConvention") + .isInstanceOf(CustomConvention.class); + }); + } + @Test void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) { this.contextRunner.withUserConfiguration(TestController.class) @@ -183,4 +193,18 @@ class WebFluxObservationAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class CustomConventionConfiguration { + + @Bean + CustomConvention customConvention() { + return new CustomConvention(); + } + + } + + static class CustomConvention extends DefaultServerRequestObservationConvention { + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java index ae069ebfcc..6627007768 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfigurationTests.java @@ -46,6 +46,7 @@ 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.http.server.observation.DefaultServerRequestObservationConvention; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders; @@ -97,6 +98,13 @@ class WebMvcObservationAutoConfigurationTests { .isInstanceOf(ServerRequestObservationConventionAdapter.class)); } + @Test + void customConventionWhenPresent() { + this.contextRunner.withUserConfiguration(CustomConventionConfiguration.class) + .run((context) -> assertThat(context.getBean(FilterRegistrationBean.class).getFilter()) + .extracting("observationConvention").isInstanceOf(CustomConvention.class)); + } + @Test void filterRegistrationHasExpectedDispatcherTypesAndOrder() { this.contextRunner.run((context) -> { @@ -297,4 +305,18 @@ class WebMvcObservationAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class CustomConventionConfiguration { + + @Bean + CustomConvention customConvention() { + return new CustomConvention(); + } + + } + + static class CustomConvention extends DefaultServerRequestObservationConvention { + + } + } 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 722a3258c0..9c8e2fdbd9 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 @@ -769,7 +769,7 @@ By default, Spring MVC related metrics are tagged with the following information |=== To add to the default tags, provide a `@Bean` that extends `DefaultServerRequestObservationConvention` from the `org.springframework.http.observation` package. -To replace the default tags, provide a `@Bean` that implements `GlobalObservationConvention`. +To replace the default tags, provide a `@Bean` that implements `ServerRequestObservationConvention`. TIP: In some cases, exceptions handled in web controllers are not recorded as request metrics tags. @@ -809,7 +809,7 @@ By default, WebFlux related metrics are tagged with the following information: |=== To add to the default tags, provide a `@Bean` that extends `DefaultServerRequestObservationConvention` from the `org.springframework.http.observation.reactive` package. -To replace the default tags, provide a `@Bean` that implements `GlobalObservationConvention`. +To replace the default tags, provide a `@Bean` that implements `ServerRequestObservationConvention`. TIP: In some cases, exceptions handled in controllers and handler functions are not recorded as request metrics tags. Applications can opt in and record exceptions by <>.