This is an automated email from the ASF dual-hosted git repository.
squakez pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 4db6ef7c1159 chore(components): specify custom span nesting in
opentelemetry
4db6ef7c1159 is described below
commit 4db6ef7c1159223a002cf6539648059b4f58516e
Author: Pasquale Congiusti <[email protected]>
AuthorDate: Mon May 11 12:37:34 2026 +0200
chore(components): specify custom span nesting in opentelemetry
As we have moved the scope only for Processors, any call to the endpoint,
which generates an event is not providing the scope.
This is needed by custom span to nest the execution under the specific
processor.
Added some test and documentation to clarify this architecture constraint.
---
.../src/main/docs/opentelemetry2.adoc | 32 +++++
.../camel/opentelemetry2/SpanToBeanTest.java | 139 +++++++++++++++++++
.../org/apache/camel/telemetry/SpanToBeanTest.java | 151 +++++++++++++++++++++
3 files changed, 322 insertions(+)
diff --git a/components/camel-opentelemetry2/src/main/docs/opentelemetry2.adoc
b/components/camel-opentelemetry2/src/main/docs/opentelemetry2.adoc
index e443cebd3e83..15b90da6144a 100644
--- a/components/camel-opentelemetry2/src/main/docs/opentelemetry2.adoc
+++ b/components/camel-opentelemetry2/src/main/docs/opentelemetry2.adoc
@@ -206,6 +206,38 @@ public void process(Exchange exchange) throws Exception {
}
----
+==== Custom spans or third party spans hierarchy
+
+In complex integrations it is advisable to have third party dependencies or
add custom spans at a Processor level in order to get advanced telemetry
information. When these spans are added at Processor level, then, you can
expect the span to be nested under the specific core Processor. For example:
+
+[source,java]
+----
+ from("direct:start")
+ .bean(MyBean.class)
+...
+ class MyBean {
+ @WithSpan
+ public void myLogic() {
+ // custom logic
+ }
+ }
+----
+
+You should expect your custom span "myLogic" to be nested under the Bean
processor span.
+
+If you instead call it with an endpoint producer, the process is converted to
an event, and as we cannot capture the scope, then it would nest the custom
span under the endpoint instead. For example:
+
+[source,java]
+----
+ from("direct:start")
+ .to("bean:myBean")
+...
+----
+
+The spans produced are slightly different because now they include an
additional span for the endpoint ("to" node). And, more important, as
highlighted above, the custom span is going to be nested directly under the
endpoint span "to" instead of the processor span ("bean").
+
+For this reason, whenever you need to provide custom telemetry information, it
is highly advisable to call directly the processors instead of the endpoints
DSL. Otherwise you just have to expect the custom span to be nested under the
endpoint.
+
=== Baggage customization
`Baggage` is a way to attach key-value metadata to a request and carry it
across service boundaries. In the context of OpenTelemetry, baggage travels
along with the context (like trace/span), but it's meant for custom data you
define, not telemetry internals. Camel allows you to programmatically provide
any `Baggage` information via header settings. Whenever the component finds an
header defined as `OTEL_BAGGAGE_xyz` it will consider it as a baggage variable
named `xyz`. For example, in [...]
diff --git
a/components/camel-opentelemetry2/src/test/java/org/apache/camel/opentelemetry2/SpanToBeanTest.java
b/components/camel-opentelemetry2/src/test/java/org/apache/camel/opentelemetry2/SpanToBeanTest.java
new file mode 100644
index 000000000000..64f0eb2d8c1c
--- /dev/null
+++
b/components/camel-opentelemetry2/src/test/java/org/apache/camel/opentelemetry2/SpanToBeanTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.opentelemetry2;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.sdk.trace.data.SpanData;
+import org.apache.camel.CamelContext;
+import org.apache.camel.CamelContextAware;
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.opentelemetry2.CamelOpenTelemetryExtension.OtelTrace;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class SpanToBeanTest extends OpenTelemetryTracerTestSupport {
+
+ Tracer tracer =
otelExtension.getOpenTelemetry().getTracer("spanInjection");
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ OpenTelemetryTracer tst = new OpenTelemetryTracer();
+ tst.setTracer(tracer);
+
tst.setContextPropagators(otelExtension.getOpenTelemetry().getPropagators());
+ CamelContext context = super.createCamelContext();
+ CamelContextAware.trySetCamelContext(tst, context);
+ tst.init(context);
+ return context;
+ }
+
+ @Test
+ void testRouteSingleRequest() throws IOException {
+ template.sendBody("direct:start", "my-body");
+ Map<String, OtelTrace> traces = otelExtension.getTraces();
+ assertEquals(1, traces.size());
+ checkTrace(traces.values().iterator().next());
+ }
+
+ private void checkTrace(OtelTrace trace) {
+ List<SpanData> spans = trace.getSpans();
+ assertEquals(8, spans.size());
+ SpanData testProducer = spans.get(0);
+ SpanData direct = spans.get(1);
+ SpanData innerLog = spans.get(2);
+ SpanData beanTo = spans.get(3);
+ SpanData beanProcessor = spans.get(4);
+ SpanData beanMethod = spans.get(5);
+ SpanData log = spans.get(6);
+ SpanData innerToLog = spans.get(7);
+
+ // Validate span completion
+ assertTrue(testProducer.hasEnded());
+ assertTrue(direct.hasEnded());
+ assertTrue(innerLog.hasEnded());
+ assertTrue(beanTo.hasEnded());
+ assertTrue(beanProcessor.hasEnded());
+ assertTrue(beanMethod.hasEnded());
+ assertTrue(log.hasEnded());
+ assertTrue(innerToLog.hasEnded());
+
+ // Validate same trace
+ assertEquals(testProducer.getSpanContext().getTraceId(),
direct.getSpanContext().getTraceId());
+ assertEquals(testProducer.getSpanContext().getTraceId(),
innerLog.getSpanContext().getTraceId());
+ assertEquals(testProducer.getSpanContext().getTraceId(),
beanTo.getSpanContext().getTraceId());
+ assertEquals(testProducer.getSpanContext().getTraceId(),
beanProcessor.getSpanContext().getTraceId());
+ assertEquals(testProducer.getSpanContext().getTraceId(),
beanMethod.getSpanContext().getTraceId());
+ assertEquals(testProducer.getSpanContext().getTraceId(),
log.getSpanContext().getTraceId());
+ assertEquals(testProducer.getSpanContext().getTraceId(),
innerToLog.getSpanContext().getTraceId());
+
+ // Validate hierarchy
+ assertFalse(testProducer.getParentSpanContext().isValid());
+
+ assertEquals(testProducer.getSpanContext().getSpanId(),
direct.getParentSpanContext().getSpanId());
+ assertEquals(direct.getSpanContext().getSpanId(),
innerLog.getParentSpanContext().getSpanId());
+ assertEquals(direct.getSpanContext().getSpanId(),
beanTo.getParentSpanContext().getSpanId());
+ assertEquals(beanTo.getSpanContext().getSpanId(),
beanProcessor.getParentSpanContext().getSpanId());
+ // NOTE: the bean method belongs to the component ("to") node.
+ assertEquals(beanTo.getSpanContext().getSpanId(),
beanMethod.getParentSpanContext().getSpanId());
+ assertEquals(direct.getSpanContext().getSpanId(),
log.getParentSpanContext().getSpanId());
+ assertEquals(log.getSpanContext().getSpanId(),
innerToLog.getParentSpanContext().getSpanId());
+ }
+
+ @Override
+ protected RoutesBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ MyBean myBean = new MyBean();
+ this.getCamelContext().getRegistry().bind("myBean", myBean);
+
+ from("direct:start")
+ .routeId("start")
+ .log("A message")
+ .to("bean:myBean")
+ .process(new Processor() {
+ @Override
+ public void process(Exchange exchange) throws
Exception {
+ exchange.getIn().setHeader("operation",
"fake");
+ }
+ })
+ .to("log:info");
+ }
+ };
+ }
+
+ class MyBean {
+ // NOTE: the commented annotation below would work only when an agent
or a runtime framework (quarkus or spring)
+ // is available. We simulate it creating the Span by hand instead.
+ //@WithSpan
+ public void helloWorld() {
+ Span mySpan = tracer.spanBuilder("helloworld").startSpan();
+ // Do the work here
+ mySpan.end();
+ }
+ }
+}
diff --git
a/components/camel-telemetry/src/test/java/org/apache/camel/telemetry/SpanToBeanTest.java
b/components/camel-telemetry/src/test/java/org/apache/camel/telemetry/SpanToBeanTest.java
new file mode 100644
index 000000000000..0e4433ea4654
--- /dev/null
+++
b/components/camel-telemetry/src/test/java/org/apache/camel/telemetry/SpanToBeanTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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.telemetry;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.CamelContextAware;
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.telemetry.mock.MockSpanAdapter;
+import org.apache.camel.telemetry.mock.MockTrace;
+import org.apache.camel.telemetry.mock.MockTracer;
+import org.apache.camel.test.junit6.ExchangeTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class SpanToBeanTest extends ExchangeTestSupport {
+
+ MockTracer mockTracer;
+
+ @Override
+ protected CamelContext createCamelContext() throws Exception {
+ CamelContext context = super.createCamelContext();
+ this.mockTracer = new MockTracer();
+ CamelContextAware.trySetCamelContext(mockTracer, context);
+ mockTracer.init(context);
+ return context;
+ }
+
+ @Test
+ void testProcessorsTraceRequest() {
+ template.sendBody("direct:start", "my-body");
+ Map<String, MockTrace> traces = mockTracer.traces();
+ assertEquals(1, traces.size());
+ checkTrace(traces.values().iterator().next());
+ }
+
+ private void checkTrace(MockTrace trace) {
+ List<Span> spans = trace.spans();
+ assertEquals(8, spans.size());
+ // Cast to implementation object to be able to
+ // inspect the status of the Span.
+ MockSpanAdapter testProducer = (MockSpanAdapter) spans.get(0);
+ MockSpanAdapter direct = (MockSpanAdapter) spans.get(1);
+ MockSpanAdapter innerLog = (MockSpanAdapter) spans.get(2);
+ MockSpanAdapter toBean = (MockSpanAdapter) spans.get(3);
+ MockSpanAdapter bean = (MockSpanAdapter) spans.get(4);
+ MockSpanAdapter beanMethod = (MockSpanAdapter) spans.get(5);
+ MockSpanAdapter log = (MockSpanAdapter) spans.get(6);
+ MockSpanAdapter innerToLog = (MockSpanAdapter) spans.get(7);
+
+ // Validate span completion
+ assertEquals("true", testProducer.getTag("isDone"));
+ assertEquals("true", direct.getTag("isDone"));
+ assertEquals("true", innerLog.getTag("isDone"));
+ assertEquals("true", toBean.getTag("isDone"));
+ assertEquals("true", bean.getTag("isDone"));
+ assertEquals("true", beanMethod.getTag("isDone"));
+ assertEquals("true", log.getTag("isDone"));
+ assertEquals("true", innerToLog.getTag("isDone"));
+
+ // Validate same trace
+ assertEquals(testProducer.getTag("traceid"), direct.getTag("traceid"));
+ assertEquals(testProducer.getTag("traceid"),
innerLog.getTag("traceid"));
+ assertEquals(testProducer.getTag("traceid"), log.getTag("traceid"));
+ assertEquals(testProducer.getTag("traceid"), toBean.getTag("traceid"));
+ assertEquals(testProducer.getTag("traceid"), bean.getTag("traceid"));
+ assertEquals(testProducer.getTag("traceid"),
beanMethod.getTag("traceid"));
+ assertEquals(testProducer.getTag("traceid"),
innerToLog.getTag("traceid"));
+
+ // Validate op
+ assertEquals(Op.EVENT_RECEIVED.toString(), direct.getTag("op"));
+
+ // Validate hierarchy
+ assertNull(testProducer.getTag("parentSpan"));
+ assertEquals(testProducer.getTag("spanid"),
direct.getTag("parentSpan"));
+ assertEquals(direct.getTag("spanid"), innerLog.getTag("parentSpan"));
+ assertEquals(direct.getTag("spanid"), log.getTag("parentSpan"));
+ assertEquals(direct.getTag("spanid"), toBean.getTag("parentSpan"));
+ assertEquals(toBean.getTag("spanid"), bean.getTag("parentSpan"));
+ assertEquals(bean.getTag("spanid"), beanMethod.getTag("parentSpan"));
+ assertEquals(log.getTag("spanid"), innerToLog.getTag("parentSpan"));
+
+ // Validate operations
+ assertEquals(Op.EVENT_SENT.toString(), testProducer.getTag("op"));
+ assertEquals(Op.EVENT_RECEIVED.toString(), direct.getTag("op"));
+
+ // Validate message logging
+ assertEquals("A message",
innerLog.logEntries().get(0).fields().get("message"));
+ assertEquals(
+ "Exchange[ExchangePattern: InOnly, BodyType: String, Body:
my-body]",
+ innerToLog.logEntries().get(0).fields().get("message"));
+ }
+
+ @Override
+ protected RoutesBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ MyBean myBean = new MyBean();
+ this.getCamelContext().getRegistry().bind("myBean", myBean);
+
+ from("direct:start")
+ .routeId("start")
+ .log("A message")
+ .to("bean:myBean")
+ .process(new Processor() {
+ @Override
+ public void process(Exchange exchange) throws
Exception {
+ exchange.getIn().setHeader("operation",
"fake");
+ }
+ })
+ .to("log:info");
+ }
+ };
+ }
+
+ class MyBean {
+ // We simulate the creation of a Span by hand.
+ public void helloWorld(Exchange exchange) {
+ // We just simulate the creation of a span and proper nesting. In
a real implementation
+ // it is up to the telemetry technology to do so (for example, via
method annotations)
+ Span parentSpan = new SpanStorageManagerExchange().peek(exchange);
+ Span span = mockTracer.getSpanLifecycleManager().create("mySpan",
"bo", parentSpan, null);
+ mockTracer.getSpanLifecycleManager().activate(span);
+ mockTracer.getSpanLifecycleManager().deactivate(span);
+ mockTracer.getSpanLifecycleManager().close(span);
+ }
+ }
+
+}