Skip to content

Commit 9aa1769

Browse files
Add spring boot 4 support for WebClientCustomizer
Signed-off-by: Lenin Jaganathan <lenin.jaganathan@gmail.com>
1 parent 4870571 commit 9aa1769

File tree

8 files changed

+231
-66
lines changed

8 files changed

+231
-66
lines changed

instrumentation/spring/spring-boot-autoconfigure/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,13 +128,15 @@ dependencies {
128128
add("javaSpring4CompileOnly", "org.springframework.boot:spring-boot-jdbc:4.0.0")
129129
add("javaSpring4CompileOnly", "org.springframework.boot:spring-boot-starter-jdbc:4.0.0")
130130
add("javaSpring4CompileOnly", "org.springframework.boot:spring-boot-restclient:4.0.0")
131+
add("javaSpring4CompileOnly", "org.springframework.boot:spring-boot-webclient:4.0.0")
131132
add("javaSpring4CompileOnly", "org.springframework.boot:spring-boot-starter-data-mongodb:4.0.0")
132133
add("javaSpring4CompileOnly", "org.springframework.boot:spring-boot-starter-micrometer-metrics:4.0.0")
133134
add("javaSpring4CompileOnly", project(":instrumentation:kafka:kafka-clients:kafka-clients-2.6:library"))
134135
add("javaSpring4CompileOnly", project(":instrumentation:spring:spring-kafka-2.7:library"))
135136
add("javaSpring4CompileOnly", project(":instrumentation:mongo:mongo-3.1:library"))
136137
add("javaSpring4CompileOnly", project(":instrumentation:micrometer:micrometer-1.5:library"))
137138
add("javaSpring4CompileOnly", project(":instrumentation:spring:spring-web:spring-web-3.1:library"))
139+
add("javaSpring4CompileOnly", project(":instrumentation:spring:spring-webflux:spring-webflux-5.3:library"))
138140
}
139141

140142
val latestDepTest = findProperty("testLatestDeps") as Boolean
@@ -233,6 +235,7 @@ testing {
233235
val version = if (latestDepTest) "latest.release" else "4.0.0"
234236
implementation("org.springframework.boot:spring-boot-starter-jdbc:$version")
235237
implementation("org.springframework.boot:spring-boot-restclient:$version")
238+
implementation("org.springframework.boot:spring-boot-webclient:$version")
236239
implementation("org.springframework.boot:spring-boot-starter-kafka:$version")
237240
implementation("org.springframework.boot:spring-boot-starter-actuator:$version")
238241
implementation("org.springframework.boot:spring-boot-starter-data-r2dbc:$version")

instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebfluxInstrumentationAutoConfiguration.java

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
* at any time.
2929
*/
3030
@ConditionalOnEnabledInstrumentation(module = "spring-webflux")
31-
@ConditionalOnClass({WebClient.class, WebClientCustomizer.class})
31+
@ConditionalOnClass(WebClient.class)
3232
@Configuration
3333
public class SpringWebfluxInstrumentationAutoConfiguration {
3434

@@ -42,18 +42,23 @@ static WebClientBeanPostProcessor otelWebClientBeanPostProcessor(
4242
return new WebClientBeanPostProcessor(openTelemetryProvider, configProvider);
4343
}
4444

45-
@Bean
46-
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
47-
WebClientCustomizer otelWebClientCustomizer(
48-
OpenTelemetry openTelemetry, InstrumentationConfig config) {
49-
SpringWebfluxClientTelemetry webfluxClientTelemetry =
50-
WebClientBeanPostProcessor.getWebfluxClientTelemetry(openTelemetry, config);
51-
return builder -> builder.filters(webfluxClientTelemetry::addFilter);
52-
}
53-
5445
@Bean
5546
WebFilter telemetryFilter(OpenTelemetry openTelemetry, InstrumentationConfig config) {
5647
return WebClientBeanPostProcessor.getWebfluxServerTelemetry(openTelemetry, config)
5748
.createWebFilterAndRegisterReactorHook();
5849
}
50+
51+
@Configuration
52+
@ConditionalOnClass(WebClientCustomizer.class)
53+
static class OpentelemetryWebClientCustomizerConfiguration {
54+
55+
@Bean
56+
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
57+
WebClientCustomizer otelWebClientCustomizer(
58+
OpenTelemetry openTelemetry, InstrumentationConfig config) {
59+
SpringWebfluxClientTelemetry webfluxClientTelemetry =
60+
WebClientBeanPostProcessor.getWebfluxClientTelemetry(openTelemetry, config);
61+
return builder -> builder.filters(webfluxClientTelemetry::addFilter);
62+
}
63+
}
5964
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webflux;
7+
8+
import io.opentelemetry.api.OpenTelemetry;
9+
import io.opentelemetry.instrumentation.api.incubator.config.internal.InstrumentationConfig;
10+
import io.opentelemetry.instrumentation.spring.autoconfigure.internal.ConditionalOnEnabledInstrumentation;
11+
import io.opentelemetry.instrumentation.spring.webflux.v5_3.SpringWebfluxClientTelemetry;
12+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
13+
import org.springframework.boot.webclient.WebClientCustomizer;
14+
import org.springframework.context.annotation.Bean;
15+
import org.springframework.context.annotation.Configuration;
16+
import org.springframework.core.Ordered;
17+
import org.springframework.core.annotation.Order;
18+
import org.springframework.web.reactive.function.client.WebClient;
19+
20+
/**
21+
* Configures {@link WebClient} for tracing.
22+
*
23+
* <p>Adds OpenTelemetry instrumentation via WebClientCustomizer for Spring boot 4.
24+
*
25+
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
26+
* at any time.
27+
*/
28+
@ConditionalOnEnabledInstrumentation(module = "spring-webflux")
29+
@ConditionalOnClass({WebClient.class, WebClientCustomizer.class})
30+
@Configuration
31+
public class SpringWebClientInstrumentationSpringBoot4AutoConfiguration {
32+
33+
@Bean
34+
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
35+
WebClientCustomizer otelWebClientCustomizer(
36+
OpenTelemetry openTelemetry, InstrumentationConfig config) {
37+
SpringWebfluxClientTelemetry webfluxClientTelemetry =
38+
WebClientBeanPostProcessor.getWebfluxClientTelemetry(openTelemetry, config);
39+
return builder -> builder.filters(webfluxClientTelemetry::addFilter);
40+
}
41+
}

instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.r
1414
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web.SpringWebInstrumentationAutoConfiguration
1515
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web.SpringWebInstrumentationSpringBoot4AutoConfiguration
1616
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webflux.SpringWebfluxInstrumentationAutoConfiguration
17+
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webflux.SpringWebClientInstrumentationSpringBoot4AutoConfiguration
1718
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web.RestClientInstrumentationAutoConfiguration
1819
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web.RestClientInstrumentationSpringBoot4AutoConfiguration
1920
io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webmvc.SpringWebMvc6InstrumentationAutoConfiguration

instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebfluxInstrumentationAutoConfigurationTest.java

Lines changed: 31 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -7,87 +7,62 @@
77

88
import static org.assertj.core.api.Assertions.assertThat;
99

10-
import io.opentelemetry.api.OpenTelemetry;
11-
import io.opentelemetry.instrumentation.api.incubator.config.internal.InstrumentationConfig;
12-
import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.ConfigPropertiesBridge;
13-
import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
14-
import java.util.Collections;
15-
import java.util.concurrent.atomic.AtomicLong;
10+
import io.opentelemetry.instrumentation.spring.autoconfigure.internal.AbstractWebClientCustomizerAutoConfigurationTest;
1611
import org.junit.jupiter.api.Test;
1712
import org.springframework.boot.autoconfigure.AutoConfigurations;
18-
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
13+
import org.springframework.boot.test.context.FilteredClassLoader;
1914
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
2015
import org.springframework.web.reactive.function.client.WebClient;
2116

22-
class SpringWebfluxInstrumentationAutoConfigurationTest {
17+
class SpringWebfluxInstrumentationAutoConfigurationTest
18+
extends AbstractWebClientCustomizerAutoConfigurationTest {
2319

24-
private final ApplicationContextRunner contextRunner =
25-
new ApplicationContextRunner()
26-
.withBean(OpenTelemetry.class, OpenTelemetry::noop)
27-
.withBean(
28-
InstrumentationConfig.class,
29-
() ->
30-
new ConfigPropertiesBridge(
31-
DefaultConfigProperties.createFromMap(Collections.emptyMap())))
32-
.withConfiguration(
33-
AutoConfigurations.of(SpringWebfluxInstrumentationAutoConfiguration.class));
20+
@Override
21+
protected AutoConfigurations autoConfigurations() {
22+
return AutoConfigurations.of(SpringWebfluxInstrumentationAutoConfiguration.class);
23+
}
24+
25+
@Override
26+
protected Class<?> webClientCustomizerClass() {
27+
return WebClientCustomizer.class;
28+
}
29+
30+
@Override
31+
protected void customizeWebClient(Object customizer, WebClient.Builder builder) {
32+
((WebClientCustomizer) customizer).customize(builder);
33+
}
3434

3535
@Test
36-
void instrumentationEnabled() {
36+
void shouldCreateBeanPostProcessorWhenEnabled() {
3737
contextRunner
3838
.withPropertyValues("otel.instrumentation.spring-webflux.enabled=true")
3939
.run(
4040
context ->
4141
assertThat(context)
4242
.hasBean("otelWebClientBeanPostProcessor")
43-
.hasBean("otelWebClientCustomizer"));
43+
.hasBean("telemetryFilter"));
4444
}
4545

4646
@Test
47-
void instrumentationDisabled() {
47+
void shouldNotCreateBeanPostProcessorWhenDisabled() {
4848
contextRunner
4949
.withPropertyValues("otel.instrumentation.spring-webflux.enabled=false")
5050
.run(
5151
context ->
5252
assertThat(context)
5353
.doesNotHaveBean("otelWebClientBeanPostProcessor")
54-
.doesNotHaveBean("otelWebClientCustomizer"));
55-
}
56-
57-
@Test
58-
void defaultConfiguration() {
59-
contextRunner.run(
60-
context -> {
61-
assertThat(context)
62-
.hasBean("otelWebClientBeanPostProcessor")
63-
.hasBean("otelWebClientCustomizer");
64-
});
54+
.doesNotHaveBean("telemetryFilter"));
6555
}
6656

6757
@Test
68-
void shouldAddTracingFilterWhenCustomizerApplied() {
69-
contextRunner.run(
70-
context -> {
71-
WebClientCustomizer customizer =
72-
context.getBean("otelWebClientCustomizer", WebClientCustomizer.class);
73-
WebClient.Builder builder = WebClient.builder();
74-
customizer.customize(builder);
75-
76-
AtomicLong count = new AtomicLong(0);
77-
builder
78-
.build()
79-
.mutate()
80-
.filters(
81-
filters ->
82-
count.set(
83-
filters.stream()
84-
.filter(
85-
f ->
86-
f.getClass()
87-
.getName()
88-
.startsWith("io.opentelemetry.instrumentation"))
89-
.count()));
90-
assertThat(count.get()).isEqualTo(1);
91-
});
58+
void shouldCreateBeanPostProcessorWhenWebClientCustomizerNotOnClasspath() {
59+
contextRunner
60+
.withClassLoader(new FilteredClassLoader(WebClientCustomizer.class))
61+
.run(
62+
context ->
63+
assertThat(context)
64+
.hasBean("otelWebClientBeanPostProcessor")
65+
.hasBean("telemetryFilter")
66+
.doesNotHaveBean("otelWebClientCustomizer"));
9267
}
9368
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webflux;
7+
8+
import io.opentelemetry.instrumentation.spring.autoconfigure.internal.AbstractWebClientCustomizerAutoConfigurationTest;
9+
import org.springframework.boot.autoconfigure.AutoConfigurations;
10+
import org.springframework.boot.webclient.WebClientCustomizer;
11+
import org.springframework.web.reactive.function.client.WebClient;
12+
13+
class SpringWebClientInstrumentationSpringBoot4AutoConfigurationTest
14+
extends AbstractWebClientCustomizerAutoConfigurationTest {
15+
16+
@Override
17+
protected AutoConfigurations autoConfigurations() {
18+
return AutoConfigurations.of(SpringWebClientInstrumentationSpringBoot4AutoConfiguration.class);
19+
}
20+
21+
@Override
22+
protected Class<?> webClientCustomizerClass() {
23+
return WebClientCustomizer.class;
24+
}
25+
26+
@Override
27+
protected void customizeWebClient(Object customizer, WebClient.Builder builder) {
28+
((WebClientCustomizer) customizer).customize(builder);
29+
}
30+
}

instrumentation/spring/spring-boot-autoconfigure/testing/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ val springBootVersion = "2.7.18"
66

77
dependencies {
88
compileOnly("org.springframework.boot:spring-boot-restclient:4.0.0")
9+
compileOnly("org.springframework.boot:spring-boot-starter-webflux:$springBootVersion")
910
compileOnly("org.springframework.kafka:spring-kafka:2.9.0")
1011

1112
compileOnly("org.springframework.boot:spring-boot-starter-test:$springBootVersion")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.spring.autoconfigure.internal;
7+
8+
import static java.util.Collections.emptyMap;
9+
import static org.assertj.core.api.Assertions.assertThat;
10+
11+
import io.opentelemetry.api.OpenTelemetry;
12+
import io.opentelemetry.instrumentation.api.incubator.config.internal.InstrumentationConfig;
13+
import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.ConfigPropertiesBridge;
14+
import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
15+
import java.util.concurrent.atomic.AtomicLong;
16+
import org.junit.jupiter.api.Test;
17+
import org.springframework.boot.autoconfigure.AutoConfigurations;
18+
import org.springframework.boot.test.context.FilteredClassLoader;
19+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
20+
import org.springframework.web.reactive.function.client.WebClient;
21+
22+
/**
23+
* Abstract base test for WebClient customizer auto-configurations. Subclasses must provide the
24+
* auto-configuration class and WebClientCustomizer class for their Spring Boot version.
25+
*/
26+
public abstract class AbstractWebClientCustomizerAutoConfigurationTest {
27+
28+
protected abstract AutoConfigurations autoConfigurations();
29+
30+
protected abstract Class<?> webClientCustomizerClass();
31+
32+
protected abstract void customizeWebClient(Object customizer, WebClient.Builder builder);
33+
34+
protected final ApplicationContextRunner contextRunner =
35+
new ApplicationContextRunner()
36+
.withBean(OpenTelemetry.class, OpenTelemetry::noop)
37+
.withBean(
38+
InstrumentationConfig.class,
39+
() -> new ConfigPropertiesBridge(DefaultConfigProperties.createFromMap(emptyMap())))
40+
.withConfiguration(autoConfigurations());
41+
42+
@Test
43+
void shouldCreateCustomizerWhenEnabled() {
44+
contextRunner
45+
.withPropertyValues("otel.instrumentation.spring-webflux.enabled=true")
46+
.run(
47+
context ->
48+
assertThat(context.getBean("otelWebClientCustomizer"))
49+
.isNotNull()
50+
.isInstanceOf(webClientCustomizerClass()));
51+
}
52+
53+
@Test
54+
void shouldNotCreateCustomizerWhenDisabled() {
55+
contextRunner
56+
.withPropertyValues("otel.instrumentation.spring-webflux.enabled=false")
57+
.run(context -> assertThat(context).doesNotHaveBean("otelWebClientCustomizer"));
58+
}
59+
60+
@Test
61+
void shouldCreateCustomizerByDefault() {
62+
contextRunner.run(
63+
context ->
64+
assertThat(context.getBean("otelWebClientCustomizer"))
65+
.isNotNull()
66+
.isInstanceOf(webClientCustomizerClass()));
67+
}
68+
69+
@Test
70+
void shouldAddTracingFilterWhenCustomizerApplied() {
71+
contextRunner.run(
72+
context -> {
73+
Object customizer =
74+
context.getBean("otelWebClientCustomizer", webClientCustomizerClass());
75+
WebClient.Builder builder = WebClient.builder();
76+
customizeWebClient(customizer, builder);
77+
78+
AtomicLong count = new AtomicLong(0);
79+
builder
80+
.build()
81+
.mutate()
82+
.filters(
83+
filters ->
84+
count.set(
85+
filters.stream()
86+
.filter(
87+
f ->
88+
f.getClass()
89+
.getName()
90+
.startsWith("io.opentelemetry.instrumentation"))
91+
.count()));
92+
assertThat(count.get()).isEqualTo(1);
93+
});
94+
}
95+
96+
@Test
97+
void shouldNotCreateCustomizerWhenWebClientCustomizerNotOnClasspath() {
98+
contextRunner
99+
.withClassLoader(new FilteredClassLoader(webClientCustomizerClass()))
100+
.run(context -> assertThat(context).doesNotHaveBean("otelWebClientCustomizer"));
101+
}
102+
103+
@Test
104+
void shouldNotCreateCustomizerWhenWebClientNotOnClasspath() {
105+
contextRunner
106+
.withClassLoader(new FilteredClassLoader(WebClient.class))
107+
.run(context -> assertThat(context).doesNotHaveBean("otelWebClientCustomizer"));
108+
}
109+
}

0 commit comments

Comments
 (0)