From 6523ea34957350a098707d45d77c74abf2874dc9 Mon Sep 17 00:00:00 2001 From: Bjorn Beskow Date: Thu, 2 Apr 2026 12:34:36 +0200 Subject: [PATCH 1/3] Tests to highlight the problem. --- ...rvabilityTracerPropagationTestSupport.java | 4 +- .../observability/SpanScopeTest.java | 97 +++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 components/camel-micrometer-observability/src/test/java/org/apache/camel/micrometer/observability/SpanScopeTest.java diff --git a/components/camel-micrometer-observability/src/test/java/org/apache/camel/micrometer/observability/MicrometerObservabilityTracerPropagationTestSupport.java b/components/camel-micrometer-observability/src/test/java/org/apache/camel/micrometer/observability/MicrometerObservabilityTracerPropagationTestSupport.java index c39c09f762ec9..b036864d44a39 100644 --- a/components/camel-micrometer-observability/src/test/java/org/apache/camel/micrometer/observability/MicrometerObservabilityTracerPropagationTestSupport.java +++ b/components/camel-micrometer-observability/src/test/java/org/apache/camel/micrometer/observability/MicrometerObservabilityTracerPropagationTestSupport.java @@ -18,6 +18,7 @@ import java.util.List; +import io.micrometer.tracing.Tracer; import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext; import io.micrometer.tracing.otel.bridge.OtelPropagator; import io.micrometer.tracing.otel.bridge.OtelTracer; @@ -37,6 +38,7 @@ public class MicrometerObservabilityTracerPropagationTestSupport extends Exchang protected CamelOpenTelemetryExtension otelExtension = CamelOpenTelemetryExtension.create(); protected MicrometerObservabilityTracer tst = new MicrometerObservabilityTracer(); + protected Tracer micrometerTracer; @Override protected CamelContext createCamelContext() throws Exception { @@ -48,7 +50,7 @@ protected CamelContext createCamelContext() throws Exception { OtelPropagator otelPropagator = new OtelPropagator(propagators, otelTracer); OtelCurrentTraceContext currentTraceContext = new OtelCurrentTraceContext(); // We must convert the Otel Tracer into a micrometer Tracer - io.micrometer.tracing.Tracer micrometerTracer = new OtelTracer( + micrometerTracer = new OtelTracer( otelTracer, currentTraceContext, null); diff --git a/components/camel-micrometer-observability/src/test/java/org/apache/camel/micrometer/observability/SpanScopeTest.java b/components/camel-micrometer-observability/src/test/java/org/apache/camel/micrometer/observability/SpanScopeTest.java new file mode 100644 index 0000000000000..8854bedfc8056 --- /dev/null +++ b/components/camel-micrometer-observability/src/test/java/org/apache/camel/micrometer/observability/SpanScopeTest.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.micrometer.observability; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import io.opentelemetry.sdk.trace.data.SpanData; +import org.apache.camel.RoutesBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.micrometer.observability.CamelOpenTelemetryExtension.OtelTrace; +import org.apache.camel.telemetry.Op; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * Verifies that spans are properly put in scope during route execution. This is critical for routes triggered by + * consumers that don't have framework-level tracing (e.g., JMS), where Camel must manage the span scope itself. + * + * Without proper scope management, {@code tracer.currentSpan()} returns null during route execution, which prevents the + * trace from being exported and for downstream instrumentation from attaching to the Camel trace. + */ +public class SpanScopeTest extends MicrometerObservabilityTracerPropagationTestSupport { + + private final AtomicReference capturedCurrentSpan = new AtomicReference<>(); + + @Test + void testSpanIsInScopeDuringRouteExecution() { + template.sendBody("direct:start", "Test Message"); + + // The processor captured tracer.currentSpan() during route execution. + io.micrometer.tracing.Span current = capturedCurrentSpan.get(); + assertNotNull(current); + } + + @Test + void testCapturedSpanMatchesTraceId() { + template.sendBody("direct:start", "Test Message"); + + io.micrometer.tracing.Span current = capturedCurrentSpan.get(); + assertNotNull(current); + + // The captured current span should belong to the same trace as the recorded spans + Map traces = otelExtension.getTraces(); + assertEquals(1, traces.size()); + String expectedTraceId = traces.keySet().iterator().next(); + List spans = traces.get(expectedTraceId).getSpans(); + + SpanData receivedSpan = getSpan(spans, "direct://start", Op.EVENT_RECEIVED); + assertEquals(expectedTraceId, current.context().traceId()); + assertEquals(receivedSpan.getSpanId(), current.context().spanId()); + } + + @Test + void testSpanScopeIsCleanedUpAfterRouteExecution() { + template.sendBody("direct:start", "Test Message"); + + // After the route completes, the scope should be closed and + // tracer.currentSpan() should return null + io.micrometer.tracing.Span afterRoute = micrometerTracer.currentSpan(); + assertNull(afterRoute); + } + + @Override + protected RoutesBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:start") + .routeId("start") + .process(exchange -> { + capturedCurrentSpan.set(micrometerTracer.currentSpan()); + }) + .to("log:info"); + } + }; + } + +} From f641041a3a078ff75ff07c63e38d11bedced56ce Mon Sep 17 00:00:00 2001 From: Bjorn Beskow Date: Thu, 2 Apr 2026 14:26:00 +0200 Subject: [PATCH 2/3] Fix scope management, i.e. associate span with current trace. --- .../MicrometerObservabilitySpanAdapter.java | 18 +++++++++++++----- .../MicrometerObservabilityTracer.java | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/components/camel-micrometer-observability/src/main/java/org/apache/camel/micrometer/observability/MicrometerObservabilitySpanAdapter.java b/components/camel-micrometer-observability/src/main/java/org/apache/camel/micrometer/observability/MicrometerObservabilitySpanAdapter.java index 44bdff3e920d2..40548dec88044 100644 --- a/components/camel-micrometer-observability/src/main/java/org/apache/camel/micrometer/observability/MicrometerObservabilitySpanAdapter.java +++ b/components/camel-micrometer-observability/src/main/java/org/apache/camel/micrometer/observability/MicrometerObservabilitySpanAdapter.java @@ -18,6 +18,7 @@ import java.util.Map; +import io.micrometer.tracing.Tracer; import org.apache.camel.telemetry.Span; public class MicrometerObservabilitySpanAdapter implements Span { @@ -25,9 +26,12 @@ public class MicrometerObservabilitySpanAdapter implements Span { private static final String DEFAULT_EVENT_NAME = "log"; private final io.micrometer.tracing.Span span; + private final Tracer tracer; + private Tracer.SpanInScope spanInScope; - public MicrometerObservabilitySpanAdapter(io.micrometer.tracing.Span span) { + public MicrometerObservabilitySpanAdapter(io.micrometer.tracing.Span span, Tracer tracer) { this.span = span; + this.tracer = tracer; } @Override @@ -64,14 +68,18 @@ protected io.micrometer.tracing.Span getSpan() { protected void activate() { this.span.start(); - } - - protected void close() { - this.span.end(); + this.spanInScope = this.tracer.withSpan(this.span); } protected void deactivate() { + if (this.spanInScope != null) { + this.spanInScope.close(); + this.spanInScope = null; + } + } + protected void close() { + this.span.end(); } @Override diff --git a/components/camel-micrometer-observability/src/main/java/org/apache/camel/micrometer/observability/MicrometerObservabilityTracer.java b/components/camel-micrometer-observability/src/main/java/org/apache/camel/micrometer/observability/MicrometerObservabilityTracer.java index 76b18dd6fb4cc..4cdd42a960211 100644 --- a/components/camel-micrometer-observability/src/main/java/org/apache/camel/micrometer/observability/MicrometerObservabilityTracer.java +++ b/components/camel-micrometer-observability/src/main/java/org/apache/camel/micrometer/observability/MicrometerObservabilityTracer.java @@ -140,7 +140,7 @@ public Span create(String spanName, Span parent, SpanContextPropagationExtractor } span.name(spanName); - return new MicrometerObservabilitySpanAdapter(span); + return new MicrometerObservabilitySpanAdapter(span, tracer); } @Override From 72ec108c9931a2a3af1c52d708f4370ce10d78a0 Mon Sep 17 00:00:00 2001 From: Bjorn Beskow Date: Thu, 2 Apr 2026 14:26:18 +0200 Subject: [PATCH 3/3] Prevent stale trace context propagation --- .../MicrometerObservabilityTracer.java | 16 ++++++++++++++++ ...bservabilityTracerPropagationTestSupport.java | 6 ++++++ 2 files changed, 22 insertions(+) diff --git a/components/camel-micrometer-observability/src/main/java/org/apache/camel/micrometer/observability/MicrometerObservabilityTracer.java b/components/camel-micrometer-observability/src/main/java/org/apache/camel/micrometer/observability/MicrometerObservabilityTracer.java index 4cdd42a960211..4f8515fc37a5b 100644 --- a/components/camel-micrometer-observability/src/main/java/org/apache/camel/micrometer/observability/MicrometerObservabilityTracer.java +++ b/components/camel-micrometer-observability/src/main/java/org/apache/camel/micrometer/observability/MicrometerObservabilityTracer.java @@ -136,6 +136,22 @@ public Span create(String spanName, Span parent, SpanContextPropagationExtractor return extractor.get(key) == null ? null : (String) extractor.get(key); }); + // If no trace headers were found in the carrier, any parent context in + // the builder came from the thread-local scope (e.g., Context.current() + // for OTel). This can be stale when async processing moves span lifecycle + // to a different thread, leaving the original thread's scope un-cleaned. + // Force a root span in that case to prevent trace contamination. + boolean hasTraceHeaders = false; + for (String field : propagator.fields()) { + if (extractor.get(field) != null) { + hasTraceHeaders = true; + break; + } + } + if (!hasTraceHeaders) { + builder.setNoParent(); + } + span = builder.start(); } span.name(spanName); diff --git a/components/camel-micrometer-observability/src/test/java/org/apache/camel/micrometer/observability/MicrometerObservabilityTracerPropagationTestSupport.java b/components/camel-micrometer-observability/src/test/java/org/apache/camel/micrometer/observability/MicrometerObservabilityTracerPropagationTestSupport.java index b036864d44a39..8996a363d4b9f 100644 --- a/components/camel-micrometer-observability/src/test/java/org/apache/camel/micrometer/observability/MicrometerObservabilityTracerPropagationTestSupport.java +++ b/components/camel-micrometer-observability/src/test/java/org/apache/camel/micrometer/observability/MicrometerObservabilityTracerPropagationTestSupport.java @@ -23,6 +23,7 @@ import io.micrometer.tracing.otel.bridge.OtelPropagator; import io.micrometer.tracing.otel.bridge.OtelTracer; import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.context.Context; import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.sdk.trace.data.SpanData; import org.apache.camel.CamelContext; @@ -42,6 +43,11 @@ public class MicrometerObservabilityTracerPropagationTestSupport extends Exchang @Override protected CamelContext createCamelContext() throws Exception { + // Clear any stale OTel context left by previous tests (e.g., from async thread + // handoff where deactivate() ran on a different thread). OtelPropagator.extract() + // reads Context.current() directly, so stale spans would become unwanted parents. + Context.root().makeCurrent(); + CamelContext context = super.createCamelContext(); ContextPropagators propagators = otelExtension.getPropagators();