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
pull/33335/head
Brian Clozel 2 years ago
parent f6ac891cc1
commit 07766c436c

@ -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<ExecutionRequestObservationConvention> executionConvention,
ObjectProvider<DataFetcherObservationConvention> dataFetcherConvention) {
return new GraphQlObservationInstrumentation(observationRegistry, executionConvention.getIfAvailable(),
dataFetcherConvention.getIfAvailable());
}
}

@ -45,16 +45,34 @@ class RestTemplateObservationConfiguration {
@Bean
ObservationRestTemplateCustomizer observationRestTemplateCustomizer(ObservationRegistry observationRegistry,
ObjectProvider<ClientRequestObservationConvention> customConvention,
ObservationProperties observationProperties, MetricsProperties metricsProperties,
ObjectProvider<RestTemplateExchangeTagsProvider> 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);
}
}
}

@ -42,16 +42,34 @@ class WebClientObservationConfiguration {
@Bean
ObservationWebClientCustomizer observationWebClientCustomizer(ObservationRegistry observationRegistry,
ObservationProperties observationProperties,
ObjectProvider<WebClientExchangeTagsProvider> optionalTagsProvider, MetricsProperties metricsProperties) {
ObjectProvider<ClientRequestObservationConvention> customConvention,
ObservationProperties observationProperties, ObjectProvider<WebClientExchangeTagsProvider> 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;
}
}

@ -78,6 +78,7 @@ public class WebFluxObservationAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ServerHttpObservationFilter webfluxObservationFilter(ObservationRegistry registry,
ObjectProvider<ServerRequestObservationConvention> customConvention,
ObjectProvider<WebFluxTagsProvider> tagConfigurer,
ObjectProvider<WebFluxTagsContributor> 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<WebFluxTagsContributor> 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<WebFluxTagsContributor> tagsContributors) {
if (customConvention != null) {
return customConvention;
}
if (tagsProvider != null) {
return new ServerRequestObservationConventionAdapter(name, tagsProvider);
}

@ -82,15 +82,12 @@ public class WebMvcObservationAutoConfiguration {
@Bean
@ConditionalOnMissingFilterBean
public FilterRegistrationBean<ServerHttpObservationFilter> webMvcObservationFilter(ObservationRegistry registry,
ObjectProvider<ServerRequestObservationConvention> customConvention,
ObjectProvider<WebMvcTagsProvider> customTagsProvider,
ObjectProvider<WebMvcTagsContributor> contributorsProvider) {
String name = httpRequestsMetricName(this.observationProperties, this.metricsProperties);
ServerRequestObservationConvention convention = new DefaultServerRequestObservationConvention(name);
WebMvcTagsProvider tagsProvider = customTagsProvider.getIfAvailable();
List<WebMvcTagsContributor> 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<ServerHttpObservationFilter> 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<WebMvcTagsContributor> 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();

@ -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 {
}
}

@ -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");
}
}
}

@ -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");
}
}
}

@ -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 {
}
}

@ -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 {
}
}

@ -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<ServerRequestObservationContext>`.
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<ServerRequestObservationContext>`.
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 <<web#web.reactive.webflux.error-handling, setting handled exceptions as request attributes>>.

Loading…
Cancel
Save