Skip to content

Commit c7b9736

Browse files
Add fix for WebClient
Signed-off-by: Lenin Jaganathan <[email protected]>
1 parent 07aedf9 commit c7b9736

File tree

8 files changed

+130
-85
lines changed

8 files changed

+130
-85
lines changed

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,14 @@
88
import io.opentelemetry.api.OpenTelemetry;
99
import io.opentelemetry.instrumentation.api.incubator.config.internal.InstrumentationConfig;
1010
import io.opentelemetry.instrumentation.spring.autoconfigure.internal.ConditionalOnEnabledInstrumentation;
11+
import io.opentelemetry.instrumentation.spring.webflux.v5_3.SpringWebfluxClientTelemetry;
1112
import org.springframework.beans.factory.ObjectProvider;
1213
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
14+
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
1315
import org.springframework.context.annotation.Bean;
1416
import org.springframework.context.annotation.Configuration;
17+
import org.springframework.core.Ordered;
18+
import org.springframework.core.annotation.Order;
1519
import org.springframework.web.reactive.function.client.WebClient;
1620
import org.springframework.web.server.WebFilter;
1721

@@ -24,7 +28,7 @@
2428
* at any time.
2529
*/
2630
@ConditionalOnEnabledInstrumentation(module = "spring-webflux")
27-
@ConditionalOnClass(WebClient.class)
31+
@ConditionalOnClass({WebClient.class, WebClientCustomizer.class})
2832
@Configuration
2933
public class SpringWebfluxInstrumentationAutoConfiguration {
3034

@@ -38,6 +42,15 @@ static WebClientBeanPostProcessor otelWebClientBeanPostProcessor(
3842
return new WebClientBeanPostProcessor(openTelemetryProvider, configProvider);
3943
}
4044

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+
4154
@Bean
4255
WebFilter telemetryFilter(OpenTelemetry openTelemetry, InstrumentationConfig config) {
4356
return WebClientBeanPostProcessor.getWebfluxServerTelemetry(openTelemetry, config)

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

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@
1111
import io.opentelemetry.instrumentation.spring.webflux.v5_3.SpringWebfluxClientTelemetry;
1212
import io.opentelemetry.instrumentation.spring.webflux.v5_3.SpringWebfluxServerTelemetry;
1313
import io.opentelemetry.instrumentation.spring.webflux.v5_3.internal.SpringWebfluxBuilderUtil;
14+
import io.opentelemetry.instrumentation.spring.webflux.v5_3.internal.WebClientTracingFilter;
15+
import java.util.List;
16+
import java.util.concurrent.atomic.AtomicBoolean;
1417
import org.springframework.beans.factory.ObjectProvider;
1518
import org.springframework.beans.factory.config.BeanPostProcessor;
19+
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
1620
import org.springframework.web.reactive.function.client.WebClient;
1721

1822
/**
@@ -53,18 +57,30 @@ static SpringWebfluxServerTelemetry getWebfluxServerTelemetry(
5357
@Override
5458
public Object postProcessAfterInitialization(Object bean, String beanName) {
5559
if (bean instanceof WebClient) {
56-
WebClient webClient = (WebClient) bean;
57-
return wrapBuilder(webClient.mutate()).build();
58-
} else if (bean instanceof WebClient.Builder) {
59-
WebClient.Builder webClientBuilder = (WebClient.Builder) bean;
60-
return wrapBuilder(webClientBuilder);
60+
return addWebClientFilterIfNotPresent(
61+
(WebClient) bean, openTelemetryProvider.getObject(), configProvider.getObject());
6162
}
6263
return bean;
6364
}
6465

65-
private WebClient.Builder wrapBuilder(WebClient.Builder webClientBuilder) {
66-
SpringWebfluxClientTelemetry instrumentation =
67-
getWebfluxClientTelemetry(openTelemetryProvider.getObject(), configProvider.getObject());
68-
return webClientBuilder.filters(instrumentation::addFilter);
66+
private static WebClient addWebClientFilterIfNotPresent(
67+
WebClient webClient, OpenTelemetry openTelemetry, InstrumentationConfig config) {
68+
AtomicBoolean filterAdded = new AtomicBoolean(false);
69+
WebClient.Builder builder =
70+
webClient
71+
.mutate()
72+
.filters(
73+
filters -> {
74+
if (isFilterNotPresent(filters)) {
75+
getWebfluxClientTelemetry(openTelemetry, config).addFilter(filters);
76+
filterAdded.set(true);
77+
}
78+
});
79+
80+
return filterAdded.get() ? builder.build() : webClient;
81+
}
82+
83+
private static boolean isFilterNotPresent(List<ExchangeFilterFunction> filters) {
84+
return filters.stream().noneMatch(WebClientTracingFilter.class::isInstance);
6985
}
7086
}

instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientBeanPostProcessor.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.InstrumentationConfigUtil;
1111
import io.opentelemetry.instrumentation.spring.web.v3_1.SpringWebTelemetry;
1212
import io.opentelemetry.instrumentation.spring.web.v3_1.internal.WebTelemetryUtil;
13+
import java.util.List;
1314
import java.util.concurrent.atomic.AtomicBoolean;
1415
import org.springframework.beans.factory.ObjectProvider;
1516
import org.springframework.beans.factory.config.BeanPostProcessor;
@@ -57,7 +58,7 @@ private static RestClient addRestClientInterceptorIfNotPresent(
5758
}
5859

5960
private static boolean isInterceptorNotPresent(
60-
java.util.List<ClientHttpRequestInterceptor> interceptors,
61+
List<ClientHttpRequestInterceptor> interceptors,
6162
ClientHttpRequestInterceptor instrumentationInterceptor) {
6263
return interceptors.stream()
6364
.noneMatch(interceptor -> interceptor.getClass() == instrumentationInterceptor.getClass());

instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationAutoConfiguration.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import org.springframework.boot.web.client.RestClientCustomizer;
1616
import org.springframework.context.annotation.Bean;
1717
import org.springframework.context.annotation.Configuration;
18+
import org.springframework.core.Ordered;
19+
import org.springframework.core.annotation.Order;
1820
import org.springframework.web.client.RestClient;
1921

2022
/**
@@ -39,6 +41,7 @@ static RestClientBeanPostProcessor otelRestClientBeanPostProcessor(
3941
}
4042

4143
@Bean
44+
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
4245
RestClientCustomizer otelRestClientCustomizer(
4346
ObjectProvider<OpenTelemetry> openTelemetryProvider,
4447
ObjectProvider<InstrumentationConfig> configProvider) {

instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring4/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationSpringBoot4AutoConfiguration.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration;
1616
import org.springframework.context.annotation.Bean;
1717
import org.springframework.context.annotation.Configuration;
18+
import org.springframework.core.Ordered;
19+
import org.springframework.core.annotation.Order;
1820
import org.springframework.web.client.RestClient;
1921

2022
/**
@@ -39,6 +41,7 @@ static RestClientBeanPostProcessorSpring4 otelRestClientBeanPostProcessor(
3941
}
4042

4143
@Bean
44+
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
4245
RestClientCustomizer otelRestClientCustomizer(
4346
ObjectProvider<OpenTelemetry> openTelemetryProvider,
4447
ObjectProvider<InstrumentationConfig> configProvider) {

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

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@
1212
import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.ConfigPropertiesBridge;
1313
import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
1414
import java.util.Collections;
15+
import java.util.concurrent.atomic.AtomicLong;
1516
import org.junit.jupiter.api.Test;
1617
import org.springframework.boot.autoconfigure.AutoConfigurations;
1718
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
19+
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
20+
import org.springframework.web.reactive.function.client.WebClient;
1821

1922
class SpringWebfluxInstrumentationAutoConfigurationTest {
2023

@@ -35,10 +38,9 @@ void instrumentationEnabled() {
3538
.withPropertyValues("otel.instrumentation.spring-webflux.enabled=true")
3639
.run(
3740
context ->
38-
assertThat(
39-
context.getBean(
40-
"otelWebClientBeanPostProcessor", WebClientBeanPostProcessor.class))
41-
.isNotNull());
41+
assertThat(context)
42+
.hasBean("otelWebClientBeanPostProcessor")
43+
.hasBean("otelWebClientCustomizer"));
4244
}
4345

4446
@Test
@@ -47,16 +49,45 @@ void instrumentationDisabled() {
4749
.withPropertyValues("otel.instrumentation.spring-webflux.enabled=false")
4850
.run(
4951
context ->
50-
assertThat(context.containsBean("otelWebClientBeanPostProcessor")).isFalse());
52+
assertThat(context)
53+
.doesNotHaveBean("otelWebClientBeanPostProcessor")
54+
.doesNotHaveBean("otelWebClientCustomizer"));
5155
}
5256

5357
@Test
5458
void defaultConfiguration() {
5559
contextRunner.run(
56-
context ->
57-
assertThat(
58-
context.getBean(
59-
"otelWebClientBeanPostProcessor", WebClientBeanPostProcessor.class))
60-
.isNotNull());
60+
context -> {
61+
assertThat(context)
62+
.hasBean("otelWebClientBeanPostProcessor")
63+
.hasBean("otelWebClientCustomizer");
64+
});
65+
}
66+
67+
@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+
});
6192
}
6293
}

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

Lines changed: 36 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.ConfigPropertiesBridge;
1313
import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
1414
import java.util.Collections;
15+
import java.util.concurrent.atomic.AtomicLong;
16+
import org.junit.jupiter.api.BeforeEach;
1517
import org.junit.jupiter.api.DisplayName;
1618
import org.junit.jupiter.api.Test;
1719
import org.springframework.beans.factory.config.BeanPostProcessor;
@@ -29,92 +31,62 @@ class WebClientBeanPostProcessorTest {
2931
new ConfigPropertiesBridge(DefaultConfigProperties.createFromMap(Collections.emptyMap())));
3032
}
3133

32-
@Test
33-
@DisplayName(
34-
"when processed bean is NOT of type WebClient or WebClientBuilder should return Object")
35-
void returnsObject() {
36-
BeanPostProcessor underTest =
37-
new WebClientBeanPostProcessor(
38-
beanFactory.getBeanProvider(OpenTelemetry.class),
39-
beanFactory.getBeanProvider(InstrumentationConfig.class));
40-
41-
assertThat(underTest.postProcessAfterInitialization(new Object(), "testObject"))
42-
.isExactlyInstanceOf(Object.class);
43-
}
34+
private BeanPostProcessor underTest;
4435

45-
@Test
46-
@DisplayName("when processed bean is of type WebClient should return WebClient")
47-
void returnsWebClient() {
48-
BeanPostProcessor underTest =
36+
@BeforeEach
37+
void setUp() {
38+
underTest =
4939
new WebClientBeanPostProcessor(
5040
beanFactory.getBeanProvider(OpenTelemetry.class),
5141
beanFactory.getBeanProvider(InstrumentationConfig.class));
52-
53-
assertThat(underTest.postProcessAfterInitialization(WebClient.create(), "testWebClient"))
54-
.isInstanceOf(WebClient.class);
5542
}
5643

5744
@Test
58-
@DisplayName("when processed bean is of type WebClientBuilder should return WebClientBuilder")
59-
void returnsWebClientBuilder() {
60-
BeanPostProcessor underTest =
61-
new WebClientBeanPostProcessor(
62-
beanFactory.getBeanProvider(OpenTelemetry.class),
63-
beanFactory.getBeanProvider(InstrumentationConfig.class));
45+
@DisplayName("when processed bean is NOT of type WebClient should return same Object")
46+
void returnsObject() {
47+
Object original = new Object();
6448

65-
assertThat(
66-
underTest.postProcessAfterInitialization(WebClient.builder(), "testWebClientBuilder"))
67-
.isInstanceOf(WebClient.Builder.class);
49+
assertThat(underTest.postProcessAfterInitialization(original, "testObject")).isSameAs(original);
6850
}
6951

7052
@Test
71-
@DisplayName("when processed bean is of type WebClient should add exchange filter to WebClient")
72-
void addsExchangeFilterWebClient() {
73-
BeanPostProcessor underTest =
74-
new WebClientBeanPostProcessor(
75-
beanFactory.getBeanProvider(OpenTelemetry.class),
76-
beanFactory.getBeanProvider(InstrumentationConfig.class));
77-
53+
@DisplayName("when processed bean is of type WebClient should return WebClient with filter")
54+
void returnsWebClientWithFilter() {
7855
WebClient webClient = WebClient.create();
7956
Object processedWebClient =
8057
underTest.postProcessAfterInitialization(webClient, "testWebClient");
8158

82-
assertThat(processedWebClient).isInstanceOf(WebClient.class);
83-
((WebClient) processedWebClient)
84-
.mutate()
85-
.filters(
86-
functions ->
87-
assertThat(
88-
functions.stream()
89-
.filter(WebClientBeanPostProcessorTest::isOtelExchangeFilter)
90-
.count())
91-
.isEqualTo(1));
59+
assertThat(processedWebClient).isInstanceOf(WebClient.class).isNotSameAs(webClient);
60+
assertFilterCount((WebClient) processedWebClient, 1);
9261
}
9362

9463
@Test
95-
@DisplayName(
96-
"when processed bean is of type WebClientBuilder should add ONE exchange filter to WebClientBuilder")
97-
void addsExchangeFilterWebClientBuilder() {
98-
BeanPostProcessor underTest =
99-
new WebClientBeanPostProcessor(
100-
beanFactory.getBeanProvider(OpenTelemetry.class),
101-
beanFactory.getBeanProvider(InstrumentationConfig.class));
64+
@DisplayName("when WebClient already has filter should return same instance")
65+
void doesNotAddDuplicateFilter() {
66+
WebClient webClient = WebClient.create();
67+
WebClient firstProcessed =
68+
(WebClient) underTest.postProcessAfterInitialization(webClient, "testWebClient");
69+
WebClient secondProcessed =
70+
(WebClient) underTest.postProcessAfterInitialization(firstProcessed, "testWebClient");
10271

103-
WebClient.Builder webClientBuilder = WebClient.builder();
104-
underTest.postProcessAfterInitialization(webClientBuilder, "testWebClientBuilder");
105-
underTest.postProcessAfterInitialization(webClientBuilder, "testWebClientBuilder");
106-
underTest.postProcessAfterInitialization(webClientBuilder, "testWebClientBuilder");
72+
assertThat(secondProcessed).isSameAs(firstProcessed);
73+
assertFilterCount(secondProcessed, 1);
74+
}
10775

108-
webClientBuilder.filters(
109-
functions ->
110-
assertThat(
111-
functions.stream()
76+
private static void assertFilterCount(WebClient webClient, long expectedCount) {
77+
AtomicLong count = new AtomicLong(0);
78+
webClient
79+
.mutate()
80+
.filters(
81+
filters ->
82+
count.set(
83+
filters.stream()
11284
.filter(WebClientBeanPostProcessorTest::isOtelExchangeFilter)
113-
.count())
114-
.isEqualTo(1));
85+
.count()));
86+
assertThat(count.get()).isEqualTo(expectedCount);
11587
}
11688

117-
private static boolean isOtelExchangeFilter(ExchangeFilterFunction wctf) {
118-
return wctf.getClass().getName().startsWith("io.opentelemetry.instrumentation");
89+
private static boolean isOtelExchangeFilter(ExchangeFilterFunction filter) {
90+
return filter.getClass().getName().startsWith("io.opentelemetry.instrumentation");
11991
}
12092
}

instrumentation/spring/spring-boot-autoconfigure/src/testSpring3/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationAutoConfigurationTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@
55

66
package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web;
77

8+
import static org.assertj.core.api.Assertions.assertThat;
9+
10+
import io.opentelemetry.api.OpenTelemetry;
11+
import io.opentelemetry.instrumentation.api.incubator.config.internal.InstrumentationConfig;
812
import io.opentelemetry.instrumentation.spring.autoconfigure.internal.AbstractRestClientInstrumentationAutoConfigurationTest;
13+
import org.junit.jupiter.api.Test;
914
import org.springframework.boot.autoconfigure.AutoConfigurations;
15+
import org.springframework.web.client.RestClient;
1016

1117
class RestClientInstrumentationAutoConfigurationTest
1218
extends AbstractRestClientInstrumentationAutoConfigurationTest {

0 commit comments

Comments
 (0)