diff --git a/instrumentation/spring/spring-boot-autoconfigure/build.gradle.kts b/instrumentation/spring/spring-boot-autoconfigure/build.gradle.kts index f4dbe222bfe2..aab210419c80 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/build.gradle.kts +++ b/instrumentation/spring/spring-boot-autoconfigure/build.gradle.kts @@ -141,6 +141,7 @@ dependencies { add("javaSpring4CompileOnly", "org.springframework.boot:spring-boot-jdbc:4.0.0") add("javaSpring4CompileOnly", "org.springframework.boot:spring-boot-starter-jdbc:4.0.0") add("javaSpring4CompileOnly", "org.springframework.boot:spring-boot-restclient:4.0.0") + add("javaSpring4CompileOnly", "org.springframework.boot:spring-boot-webclient:4.0.0") add("javaSpring4CompileOnly", "org.springframework.boot:spring-boot-starter-data-mongodb:4.0.0") add( "javaSpring4CompileOnly", @@ -157,6 +158,7 @@ dependencies { "javaSpring4CompileOnly", project(":instrumentation:spring:spring-web:spring-web-3.1:library") ) + add("javaSpring4CompileOnly", project(":instrumentation:spring:spring-webflux:spring-webflux-5.3:library")) } val latestDepTest = findProperty("testLatestDeps") as Boolean @@ -226,6 +228,7 @@ testing { implementation("org.springframework.boot:spring-boot-starter-test:$version") implementation("org.springframework.boot:spring-boot-starter-actuator:$version") implementation("org.springframework.boot:spring-boot-starter-web:$version") + implementation("org.springframework.boot:spring-boot-starter-webflux:$version") implementation("org.springframework.boot:spring-boot-starter-jdbc:$version") implementation("org.springframework.boot:spring-boot-starter-data-r2dbc:$version") val springKafkaVersion = if (latestDepTest) "3.+" else "2.9.0" @@ -257,6 +260,7 @@ testing { val version = if (latestDepTest) "latest.release" else "4.0.0" implementation("org.springframework.boot:spring-boot-starter-jdbc:$version") implementation("org.springframework.boot:spring-boot-restclient:$version") + implementation("org.springframework.boot:spring-boot-webclient:$version") implementation("org.springframework.boot:spring-boot-starter-kafka:$version") implementation("org.springframework.boot:spring-boot-starter-actuator:$version") implementation("org.springframework.boot:spring-boot-starter-data-r2dbc:$version") diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebfluxInstrumentationAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebfluxInstrumentationAutoConfiguration.java index 93e9a1fde417..87e6ac7d5076 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebfluxInstrumentationAutoConfiguration.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebfluxInstrumentationAutoConfiguration.java @@ -7,10 +7,14 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.spring.autoconfigure.internal.ConditionalOnEnabledInstrumentation; +import io.opentelemetry.instrumentation.spring.webflux.v5_3.SpringWebfluxClientTelemetry; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.web.reactive.function.client.WebClientCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.server.WebFilter; @@ -41,4 +45,17 @@ WebFilter telemetryFilter(OpenTelemetry openTelemetry) { return WebClientBeanPostProcessor.getWebfluxServerTelemetry(openTelemetry) .createWebFilterAndRegisterReactorHook(); } + + @Configuration + @ConditionalOnClass(WebClientCustomizer.class) + static class OpentelemetryWebClientCustomizerConfiguration { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE + 10) + WebClientCustomizer otelWebClientCustomizer(OpenTelemetry openTelemetry) { + SpringWebfluxClientTelemetry webfluxClientTelemetry = + WebClientBeanPostProcessor.getWebfluxClientTelemetry(openTelemetry); + return builder -> builder.filters(webfluxClientTelemetry::addFilter); + } + } } diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/WebClientBeanPostProcessor.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/WebClientBeanPostProcessor.java index 355bf8ea2450..8ef7a077cafe 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/WebClientBeanPostProcessor.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/WebClientBeanPostProcessor.java @@ -10,8 +10,12 @@ import io.opentelemetry.instrumentation.spring.webflux.v5_3.SpringWebfluxClientTelemetry; import io.opentelemetry.instrumentation.spring.webflux.v5_3.SpringWebfluxServerTelemetry; import io.opentelemetry.instrumentation.spring.webflux.v5_3.internal.SpringWebfluxBuilderUtil; +import io.opentelemetry.instrumentation.spring.webflux.v5_3.internal.WebClientTracingFilter; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.WebClient; /** @@ -46,18 +50,29 @@ static SpringWebfluxServerTelemetry getWebfluxServerTelemetry(OpenTelemetry open @Override public Object postProcessAfterInitialization(Object bean, String beanName) { if (bean instanceof WebClient) { - WebClient webClient = (WebClient) bean; - return wrapBuilder(webClient.mutate()).build(); - } else if (bean instanceof WebClient.Builder) { - WebClient.Builder webClientBuilder = (WebClient.Builder) bean; - return wrapBuilder(webClientBuilder); + return addWebClientFilterIfNotPresent((WebClient) bean, openTelemetryProvider.getObject()); } return bean; } - private WebClient.Builder wrapBuilder(WebClient.Builder webClientBuilder) { - SpringWebfluxClientTelemetry instrumentation = - getWebfluxClientTelemetry(openTelemetryProvider.getObject()); - return webClientBuilder.filters(instrumentation::addFilter); + private static WebClient addWebClientFilterIfNotPresent( + WebClient webClient, OpenTelemetry openTelemetry) { + AtomicBoolean filterAdded = new AtomicBoolean(false); + WebClient.Builder builder = + webClient + .mutate() + .filters( + filters -> { + if (isFilterNotPresent(filters)) { + getWebfluxClientTelemetry(openTelemetry).addFilter(filters); + filterAdded.set(true); + } + }); + + return filterAdded.get() ? builder.build() : webClient; + } + + private static boolean isFilterNotPresent(List filters) { + return filters.stream().noneMatch(WebClientTracingFilter.class::isInstance); } } diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientBeanPostProcessor.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientBeanPostProcessor.java index b229273b9c1c..cd2728d34ad8 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientBeanPostProcessor.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientBeanPostProcessor.java @@ -9,6 +9,8 @@ import io.opentelemetry.instrumentation.spring.autoconfigure.internal.properties.InstrumentationConfigUtil; import io.opentelemetry.instrumentation.spring.web.v3_1.SpringWebTelemetry; import io.opentelemetry.instrumentation.spring.web.v3_1.internal.WebTelemetryUtil; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.http.client.ClientHttpRequestInterceptor; @@ -34,18 +36,26 @@ private static RestClient addRestClientInterceptorIfNotPresent( RestClient restClient, OpenTelemetry openTelemetry) { ClientHttpRequestInterceptor instrumentationInterceptor = getInterceptor(openTelemetry); - return restClient - .mutate() - .requestInterceptors( - interceptors -> { - if (interceptors.stream() - .noneMatch( - interceptor -> - interceptor.getClass() == instrumentationInterceptor.getClass())) { - interceptors.add(0, instrumentationInterceptor); - } - }) - .build(); + AtomicBoolean interceptorAdded = new AtomicBoolean(false); + RestClient.Builder result = + restClient + .mutate() + .requestInterceptors( + interceptors -> { + if (isInterceptorNotPresent(interceptors, instrumentationInterceptor)) { + interceptors.add(0, instrumentationInterceptor); + interceptorAdded.set(true); + } + }); + + return interceptorAdded.get() ? result.build() : restClient; + } + + private static boolean isInterceptorNotPresent( + List interceptors, + ClientHttpRequestInterceptor instrumentationInterceptor) { + return interceptors.stream() + .noneMatch(interceptor -> interceptor.getClass() == instrumentationInterceptor.getClass()); } static ClientHttpRequestInterceptor getInterceptor(OpenTelemetry openTelemetry) { diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationAutoConfiguration.java index 3501b65e1b67..2dd796004871 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationAutoConfiguration.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring3/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationAutoConfiguration.java @@ -14,6 +14,8 @@ import org.springframework.boot.web.client.RestClientCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.web.client.RestClient; /** @@ -37,6 +39,7 @@ static RestClientBeanPostProcessor otelRestClientBeanPostProcessor( } @Bean + @Order(Ordered.HIGHEST_PRECEDENCE + 10) RestClientCustomizer otelRestClientCustomizer( ObjectProvider openTelemetryProvider) { return builder -> diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring4/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationSpringBoot4AutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring4/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationSpringBoot4AutoConfiguration.java index c1415e3ab8ff..50e12ca9a2ad 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring4/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationSpringBoot4AutoConfiguration.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring4/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationSpringBoot4AutoConfiguration.java @@ -14,6 +14,8 @@ import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.web.client.RestClient; /** @@ -37,6 +39,7 @@ static RestClientBeanPostProcessorSpring4 otelRestClientBeanPostProcessor( } @Bean + @Order(Ordered.HIGHEST_PRECEDENCE + 10) RestClientCustomizer otelRestClientCustomizer( ObjectProvider openTelemetryProvider) { return builder -> diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring4/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebClientInstrumentationSpringBoot4AutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring4/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebClientInstrumentationSpringBoot4AutoConfiguration.java new file mode 100644 index 000000000000..1572be708059 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/javaSpring4/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebClientInstrumentationSpringBoot4AutoConfiguration.java @@ -0,0 +1,39 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webflux; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.ConditionalOnEnabledInstrumentation; +import io.opentelemetry.instrumentation.spring.webflux.v5_3.SpringWebfluxClientTelemetry; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.webclient.WebClientCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * Configures {@link WebClient} for tracing. + * + *

Adds OpenTelemetry instrumentation via WebClientCustomizer for Spring boot 4. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + */ +@ConditionalOnEnabledInstrumentation(module = "spring-webflux") +@ConditionalOnClass({WebClient.class, WebClientCustomizer.class}) +@Configuration +public class SpringWebClientInstrumentationSpringBoot4AutoConfiguration { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE + 10) + WebClientCustomizer otelWebClientCustomizer(OpenTelemetry openTelemetry) { + SpringWebfluxClientTelemetry webfluxClientTelemetry = + WebClientBeanPostProcessor.getWebfluxClientTelemetry(openTelemetry); + return builder -> builder.filters(webfluxClientTelemetry::addFilter); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 8b91e0c90de3..3197e9ded35c 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -14,6 +14,7 @@ io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.r io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web.SpringWebInstrumentationAutoConfiguration io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web.SpringWebInstrumentationSpringBoot4AutoConfiguration io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webflux.SpringWebfluxInstrumentationAutoConfiguration +io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webflux.SpringWebClientInstrumentationSpringBoot4AutoConfiguration io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web.RestClientInstrumentationAutoConfiguration io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web.RestClientInstrumentationSpringBoot4AutoConfiguration io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webmvc.SpringWebMvc6InstrumentationAutoConfiguration diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebfluxInstrumentationAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebfluxInstrumentationAutoConfigurationTest.java deleted file mode 100644 index 6eb3dbd29fe7..000000000000 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebfluxInstrumentationAutoConfigurationTest.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webflux; - -import static org.assertj.core.api.Assertions.assertThat; - -import io.opentelemetry.api.OpenTelemetry; -import org.junit.jupiter.api.Test; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; - -class SpringWebfluxInstrumentationAutoConfigurationTest { - - private final ApplicationContextRunner contextRunner = - new ApplicationContextRunner() - .withBean(OpenTelemetry.class, OpenTelemetry::noop) - .withConfiguration( - AutoConfigurations.of(SpringWebfluxInstrumentationAutoConfiguration.class)); - - @Test - void instrumentationEnabled() { - contextRunner - .withPropertyValues("otel.instrumentation.spring-webflux.enabled=true") - .run( - context -> - assertThat( - context.getBean( - "otelWebClientBeanPostProcessor", WebClientBeanPostProcessor.class)) - .isNotNull()); - } - - @Test - void instrumentationDisabled() { - contextRunner - .withPropertyValues("otel.instrumentation.spring-webflux.enabled=false") - .run( - context -> - assertThat(context.containsBean("otelWebClientBeanPostProcessor")).isFalse()); - } - - @Test - void defaultConfiguration() { - contextRunner.run( - context -> - assertThat( - context.getBean( - "otelWebClientBeanPostProcessor", WebClientBeanPostProcessor.class)) - .isNotNull()); - } -} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/WebClientBeanPostProcessorTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/WebClientBeanPostProcessorTest.java index 3152bb8e1717..796d8b3a7f1d 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/WebClientBeanPostProcessorTest.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/test/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/WebClientBeanPostProcessorTest.java @@ -8,6 +8,8 @@ import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.api.OpenTelemetry; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.config.BeanPostProcessor; @@ -22,82 +24,59 @@ class WebClientBeanPostProcessorTest { beanFactory.registerSingleton("openTelemetry", OpenTelemetry.noop()); } - @Test - @DisplayName( - "when processed bean is NOT of type WebClient or WebClientBuilder should return Object") - void returnsObject() { - BeanPostProcessor underTest = - new WebClientBeanPostProcessor(beanFactory.getBeanProvider(OpenTelemetry.class)); + private BeanPostProcessor underTest; - assertThat(underTest.postProcessAfterInitialization(new Object(), "testObject")) - .isExactlyInstanceOf(Object.class); + @BeforeEach + void setUp() { + underTest = new WebClientBeanPostProcessor(beanFactory.getBeanProvider(OpenTelemetry.class)); } @Test - @DisplayName("when processed bean is of type WebClient should return WebClient") - void returnsWebClient() { - BeanPostProcessor underTest = - new WebClientBeanPostProcessor(beanFactory.getBeanProvider(OpenTelemetry.class)); - - assertThat(underTest.postProcessAfterInitialization(WebClient.create(), "testWebClient")) - .isInstanceOf(WebClient.class); - } - - @Test - @DisplayName("when processed bean is of type WebClientBuilder should return WebClientBuilder") - void returnsWebClientBuilder() { - BeanPostProcessor underTest = - new WebClientBeanPostProcessor(beanFactory.getBeanProvider(OpenTelemetry.class)); + @DisplayName("when processed bean is NOT of type WebClient should return same Object") + void returnsObject() { + Object original = new Object(); - assertThat( - underTest.postProcessAfterInitialization(WebClient.builder(), "testWebClientBuilder")) - .isInstanceOf(WebClient.Builder.class); + assertThat(underTest.postProcessAfterInitialization(original, "testObject")).isSameAs(original); } @Test - @DisplayName("when processed bean is of type WebClient should add exchange filter to WebClient") - void addsExchangeFilterWebClient() { - BeanPostProcessor underTest = - new WebClientBeanPostProcessor(beanFactory.getBeanProvider(OpenTelemetry.class)); - + @DisplayName("when processed bean is of type WebClient should return WebClient with filter") + void returnsWebClientWithFilter() { WebClient webClient = WebClient.create(); Object processedWebClient = underTest.postProcessAfterInitialization(webClient, "testWebClient"); - assertThat(processedWebClient).isInstanceOf(WebClient.class); - ((WebClient) processedWebClient) - .mutate() - .filters( - functions -> - assertThat( - functions.stream() - .filter(WebClientBeanPostProcessorTest::isOtelExchangeFilter) - .count()) - .isEqualTo(1)); + assertThat(processedWebClient).isInstanceOf(WebClient.class).isNotSameAs(webClient); + assertFilterCount((WebClient) processedWebClient, 1); } @Test - @DisplayName( - "when processed bean is of type WebClientBuilder should add ONE exchange filter to WebClientBuilder") - void addsExchangeFilterWebClientBuilder() { - BeanPostProcessor underTest = - new WebClientBeanPostProcessor(beanFactory.getBeanProvider(OpenTelemetry.class)); + @DisplayName("when WebClient already has filter should return same instance") + void doesNotAddDuplicateFilter() { + WebClient webClient = WebClient.create(); + WebClient firstProcessed = + (WebClient) underTest.postProcessAfterInitialization(webClient, "testWebClient"); + WebClient secondProcessed = + (WebClient) underTest.postProcessAfterInitialization(firstProcessed, "testWebClient"); - WebClient.Builder webClientBuilder = WebClient.builder(); - underTest.postProcessAfterInitialization(webClientBuilder, "testWebClientBuilder"); - underTest.postProcessAfterInitialization(webClientBuilder, "testWebClientBuilder"); - underTest.postProcessAfterInitialization(webClientBuilder, "testWebClientBuilder"); + assertThat(secondProcessed).isSameAs(firstProcessed); + assertFilterCount(secondProcessed, 1); + } - webClientBuilder.filters( - functions -> - assertThat( - functions.stream() + private static void assertFilterCount(WebClient webClient, long expectedCount) { + AtomicLong count = new AtomicLong(0); + webClient + .mutate() + .filters( + filters -> + count.set( + filters.stream() .filter(WebClientBeanPostProcessorTest::isOtelExchangeFilter) - .count()) - .isEqualTo(1)); + .count())); + assertThat(count.get()).isEqualTo(expectedCount); } - private static boolean isOtelExchangeFilter(ExchangeFilterFunction wctf) { - return wctf.getClass().getName().startsWith("io.opentelemetry.instrumentation"); + private static boolean isOtelExchangeFilter(ExchangeFilterFunction filter) { + return filter.getClass().getName().startsWith("io.opentelemetry.instrumentation"); } } diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/testSpring2/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebfluxInstrumentationAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/testSpring2/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebfluxInstrumentationAutoConfigurationTest.java new file mode 100644 index 000000000000..10f7964814e7 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/testSpring2/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebfluxInstrumentationAutoConfigurationTest.java @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webflux; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.AbstractWebClientCustomizerAutoConfigurationTest; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.web.reactive.function.client.WebClientCustomizer; +import org.springframework.web.reactive.function.client.WebClient; + +class SpringWebfluxInstrumentationAutoConfigurationTest + extends AbstractWebClientCustomizerAutoConfigurationTest { + + @Override + protected AutoConfigurations autoConfigurations() { + return AutoConfigurations.of(SpringWebfluxInstrumentationAutoConfiguration.class); + } + + @Override + protected Class webClientCustomizerClass() { + return WebClientCustomizer.class; + } + + @Override + protected void customizeWebClient(WebClientCustomizer customizer, WebClient.Builder builder) { + customizer.customize(builder); + } + + @Test + void shouldCreateBeanPostProcessorWhenEnabled() { + contextRunner + .withPropertyValues("otel.instrumentation.spring-webflux.enabled=true") + .run( + context -> + assertThat(context) + .hasBean("otelWebClientBeanPostProcessor") + .hasBean("telemetryFilter")); + } + + @Test + void shouldNotCreateBeanPostProcessorWhenDisabled() { + contextRunner + .withPropertyValues("otel.instrumentation.spring-webflux.enabled=false") + .run( + context -> + assertThat(context) + .doesNotHaveBean("otelWebClientBeanPostProcessor") + .doesNotHaveBean("telemetryFilter")); + } + + @Test + void shouldCreateBeanPostProcessorWhenWebClientCustomizerNotOnClasspath() { + contextRunner + .withClassLoader(new FilteredClassLoader(WebClientCustomizer.class)) + .run( + context -> + assertThat(context) + .hasBean("otelWebClientBeanPostProcessor") + .hasBean("telemetryFilter") + .doesNotHaveBean("otelWebClientCustomizer")); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/testSpring3/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/testSpring3/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationAutoConfigurationTest.java index bbac07a60b17..86489ce4435d 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/testSpring3/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationAutoConfigurationTest.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/testSpring3/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationAutoConfigurationTest.java @@ -5,8 +5,10 @@ package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web; +import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.spring.autoconfigure.internal.AbstractRestClientInstrumentationAutoConfigurationTest; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.http.client.ClientHttpRequestInterceptor; class RestClientInstrumentationAutoConfigurationTest extends AbstractRestClientInstrumentationAutoConfigurationTest { @@ -17,7 +19,12 @@ protected AutoConfigurations autoConfigurations() { } @Override - protected Class postProcessorClass() { + protected Class postProcessorClass() { return RestClientBeanPostProcessor.class; } + + @Override + protected ClientHttpRequestInterceptor getInterceptor(OpenTelemetry openTelemetry) { + return RestClientBeanPostProcessor.getInterceptor(openTelemetry); + } } diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/testSpring4/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationSpringBoot4AutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/testSpring4/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationSpringBoot4AutoConfigurationTest.java index 091db2d08af5..1a76e24cf829 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/testSpring4/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationSpringBoot4AutoConfigurationTest.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/testSpring4/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/web/RestClientInstrumentationSpringBoot4AutoConfigurationTest.java @@ -5,13 +5,10 @@ package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.web; -import static org.assertj.core.api.Assertions.assertThat; - import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.instrumentation.spring.autoconfigure.internal.AbstractRestClientInstrumentationAutoConfigurationTest; -import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.web.client.RestClient; +import org.springframework.http.client.ClientHttpRequestInterceptor; class RestClientInstrumentationSpringBoot4AutoConfigurationTest extends AbstractRestClientInstrumentationAutoConfigurationTest { @@ -22,50 +19,12 @@ protected AutoConfigurations autoConfigurations() { } @Override - protected Class postProcessorClass() { + protected Class postProcessorClass() { return RestClientBeanPostProcessorSpring4.class; } - @Test - void shouldNotCreateNewBeanWhenInterceptorAlreadyPresent() { - contextRunner - .withPropertyValues("otel.instrumentation.spring-web.enabled=true") - .run( - context -> { - RestClientBeanPostProcessorSpring4 beanPostProcessor = - context.getBean( - "otelRestClientBeanPostProcessor", RestClientBeanPostProcessorSpring4.class); - - RestClient restClientWithInterceptor = - RestClient.builder() - .requestInterceptor( - RestClientBeanPostProcessor.getInterceptor( - context.getBean(OpenTelemetry.class))) - .build(); - - RestClient processed = - (RestClient) - beanPostProcessor.postProcessAfterInitialization( - restClientWithInterceptor, "testBean"); - - // Should return the same instance when interceptor is already present - assertThat(processed).isSameAs(restClientWithInterceptor); - - // Verify only one interceptor exists - processed - .mutate() - .requestInterceptors( - interceptors -> { - long count = - interceptors.stream() - .filter( - rti -> - rti.getClass() - .getName() - .startsWith("io.opentelemetry.instrumentation")) - .count(); - assertThat(count).isEqualTo(1); - }); - }); + @Override + protected ClientHttpRequestInterceptor getInterceptor(OpenTelemetry openTelemetry) { + return RestClientBeanPostProcessorSpring4.getInterceptor(openTelemetry); } } diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/testSpring4/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebClientInstrumentationSpringBoot4AutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/testSpring4/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebClientInstrumentationSpringBoot4AutoConfigurationTest.java new file mode 100644 index 000000000000..491650ed1619 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/src/testSpring4/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/instrumentation/webflux/SpringWebClientInstrumentationSpringBoot4AutoConfigurationTest.java @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal.instrumentation.webflux; + +import io.opentelemetry.instrumentation.spring.autoconfigure.internal.AbstractWebClientCustomizerAutoConfigurationTest; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.webclient.WebClientCustomizer; +import org.springframework.web.reactive.function.client.WebClient; + +class SpringWebClientInstrumentationSpringBoot4AutoConfigurationTest + extends AbstractWebClientCustomizerAutoConfigurationTest { + + @Override + protected AutoConfigurations autoConfigurations() { + return AutoConfigurations.of(SpringWebClientInstrumentationSpringBoot4AutoConfiguration.class); + } + + @Override + protected Class webClientCustomizerClass() { + return WebClientCustomizer.class; + } + + @Override + protected void customizeWebClient(WebClientCustomizer customizer, WebClient.Builder builder) { + customizer.customize(builder); + } +} diff --git a/instrumentation/spring/spring-boot-autoconfigure/testing/build.gradle.kts b/instrumentation/spring/spring-boot-autoconfigure/testing/build.gradle.kts index c00480a8fa28..6ec1fbe7b4cc 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/testing/build.gradle.kts +++ b/instrumentation/spring/spring-boot-autoconfigure/testing/build.gradle.kts @@ -6,6 +6,7 @@ val springBootVersion = "2.7.18" dependencies { compileOnly("org.springframework.boot:spring-boot-restclient:4.0.0") + compileOnly("org.springframework.boot:spring-boot-starter-webflux:$springBootVersion") compileOnly("org.springframework.kafka:spring-kafka:2.9.0") compileOnly("org.springframework.boot:spring-boot-starter-test:$springBootVersion") diff --git a/instrumentation/spring/spring-boot-autoconfigure/testing/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/AbstractRestClientInstrumentationAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/testing/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/AbstractRestClientInstrumentationAutoConfigurationTest.java index d7e7ebe49658..fff10b873fe3 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/testing/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/AbstractRestClientInstrumentationAutoConfigurationTest.java +++ b/instrumentation/spring/spring-boot-autoconfigure/testing/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/AbstractRestClientInstrumentationAutoConfigurationTest.java @@ -9,15 +9,19 @@ import io.opentelemetry.api.OpenTelemetry; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.web.client.RestClient; public abstract class AbstractRestClientInstrumentationAutoConfigurationTest { protected abstract AutoConfigurations autoConfigurations(); - protected abstract Class postProcessorClass(); + protected abstract Class postProcessorClass(); + + protected abstract ClientHttpRequestInterceptor getInterceptor(OpenTelemetry openTelemetry); protected final ApplicationContextRunner contextRunner = new ApplicationContextRunner() @@ -77,4 +81,44 @@ void defaultConfiguration() { assertThat(context.getBean("otelRestClientBeanPostProcessor", postProcessorClass())) .isNotNull()); } + + @Test + void shouldNotCreateNewBeanWhenInterceptorAlreadyPresent() { + contextRunner + .withPropertyValues("otel.instrumentation.spring-web.enabled=true") + .run( + context -> { + BeanPostProcessor beanPostProcessor = + context.getBean("otelRestClientBeanPostProcessor", postProcessorClass()); + + RestClient restClientWithInterceptor = + RestClient.builder() + .requestInterceptor(getInterceptor(context.getBean(OpenTelemetry.class))) + .build(); + + RestClient processed = + (RestClient) + beanPostProcessor.postProcessAfterInitialization( + restClientWithInterceptor, "testBean"); + + // Should return the same instance when interceptor is already present + assertThat(processed).isSameAs(restClientWithInterceptor); + + // Verify only one interceptor exists + processed + .mutate() + .requestInterceptors( + interceptors -> { + long count = + interceptors.stream() + .filter( + rti -> + rti.getClass() + .getName() + .startsWith("io.opentelemetry.instrumentation")) + .count(); + assertThat(count).isEqualTo(1); + }); + }); + } } diff --git a/instrumentation/spring/spring-boot-autoconfigure/testing/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/AbstractWebClientCustomizerAutoConfigurationTest.java b/instrumentation/spring/spring-boot-autoconfigure/testing/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/AbstractWebClientCustomizerAutoConfigurationTest.java new file mode 100644 index 000000000000..19b80024cc83 --- /dev/null +++ b/instrumentation/spring/spring-boot-autoconfigure/testing/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/internal/AbstractWebClientCustomizerAutoConfigurationTest.java @@ -0,0 +1,109 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.spring.autoconfigure.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.OpenTelemetry; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * Abstract base test for WebClient customizer auto-configurations. Subclasses must provide the + * autoconfiguration class and WebClientCustomizer class for their Spring Boot version. + * + * @param the WebClientCustomizer type for the specific Spring Boot version + */ +public abstract class AbstractWebClientCustomizerAutoConfigurationTest { + + protected abstract AutoConfigurations autoConfigurations(); + + protected abstract Class webClientCustomizerClass(); + + protected abstract void customizeWebClient(T customizer, WebClient.Builder builder); + + protected ApplicationContextRunner contextRunner; + + @BeforeEach + void setUp() { + contextRunner = + new ApplicationContextRunner() + .withBean(OpenTelemetry.class, OpenTelemetry::noop) + .withConfiguration(autoConfigurations()); + } + + @Test + void shouldCreateCustomizerWhenEnabled() { + contextRunner + .withPropertyValues("otel.instrumentation.spring-webflux.enabled=true") + .run( + context -> + assertThat(context.getBean("otelWebClientCustomizer")) + .isNotNull() + .isInstanceOf(webClientCustomizerClass())); + } + + @Test + void shouldNotCreateCustomizerWhenDisabled() { + contextRunner + .withPropertyValues("otel.instrumentation.spring-webflux.enabled=false") + .run(context -> assertThat(context).doesNotHaveBean("otelWebClientCustomizer")); + } + + @Test + void shouldCreateCustomizerByDefault() { + contextRunner.run( + context -> + assertThat(context.getBean("otelWebClientCustomizer")) + .isNotNull() + .isInstanceOf(webClientCustomizerClass())); + } + + @Test + void shouldAddTracingFilterWhenCustomizerApplied() { + contextRunner.run( + context -> { + T customizer = context.getBean("otelWebClientCustomizer", webClientCustomizerClass()); + WebClient.Builder builder = WebClient.builder(); + customizeWebClient(customizer, builder); + + AtomicLong count = new AtomicLong(0); + builder + .build() + .mutate() + .filters( + filters -> + count.set( + filters.stream() + .filter( + f -> + f.getClass() + .getName() + .startsWith("io.opentelemetry.instrumentation")) + .count())); + assertThat(count.get()).isEqualTo(1); + }); + } + + @Test + void shouldNotCreateCustomizerWhenWebClientCustomizerNotOnClasspath() { + contextRunner + .withClassLoader(new FilteredClassLoader(webClientCustomizerClass())) + .run(context -> assertThat(context).doesNotHaveBean("otelWebClientCustomizer")); + } + + @Test + void shouldNotCreateCustomizerWhenWebClientNotOnClasspath() { + contextRunner + .withClassLoader(new FilteredClassLoader(WebClient.class)) + .run(context -> assertThat(context).doesNotHaveBean("otelWebClientCustomizer")); + } +}