Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I understand correctly that the intention here is that the interceptor will be added here when the WebClient wasn't created by spring. For example when there is something like

@Bean
WebClient webClient() {
  return WebClient.create();
}

I'd guess the WebClientCustomizer won't run then. What if there is

@Bean
WebClient.Builder webClientBuilder() {
  return WebClient.builder();
}

I'd guess the WebClientCustomizer won't run then either. Should we handle that? Should this be aligned with RestClientBeanPostProcessor (cc @zeitlinger, do you remember why you wrote RestClientBeanPostProcessor the way it is).
Assuming that the intent is to add the interceptor in post processor when the customizer didn't run do we have any tests for this?

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<ExchangeFilterFunction> filters) {
return filters.stream().noneMatch(WebClientTracingFilter.class::isInstance);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<ClientHttpRequestInterceptor> interceptors,
ClientHttpRequestInterceptor instrumentationInterceptor) {
return interceptors.stream()
.noneMatch(interceptor -> interceptor.getClass() == instrumentationInterceptor.getClass());
}

static ClientHttpRequestInterceptor getInterceptor(OpenTelemetry openTelemetry) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -37,6 +39,7 @@ static RestClientBeanPostProcessor otelRestClientBeanPostProcessor(
}

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
RestClientCustomizer otelRestClientCustomizer(
ObjectProvider<OpenTelemetry> openTelemetryProvider) {
return builder ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -37,6 +39,7 @@ static RestClientBeanPostProcessorSpring4 otelRestClientBeanPostProcessor(
}

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
RestClientCustomizer otelRestClientCustomizer(
ObjectProvider<OpenTelemetry> openTelemetryProvider) {
return builder ->
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Adds OpenTelemetry instrumentation via WebClientCustomizer for Spring boot 4.
*
* <p>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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

This file was deleted.

Loading
Loading