Skip to content

fix(spring-boot-autoconfigure): OpenTelemetry autoconfig to wire Spring-managed OpenTelemetry#4614

Open
MateuszNaKodach wants to merge 2 commits into
axon-4.13.xfrom
fix/spring-boot-open-telemetry
Open

fix(spring-boot-autoconfigure): OpenTelemetry autoconfig to wire Spring-managed OpenTelemetry#4614
MateuszNaKodach wants to merge 2 commits into
axon-4.13.xfrom
fix/spring-boot-open-telemetry

Conversation

@MateuszNaKodach
Copy link
Copy Markdown
Contributor

@MateuszNaKodach MateuszNaKodach commented May 25, 2026

Summary

OpenTelemetryAutoConfiguration currently builds the OpenTelemetrySpanFactory without passing in the application's OpenTelemetry instance:

@Bean
@ConditionalOnMissingBean(SpanFactory.class)
public SpanFactory spanFactory() {
    return OpenTelemetrySpanFactory.builder().build();
}

OpenTelemetrySpanFactory.Builder.build() then falls back to GlobalOpenTelemetry:

if (tracer == null) {
    tracer = GlobalOpenTelemetry.getTracer("AxonFramework-OpenTelemetry");
}
if (textMapPropagator == null) {
    textMapPropagator = GlobalOpenTelemetry.getPropagators().getTextMapPropagator();
}

This works for the OpenTelemetry Java agent and for setups that explicitly call OpenTelemetrySdk.builder()…buildAndRegisterGlobal(). It silently breaks for Spring Boot 3 applications using micrometer-tracing-bridge-otel: Spring Boot creates an OpenTelemetry bean (intentionally not registered as global, to avoid hidden static state in the container) and the micrometer-tracing-bridge-otel adapter consumes it via dependency injection. Because GlobalOpenTelemetry is never populated, both builder defaults resolve to no-ops — propagateContext() silently drops the W3C trace context across every asynchronous Axon boundary (event processors, deadlines, distributed command/query buses) without any error or warning.

The symptoms are subtle: HTTP-side spans render correctly inside one trace, but StreamingEventProcessor.process(…), EventProcessor.process(…), CommandBus.handleDistributedCommand(…) etc. always start brand-new root traces with no link back to the originator. The framework's documented linking behaviour ("asynchronous invocations are linked to their originating spans") becomes effectively unreachable, because the parent context never makes it into the message metadata in the first place.

Fix

Inject ObjectProvider<OpenTelemetry> and, when a Spring-managed OpenTelemetry bean is available, explicitly wire its Tracer and TextMapPropagator into the builder. If no bean is present, the previous default behaviour is preserved:

@Bean
@ConditionalOnMissingBean(SpanFactory.class)
public SpanFactory spanFactory(ObjectProvider<OpenTelemetry> openTelemetryProvider) {
    OpenTelemetry openTelemetry = openTelemetryProvider.getIfAvailable();
    if (openTelemetry == null) {
        return OpenTelemetrySpanFactory.builder().build();
    }
    return OpenTelemetrySpanFactory.builder()
                                   .tracer(openTelemetry.getTracer("AxonFramework-OpenTelemetry"))
                                   .contextPropagators(openTelemetry.getPropagators().getTextMapPropagator())
                                   .build();
}

Backwards compatibility

User setup Before After Behaviour change?
OpenTelemetry Java agent (-javaagent:opentelemetry-javaagent.jar) Agent registers GlobalOpenTelemetry; no Spring OpenTelemetry bean exists → builder defaults pick it up ObjectProvider.getIfAvailable() returns null → builder defaults pick up GlobalOpenTelemetry None
Manual SDK with buildAndRegisterGlobal() (the path the Axon reference guide describes) GlobalOpenTelemetry used If OpenTelemetry bean is not exposed in Spring (the usual case): identical. If it is exposed: the same SDK instance is wired explicitly — observationally identical None
Spring Boot 3 + micrometer-tracing-bridge-otel Silent no-op (propagator drops W3C context, span links never emitted) Spring-managed OpenTelemetry wired in; W3C context propagated, span links visible across async boundaries Fixed (was broken)

The legacy fallback to GlobalOpenTelemetry remains intact for users on the Java agent or manual SDK setups. The @ConditionalOnMissingBean(SpanFactory.class) guard is unchanged, so users with a custom SpanFactory bean are unaffected.

Notes

  • I found uncoducmented properties and added it to the docs.
  • Verified end-to-end in a Spring Boot 3.5 + micrometer-tracing-bridge-otel + axon-tracing-opentelemetry 4.13.1 application: after the fix, propagateContext() injects the W3C traceparent into message metadata (confirmed by inspecting stored event metadata and by the added regression test). Without this fix, message metadata stays empty and async handlers cannot reconstruct the parent context — making the documented span-linking behaviour effectively unreachable, regardless of what other tracing properties (axon.tracing.event-processor.distributed-in-same-trace, …disable-batch-trace, etc.) are configured.

… the span factory

`OpenTelemetryAutoConfiguration#spanFactory()` built `OpenTelemetrySpanFactory` with no arguments, so its builder fell back to `GlobalOpenTelemetry.getTracer(...)` and `GlobalOpenTelemetry.getPropagators()` for tracer and W3C propagator. With Spring Boot 3 + `micrometer-tracing-bridge-otel` (Spring's recommended path) the `OpenTelemetry` bean is never registered as global, so both defaults resolve to no-ops — `propagateContext()` silently drops the W3C trace context and async Axon handlers (event processors, deadlines, distributed command/query buses) start disconnected root traces.

Inject `ObjectProvider<OpenTelemetry>` and, when a Spring-managed bean is available, pass its tracer and `TextMapPropagator` explicitly to the builder. When no bean is available the defaults are kept, preserving compatibility with the OpenTelemetry Java agent and with manual SDK installations that call `OpenTelemetrySdkBuilder#buildAndRegisterGlobal()`.

Adds two tests:
- `spanFactoryUsesSpringManagedOpenTelemetryWhenAvailable` — verifies the factory's tracer and propagator are the same instances as the Spring-managed bean's, and behaviourally confirms `propagateContext` injects the W3C `traceparent` into message metadata.
- `spanFactoryFallsBackToGlobalOpenTelemetryWhenNoBeanAvailable` — guards the legacy Java-agent / `buildAndRegisterGlobal()` path.

Adds `opentelemetry-sdk` as a test-scope dependency to allow constructing a real `OpenTelemetry` with a W3C propagator.
@MateuszNaKodach MateuszNaKodach self-assigned this May 25, 2026
@MateuszNaKodach MateuszNaKodach added the Type: Bug Use to signal issues that describe a bug within the system. label May 25, 2026
@MateuszNaKodach MateuszNaKodach added this to the Release 4.13.2 milestone May 25, 2026
@MateuszNaKodach MateuszNaKodach added the Type: Documentation Use to signal issues that describe documentation work. label May 25, 2026
…properties for span nesting and propagation timing
@MateuszNaKodach MateuszNaKodach force-pushed the fix/spring-boot-open-telemetry branch from aa4e585 to 196f1c8 Compare May 25, 2026 15:09
@sonarqubecloud
Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Type: Bug Use to signal issues that describe a bug within the system. Type: Documentation Use to signal issues that describe documentation work.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant