Describe the bug
AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder> is the documented extension point for customizing the underlying Service Bus client builder (it is even the workaround recommended in #43555). However, for @ServiceBusListener processors created by DefaultServiceBusNamespaceProcessorFactory, any ClientOptions set by such a customizer is silently discarded.
This makes it impossible to configure Azure SDK tracing (azure-core-tracing-opentelemetry + OpenTelemetryTracingOptions) for Spring Messaging Service Bus listeners, because TracingOptions can only travel via ClientOptions and cannot be expressed through configuration properties.
Root cause (traced in spring-cloud-azure_7.2.0 sources)
- Root-builder customizers do run —
customizeBuilder(...) is the last step of ServiceBusClientBuilderFactory.build(), so at that point the customizer's ClientOptions is set on the root ServiceBusClientBuilder.
- The enclosing
ServiceBusProcessorClientBuilderFactory (non-shared path, used by DefaultServiceBusNamespaceProcessorFactory#doCreateProcessor) then runs its own configureCore, and AbstractServiceBusSubClientBuilderFactory#consumeClientOptions() executes:
if (!isShareServiceBusClientBuilder()) {
getServiceBusClientBuilder().clientOptions(client); // properties-derived instance
}
replacing the customizer's ClientOptions with the sub-factory's own instance (populated only from properties, e.g. application id).
azure-messaging-servicebus creates the tracer from the root builder's clientOptions field only at buildProcessorClient() time (createTracer(clientOptions) in ServiceBusClientBuilder) — i.e. after the overwrite — so the tracing configuration never reaches the SDK instrumentation.
To Reproduce
Spring Boot app with spring-messaging-azure-servicebus, an @ServiceBusListener, and:
@Configuration(proxyBeanMethods = false)
class ServiceBusTracingConfig {
@Bean
AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder> serviceBusTracingCustomizer(
OpenTelemetry openTelemetry) {
return builder ->
builder.clientOptions(
new ClientOptions()
.setTracingOptions(new OpenTelemetryTracingOptions().setOpenTelemetry(openTelemetry)));
}
}
The customizer executes (breakpoint confirms), yet no ServiceBus.* spans are ever emitted. Reproduced end-to-end with an integration test against the Service Bus emulator and an in-memory span exporter attached to the application's OpenTelemetry SDK: the listener's downstream spans (e.g. MongoDB) are exported, each starting a fresh root trace, while no Service Bus process/receive/settle spans appear and the producer's traceparent from the message application properties is never continued.
Expected behavior
ClientOptions set by a root-builder customizer should survive processor creation (customizers are documented to run last), or there should be a supported way to pass TracingOptions to processor clients created for @ServiceBusListener.
Setup
spring-cloud-azure-dependencies:7.2.0, spring-messaging-azure-servicebus, azure-messaging-servicebus:7.17.17
azure-core-tracing-opentelemetry:1.0.0-beta.64, OpenTelemetry Spring Boot starter 2.27.0 (SDK exposed as a Spring bean; GlobalOpenTelemetry intentionally not used)
- Spring Boot 4.0.6, Java 21
Additional context
The same guarded-redirect pattern in consumeClientOptions() applies to the other consume* methods, but ClientOptions is the only carrier of TracingOptions, so tracing is the visible casualty. Binding via GlobalOpenTelemetry instead is not a viable workaround because the plugin resolves the global eagerly at client build time, which conflicts with frameworks that never register it (e.g. the OpenTelemetry Spring Boot starter).
Describe the bug
AzureServiceClientBuilderCustomizer<ServiceBusClientBuilder>is the documented extension point for customizing the underlying Service Bus client builder (it is even the workaround recommended in #43555). However, for@ServiceBusListenerprocessors created byDefaultServiceBusNamespaceProcessorFactory, anyClientOptionsset by such a customizer is silently discarded.This makes it impossible to configure Azure SDK tracing (
azure-core-tracing-opentelemetry+OpenTelemetryTracingOptions) for Spring Messaging Service Bus listeners, becauseTracingOptionscan only travel viaClientOptionsand cannot be expressed through configuration properties.Root cause (traced in
spring-cloud-azure_7.2.0sources)customizeBuilder(...)is the last step ofServiceBusClientBuilderFactory.build(), so at that point the customizer'sClientOptionsis set on the rootServiceBusClientBuilder.ServiceBusProcessorClientBuilderFactory(non-shared path, used byDefaultServiceBusNamespaceProcessorFactory#doCreateProcessor) then runs its ownconfigureCore, andAbstractServiceBusSubClientBuilderFactory#consumeClientOptions()executes:ClientOptionswith the sub-factory's own instance (populated only from properties, e.g. application id).azure-messaging-servicebuscreates the tracer from the root builder'sclientOptionsfield only atbuildProcessorClient()time (createTracer(clientOptions)inServiceBusClientBuilder) — i.e. after the overwrite — so the tracing configuration never reaches the SDK instrumentation.To Reproduce
Spring Boot app with
spring-messaging-azure-servicebus, an@ServiceBusListener, and:The customizer executes (breakpoint confirms), yet no
ServiceBus.*spans are ever emitted. Reproduced end-to-end with an integration test against the Service Bus emulator and an in-memory span exporter attached to the application's OpenTelemetry SDK: the listener's downstream spans (e.g. MongoDB) are exported, each starting a fresh root trace, while no Service Bus process/receive/settle spans appear and the producer'straceparentfrom the message application properties is never continued.Expected behavior
ClientOptionsset by a root-builder customizer should survive processor creation (customizers are documented to run last), or there should be a supported way to passTracingOptionsto processor clients created for@ServiceBusListener.Setup
spring-cloud-azure-dependencies:7.2.0,spring-messaging-azure-servicebus,azure-messaging-servicebus:7.17.17azure-core-tracing-opentelemetry:1.0.0-beta.64, OpenTelemetry Spring Boot starter 2.27.0 (SDK exposed as a Spring bean;GlobalOpenTelemetryintentionally not used)Additional context
The same guarded-redirect pattern in
consumeClientOptions()applies to the otherconsume*methods, butClientOptionsis the only carrier ofTracingOptions, so tracing is the visible casualty. Binding viaGlobalOpenTelemetryinstead is not a viable workaround because the plugin resolves the global eagerly at client build time, which conflicts with frameworks that never register it (e.g. the OpenTelemetry Spring Boot starter).