Skip to content

Commit 07766c4

Browse files
committed
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
1 parent f6ac891 commit 07766c4

File tree

11 files changed

+238
-28
lines changed

11 files changed

+238
-28
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/graphql/GraphQlObservationAutoConfiguration.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import io.micrometer.observation.Observation;
2121
import io.micrometer.observation.ObservationRegistry;
2222

23+
import org.springframework.beans.factory.ObjectProvider;
2324
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
2425
import org.springframework.boot.autoconfigure.AutoConfiguration;
2526
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@@ -28,6 +29,8 @@
2829
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2930
import org.springframework.context.annotation.Bean;
3031
import org.springframework.graphql.execution.GraphQlSource;
32+
import org.springframework.graphql.observation.DataFetcherObservationConvention;
33+
import org.springframework.graphql.observation.ExecutionRequestObservationConvention;
3134
import org.springframework.graphql.observation.GraphQlObservationInstrumentation;
3235

3336
/**
@@ -45,9 +48,11 @@ public class GraphQlObservationAutoConfiguration {
4548

4649
@Bean
4750
@ConditionalOnMissingBean
48-
public GraphQlObservationInstrumentation graphQlObservationInstrumentation(
49-
ObservationRegistry observationRegistry) {
50-
return new GraphQlObservationInstrumentation(observationRegistry);
51+
public GraphQlObservationInstrumentation graphQlObservationInstrumentation(ObservationRegistry observationRegistry,
52+
ObjectProvider<ExecutionRequestObservationConvention> executionConvention,
53+
ObjectProvider<DataFetcherObservationConvention> dataFetcherConvention) {
54+
return new GraphQlObservationInstrumentation(observationRegistry, executionConvention.getIfAvailable(),
55+
dataFetcherConvention.getIfAvailable());
5156
}
5257

5358
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfiguration.java

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,34 @@ class RestTemplateObservationConfiguration {
4545

4646
@Bean
4747
ObservationRestTemplateCustomizer observationRestTemplateCustomizer(ObservationRegistry observationRegistry,
48+
ObjectProvider<ClientRequestObservationConvention> customConvention,
4849
ObservationProperties observationProperties, MetricsProperties metricsProperties,
4950
ObjectProvider<RestTemplateExchangeTagsProvider> optionalTagsProvider) {
51+
String name = observationName(observationProperties, metricsProperties);
52+
ClientRequestObservationConvention observationConvention = createConvention(customConvention.getIfAvailable(),
53+
name, optionalTagsProvider.getIfAvailable());
54+
return new ObservationRestTemplateCustomizer(observationRegistry, observationConvention);
55+
}
56+
57+
private static String observationName(ObservationProperties observationProperties,
58+
MetricsProperties metricsProperties) {
5059
String metricName = metricsProperties.getWeb().getClient().getRequest().getMetricName();
5160
String observationName = observationProperties.getHttp().getClient().getRequests().getName();
52-
String name = (observationName != null) ? observationName : metricName;
53-
RestTemplateExchangeTagsProvider tagsProvider = optionalTagsProvider.getIfAvailable();
54-
ClientRequestObservationConvention observationConvention = (tagsProvider != null)
55-
? new ClientHttpObservationConventionAdapter(name, tagsProvider)
56-
: new DefaultClientRequestObservationConvention(name);
57-
return new ObservationRestTemplateCustomizer(observationRegistry, observationConvention);
61+
return (observationName != null) ? observationName : metricName;
62+
}
63+
64+
private static ClientRequestObservationConvention createConvention(
65+
ClientRequestObservationConvention customConvention, String name,
66+
RestTemplateExchangeTagsProvider tagsProvider) {
67+
if (customConvention != null) {
68+
return customConvention;
69+
}
70+
else if (tagsProvider != null) {
71+
return new ClientHttpObservationConventionAdapter(name, tagsProvider);
72+
}
73+
else {
74+
return new DefaultClientRequestObservationConvention(name);
75+
}
5876
}
5977

6078
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/WebClientObservationConfiguration.java

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,34 @@ class WebClientObservationConfiguration {
4242

4343
@Bean
4444
ObservationWebClientCustomizer observationWebClientCustomizer(ObservationRegistry observationRegistry,
45-
ObservationProperties observationProperties,
46-
ObjectProvider<WebClientExchangeTagsProvider> optionalTagsProvider, MetricsProperties metricsProperties) {
45+
ObjectProvider<ClientRequestObservationConvention> customConvention,
46+
ObservationProperties observationProperties, ObjectProvider<WebClientExchangeTagsProvider> tagsProvider,
47+
MetricsProperties metricsProperties) {
48+
String name = observationName(observationProperties, metricsProperties);
49+
ClientRequestObservationConvention observationConvention = createConvention(customConvention.getIfAvailable(),
50+
tagsProvider.getIfAvailable(), name);
51+
return new ObservationWebClientCustomizer(observationRegistry, observationConvention);
52+
}
53+
54+
private static ClientRequestObservationConvention createConvention(
55+
ClientRequestObservationConvention customConvention, WebClientExchangeTagsProvider tagsProvider,
56+
String name) {
57+
if (customConvention != null) {
58+
return customConvention;
59+
}
60+
else if (tagsProvider != null) {
61+
return new ClientObservationConventionAdapter(name, tagsProvider);
62+
}
63+
else {
64+
return new DefaultClientRequestObservationConvention(name);
65+
}
66+
}
67+
68+
private static String observationName(ObservationProperties observationProperties,
69+
MetricsProperties metricsProperties) {
4770
String metricName = metricsProperties.getWeb().getClient().getRequest().getMetricName();
4871
String observationName = observationProperties.getHttp().getClient().getRequests().getName();
49-
String name = (observationName != null) ? observationName : metricName;
50-
WebClientExchangeTagsProvider tagsProvider = optionalTagsProvider.getIfAvailable();
51-
ClientRequestObservationConvention observationConvention = (tagsProvider != null)
52-
? new ClientObservationConventionAdapter(name, tagsProvider)
53-
: new DefaultClientRequestObservationConvention(name);
54-
return new ObservationWebClientCustomizer(observationRegistry, observationConvention);
72+
return (observationName != null) ? observationName : metricName;
5573
}
5674

5775
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/reactive/WebFluxObservationAutoConfiguration.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,19 +78,25 @@ public WebFluxObservationAutoConfiguration(MetricsProperties metricsProperties,
7878
@Bean
7979
@ConditionalOnMissingBean
8080
public ServerHttpObservationFilter webfluxObservationFilter(ObservationRegistry registry,
81+
ObjectProvider<ServerRequestObservationConvention> customConvention,
8182
ObjectProvider<WebFluxTagsProvider> tagConfigurer,
8283
ObjectProvider<WebFluxTagsContributor> contributorsProvider) {
8384
String observationName = this.observationProperties.getHttp().getServer().getRequests().getName();
8485
String metricName = this.metricsProperties.getWeb().getServer().getRequest().getMetricName();
8586
String name = (observationName != null) ? observationName : metricName;
8687
WebFluxTagsProvider tagsProvider = tagConfigurer.getIfAvailable();
8788
List<WebFluxTagsContributor> tagsContributors = contributorsProvider.orderedStream().toList();
88-
ServerRequestObservationConvention convention = createConvention(name, tagsProvider, tagsContributors);
89+
ServerRequestObservationConvention convention = createConvention(customConvention.getIfAvailable(), name,
90+
tagsProvider, tagsContributors);
8991
return new ServerHttpObservationFilter(registry, convention);
9092
}
9193

92-
private ServerRequestObservationConvention createConvention(String name, WebFluxTagsProvider tagsProvider,
94+
private static ServerRequestObservationConvention createConvention(
95+
ServerRequestObservationConvention customConvention, String name, WebFluxTagsProvider tagsProvider,
9396
List<WebFluxTagsContributor> tagsContributors) {
97+
if (customConvention != null) {
98+
return customConvention;
99+
}
94100
if (tagsProvider != null) {
95101
return new ServerRequestObservationConventionAdapter(name, tagsProvider);
96102
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/observation/web/servlet/WebMvcObservationAutoConfiguration.java

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,22 +82,33 @@ public WebMvcObservationAutoConfiguration(ObservationProperties observationPrope
8282
@Bean
8383
@ConditionalOnMissingFilterBean
8484
public FilterRegistrationBean<ServerHttpObservationFilter> webMvcObservationFilter(ObservationRegistry registry,
85+
ObjectProvider<ServerRequestObservationConvention> customConvention,
8586
ObjectProvider<WebMvcTagsProvider> customTagsProvider,
8687
ObjectProvider<WebMvcTagsContributor> contributorsProvider) {
8788
String name = httpRequestsMetricName(this.observationProperties, this.metricsProperties);
88-
ServerRequestObservationConvention convention = new DefaultServerRequestObservationConvention(name);
89-
WebMvcTagsProvider tagsProvider = customTagsProvider.getIfAvailable();
90-
List<WebMvcTagsContributor> contributors = contributorsProvider.orderedStream().toList();
91-
if (tagsProvider != null || contributors.size() > 0) {
92-
convention = new ServerRequestObservationConventionAdapter(name, tagsProvider, contributors);
93-
}
89+
ServerRequestObservationConvention convention = createConvention(customConvention.getIfAvailable(), name,
90+
customTagsProvider.getIfAvailable(), contributorsProvider.orderedStream().toList());
9491
ServerHttpObservationFilter filter = new ServerHttpObservationFilter(registry, convention);
9592
FilterRegistrationBean<ServerHttpObservationFilter> registration = new FilterRegistrationBean<>(filter);
9693
registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
9794
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
9895
return registration;
9996
}
10097

98+
private static ServerRequestObservationConvention createConvention(
99+
ServerRequestObservationConvention customConvention, String name, WebMvcTagsProvider tagsProvider,
100+
List<WebMvcTagsContributor> contributors) {
101+
if (customConvention != null) {
102+
return customConvention;
103+
}
104+
else if (tagsProvider != null || contributors.size() > 0) {
105+
return new ServerRequestObservationConventionAdapter(name, tagsProvider, contributors);
106+
}
107+
else {
108+
return new DefaultServerRequestObservationConvention(name);
109+
}
110+
}
111+
101112
private static String httpRequestsMetricName(ObservationProperties observationProperties,
102113
MetricsProperties metricsProperties) {
103114
String observationName = observationProperties.getHttp().getServer().getRequests().getName();

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/graphql/GraphQlObservationAutoConfigurationTests.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
2525
import org.springframework.context.annotation.Bean;
2626
import org.springframework.context.annotation.Configuration;
27+
import org.springframework.graphql.observation.DefaultDataFetcherObservationConvention;
28+
import org.springframework.graphql.observation.DefaultExecutionRequestObservationConvention;
2729
import org.springframework.graphql.observation.GraphQlObservationInstrumentation;
2830

2931
import static org.assertj.core.api.Assertions.assertThat;
@@ -58,6 +60,18 @@ void instrumentationBacksOffIfAlreadyPresent() {
5860
.hasBean("customInstrumentation"));
5961
}
6062

63+
@Test
64+
void instrumentationUsesCustomConventionsIfAvailable() {
65+
this.contextRunner.withUserConfiguration(CustomConventionsConfiguration.class).run((context) -> {
66+
GraphQlObservationInstrumentation instrumentation = context
67+
.getBean(GraphQlObservationInstrumentation.class);
68+
assertThat(instrumentation).extracting("requestObservationConvention")
69+
.isInstanceOf(CustomExecutionRequestObservationConvention.class);
70+
assertThat(instrumentation).extracting("dataFetcherObservationConvention")
71+
.isInstanceOf(CustomDataFetcherObservationConvention.class);
72+
});
73+
}
74+
6175
@Configuration(proxyBeanMethods = false)
6276
static class InstrumentationConfiguration {
6377

@@ -68,4 +82,27 @@ GraphQlObservationInstrumentation customInstrumentation(ObservationRegistry regi
6882

6983
}
7084

85+
@Configuration(proxyBeanMethods = false)
86+
static class CustomConventionsConfiguration {
87+
88+
@Bean
89+
CustomExecutionRequestObservationConvention customExecutionConvention() {
90+
return new CustomExecutionRequestObservationConvention();
91+
}
92+
93+
@Bean
94+
CustomDataFetcherObservationConvention customDataFetcherConvention() {
95+
return new CustomDataFetcherObservationConvention();
96+
}
97+
98+
}
99+
100+
static class CustomExecutionRequestObservationConvention extends DefaultExecutionRequestObservationConvention {
101+
102+
}
103+
104+
static class CustomDataFetcherObservationConvention extends DefaultDataFetcherObservationConvention {
105+
106+
}
107+
71108
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/RestTemplateObservationConfigurationTests.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.observation.web.client;
1818

19+
import io.micrometer.common.KeyValues;
1920
import io.micrometer.core.instrument.Tag;
2021
import io.micrometer.core.instrument.Tags;
2122
import io.micrometer.observation.ObservationRegistry;
@@ -41,6 +42,8 @@
4142
import org.springframework.http.HttpRequest;
4243
import org.springframework.http.HttpStatus;
4344
import org.springframework.http.client.ClientHttpResponse;
45+
import org.springframework.http.client.observation.ClientRequestObservationContext;
46+
import org.springframework.http.client.observation.DefaultClientRequestObservationConvention;
4447
import org.springframework.test.web.client.MockRestServiceServer;
4548
import org.springframework.web.client.RestTemplate;
4649

@@ -116,6 +119,17 @@ void restTemplateCreatedWithBuilderUsesCustomTagsProvider() {
116119
});
117120
}
118121

122+
@Test
123+
void restTemplateCreatedWithBuilderUsesCustomConvention() {
124+
this.contextRunner.withUserConfiguration(CustomConvention.class).run((context) -> {
125+
RestTemplate restTemplate = buildRestTemplate(context);
126+
restTemplate.getForEntity("/projects/{project}", Void.class, "spring-boot");
127+
TestObservationRegistry registry = context.getBean(TestObservationRegistry.class);
128+
TestObservationRegistryAssert.assertThat(registry).hasObservationWithNameEqualTo("http.client.requests")
129+
.that().hasLowCardinalityKeyValue("project", "spring-boot");
130+
});
131+
}
132+
119133
@Test
120134
void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) {
121135
this.contextRunner.with(MetricsRun.simple()).withPropertyValues("management.metrics.web.client.max-uri-tags=2")
@@ -153,7 +167,7 @@ private RestTemplate buildRestTemplate(AssertableApplicationContext context) {
153167
return restTemplate;
154168
}
155169

156-
@Configuration
170+
@Configuration(proxyBeanMethods = false)
157171
static class CustomTagsConfiguration {
158172

159173
@Bean
@@ -173,4 +187,23 @@ public Iterable<Tag> getTags(String urlTemplate, HttpRequest request, ClientHttp
173187

174188
}
175189

190+
@Configuration(proxyBeanMethods = false)
191+
static class CustomConventionConfiguration {
192+
193+
@Bean
194+
CustomConvention customConvention() {
195+
return new CustomConvention();
196+
}
197+
198+
}
199+
200+
static class CustomConvention extends DefaultClientRequestObservationConvention {
201+
202+
@Override
203+
public KeyValues getLowCardinalityKeyValues(ClientRequestObservationContext context) {
204+
return super.getLowCardinalityKeyValues(context).and("project", "spring-boot");
205+
}
206+
207+
}
208+
176209
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/observation/web/client/WebClientObservationConfigurationTests.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.time.Duration;
2020

21+
import io.micrometer.common.KeyValues;
2122
import io.micrometer.observation.ObservationRegistry;
2223
import io.micrometer.observation.tck.TestObservationRegistry;
2324
import io.micrometer.observation.tck.TestObservationRegistryAssert;
@@ -41,6 +42,8 @@
4142
import org.springframework.http.HttpStatus;
4243
import org.springframework.http.client.reactive.ClientHttpConnector;
4344
import org.springframework.mock.http.client.reactive.MockClientHttpResponse;
45+
import org.springframework.web.reactive.function.client.ClientRequestObservationContext;
46+
import org.springframework.web.reactive.function.client.DefaultClientRequestObservationConvention;
4447
import org.springframework.web.reactive.function.client.WebClient;
4548

4649
import static org.assertj.core.api.Assertions.assertThat;
@@ -84,6 +87,20 @@ void shouldNotOverrideCustomTagsProvider() {
8487
.getBeans(WebClientExchangeTagsProvider.class).hasSize(1).containsKey("customTagsProvider"));
8588
}
8689

90+
@Test
91+
void shouldUseCustomConventionIfAvailable() {
92+
this.contextRunner.withUserConfiguration(CustomConvention.class).run((context) -> {
93+
TestObservationRegistry registry = context.getBean(TestObservationRegistry.class);
94+
WebClient.Builder builder = context.getBean(WebClient.Builder.class);
95+
WebClient webClient = mockWebClient(builder);
96+
TestObservationRegistryAssert.assertThat(registry).doesNotHaveAnyObservation();
97+
webClient.get().uri("https://example.org/projects/{project}", "spring-boot").retrieve().toBodilessEntity()
98+
.block(Duration.ofSeconds(30));
99+
TestObservationRegistryAssert.assertThat(registry).hasObservationWithNameEqualTo("http.client.requests")
100+
.that().hasLowCardinalityKeyValue("project", "spring-boot");
101+
});
102+
}
103+
87104
@Test
88105
void afterMaxUrisReachedFurtherUrisAreDenied(CapturedOutput output) {
89106
this.contextRunner.withPropertyValues("management.metrics.web.client.max-uri-tags=2").run((context) -> {
@@ -141,4 +158,23 @@ WebClientExchangeTagsProvider customTagsProvider() {
141158

142159
}
143160

161+
@Configuration(proxyBeanMethods = false)
162+
static class CustomConventionConfig {
163+
164+
@Bean
165+
CustomConvention customConvention() {
166+
return new CustomConvention();
167+
}
168+
169+
}
170+
171+
static class CustomConvention extends DefaultClientRequestObservationConvention {
172+
173+
@Override
174+
public KeyValues getLowCardinalityKeyValues(ClientRequestObservationContext context) {
175+
return super.getLowCardinalityKeyValues(context).and("project", "spring-boot");
176+
}
177+
178+
}
179+
144180
}

0 commit comments

Comments
 (0)