This is an automated email from the ASF dual-hosted git repository.
davsclaus 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 446baf1a1be4 CAMEL-23718: Embedded OTLP receiver for OTel Java Agent
in Camel JBang (#23868)
446baf1a1be4 is described below
commit 446baf1a1be49a857e0769552c06f041fe5755b7
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Jun 9 14:41:41 2026 +0200
CAMEL-23718: Embedded OTLP receiver for OTel Java Agent in Camel JBang
(#23868)
* CAMEL-23718: Embedded OTLP receiver for OpenTelemetry Java Agent in Camel
JBang
Add --open-telemetry-agent flag to camel run/dev/debug that automatically
attaches the OpenTelemetry Java Agent and configures an embedded OTLP
receiver
inside the Camel process. The agent's protobuf-encoded spans are received on
/v1/traces and fed into the in-memory DevSpanExporter, making them visible
in the TUI OTel Spans tab — zero config, no external collector needed.
Also adds a Process tab in the TUI (under More) showing PID, Java version,
command line and other process metadata, and an F2 checkbox to toggle the
OTel Agent option from the run options form.
Co-Authored-By: Claude <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23718: Revert unrelated generated configurer change
Co-Authored-By: Claude <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23718: Exclude OTLP receiver route from tracing to avoid
self-tracing loop
Co-Authored-By: Claude <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23718: Filter out self-tracing spans from OTLP receiver
The Java Agent instruments the HTTP POST to /v1/traces at the JVM level,
creating spans for the receiver itself. Filter these out before feeding
to DevSpanExporter to avoid noise in the OTel Spans tab.
Co-Authored-By: Claude <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23718: Add Jaeger export mode for OpenTelemetry Agent
Co-Authored-By: Claude <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23718: Distinguish Camel vs 3rd-party spans in TUI waterfall
- Add scopeName to span JSON serialization and SpanEntry record
- Camel spans shown in cyan, 3rd-party agent spans in magenta
- Span detail panel shows Source field for agent-instrumented spans
- Update F1 help with span color legend and OTel agent mode info
- Fix MCP Process tab missing from MORE_TAB_NAMES and navigateToTab
- Add Spans and Process to tui_navigate MCP tool description
- Enhance CLI --observe and --open-telemetry-agent help descriptions
Co-Authored-By: Claude <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23718: Add camel-only filter toggle in OTel waterfall
Press 'c' to hide 3rd-party agent spans (SQL, HTTP client, etc.)
and focus on Camel route-level flow. Filtered spans promote their
children to the same depth so the tree stays connected.
Co-Authored-By: Claude <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23718: Address review feedback
- Add unit tests for OtlpProtobufSpanData protobuf parsing (scope info,
attributes, span kinds, error status, root spans, multiple spans)
- Refactor duplicated deferred launch logic in ActionsPopup into a single
DeferredLaunch record with a Runnable callback
- Add upgrade guide entry for new exportTarget option and CLI flags
Co-Authored-By: Claude <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---------
Signed-off-by: Claus Ibsen <[email protected]>
Co-authored-by: Claude <[email protected]>
---
components/camel-opentelemetry2/pom.xml | 9 +
.../OpenTelemetryTracerConfigurer.java | 6 +
.../src/main/docs/opentelemetry2.adoc | 58 ++++
.../opentelemetry2/OpenTelemetryDevConsole.java | 5 +
.../camel/opentelemetry2/OpenTelemetryTracer.java | 49 ++++
.../camel/opentelemetry2/OtlpProtobufSpanData.java | 280 +++++++++++++++++++
.../opentelemetry2/OtlpReceiverProcessor.java | 68 +++++
.../opentelemetry2/OtlpProtobufSpanDataTest.java | 303 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_21.adoc | 20 ++
.../pages/jbang-commands/camel-jbang-debug.adoc | 4 +-
.../ROOT/pages/jbang-commands/camel-jbang-dev.adoc | 4 +-
.../ROOT/pages/jbang-commands/camel-jbang-run.adoc | 4 +-
.../META-INF/camel-jbang-commands-metadata.json | 6 +-
.../apache/camel/dsl/jbang/core/commands/Run.java | 49 +++-
.../dsl/jbang/core/commands/tui/ActionsPopup.java | 70 +++--
.../dsl/jbang/core/commands/tui/CamelMonitor.java | 29 +-
.../dsl/jbang/core/commands/tui/ProcessTab.java | 252 +++++++++++++++++
.../jbang/core/commands/tui/RunOptionsForm.java | 56 +++-
.../dsl/jbang/core/commands/tui/SpanEntry.java | 6 +
.../dsl/jbang/core/commands/tui/SpansTab.java | 45 ++-
.../dsl/jbang/core/commands/tui/TuiMcpServer.java | 2 +-
parent/pom.xml | 6 +
.../services/JaegerLocalContainerInfraService.java | 16 +-
23 files changed, 1292 insertions(+), 55 deletions(-)
diff --git a/components/camel-opentelemetry2/pom.xml
b/components/camel-opentelemetry2/pom.xml
index 94b298e19c90..1c0e8f6dda5e 100644
--- a/components/camel-opentelemetry2/pom.xml
+++ b/components/camel-opentelemetry2/pom.xml
@@ -54,10 +54,19 @@
<groupId>org.apache.camel</groupId>
<artifactId>camel-telemetry</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-core-model</artifactId>
+ </dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
</dependency>
+ <dependency>
+ <groupId>io.opentelemetry.proto</groupId>
+ <artifactId>opentelemetry-proto</artifactId>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-test-spring-junit6</artifactId>
diff --git
a/components/camel-opentelemetry2/src/generated/java/org/apache/camel/opentelemetry2/OpenTelemetryTracerConfigurer.java
b/components/camel-opentelemetry2/src/generated/java/org/apache/camel/opentelemetry2/OpenTelemetryTracerConfigurer.java
index 98722d317372..fddc7e80639a 100644
---
a/components/camel-opentelemetry2/src/generated/java/org/apache/camel/opentelemetry2/OpenTelemetryTracerConfigurer.java
+++
b/components/camel-opentelemetry2/src/generated/java/org/apache/camel/opentelemetry2/OpenTelemetryTracerConfigurer.java
@@ -29,6 +29,8 @@ public class OpenTelemetryTracerConfigurer extends
org.apache.camel.support.comp
case "disableCoreProcessors":
target.setDisableCoreProcessors(property(camelContext, boolean.class, value));
return true;
case "excludepatterns":
case "excludePatterns":
target.setExcludePatterns(property(camelContext, java.lang.String.class,
value)); return true;
+ case "exporttarget":
+ case "exportTarget": target.setExportTarget(property(camelContext,
java.lang.String.class, value)); return true;
case "includepatterns":
case "includePatterns":
target.setIncludePatterns(property(camelContext, java.lang.String.class,
value)); return true;
case "spanlifecyclemanager":
@@ -50,6 +52,8 @@ public class OpenTelemetryTracerConfigurer extends
org.apache.camel.support.comp
case "disableCoreProcessors": return boolean.class;
case "excludepatterns":
case "excludePatterns": return java.lang.String.class;
+ case "exporttarget":
+ case "exportTarget": return java.lang.String.class;
case "includepatterns":
case "includePatterns": return java.lang.String.class;
case "spanlifecyclemanager":
@@ -72,6 +76,8 @@ public class OpenTelemetryTracerConfigurer extends
org.apache.camel.support.comp
case "disableCoreProcessors": return target.isDisableCoreProcessors();
case "excludepatterns":
case "excludePatterns": return target.getExcludePatterns();
+ case "exporttarget":
+ case "exportTarget": return target.getExportTarget();
case "includepatterns":
case "includePatterns": return target.getIncludePatterns();
case "spanlifecyclemanager":
diff --git a/components/camel-opentelemetry2/src/main/docs/opentelemetry2.adoc
b/components/camel-opentelemetry2/src/main/docs/opentelemetry2.adoc
index 9179c7bb1547..f87baf46f3b6 100644
--- a/components/camel-opentelemetry2/src/main/docs/opentelemetry2.adoc
+++ b/components/camel-opentelemetry2/src/main/docs/opentelemetry2.adoc
@@ -30,6 +30,7 @@ The configuration properties for the OpenTelemetry2 tracer
are:
| `excludePatterns` | | A comma-separated list of patterns (e.g.,
`log*,direct*,setBody*`) to exclude from tracing. Spans matching these patterns
will be disabled. If nothing is specified, no processors are excluded by
default.
| `includePatterns` | | A comma-separated list of patterns (e.g.,
`log*,direct*,setBody*`) to explicitly include in a trace. Spans matching these
patterns will be enabled. If nothing is specified, all processors are included
by default.
| `traceHeadersInclusion`| `false` | If set to `true`, adds the generated
telemetry `CAMEL_TRACE_ID` and `CAMEL_SPAN_ID` Exchange headers.
+| `exportTarget` | | Where to export traces when using the OpenTelemetry Java
Agent: `tui` (embedded OTLP receiver, default) or `jaeger` (external Jaeger
collector).
|=======================================================================
=== Using with Standalone Camel
@@ -89,6 +90,63 @@ NOTE: The in-memory exporter is only activated when the
Camel profile is `dev` a
OTel `Tracer` bean is registered. For production deployments, configure a
proper span exporter
(OTLP, Jaeger, Zipkin, etc.).
+=== Camel JBang with OpenTelemetry Java Agent
+
+When running with Camel JBang, you can use the `--open-telemetry-agent` flag
to automatically attach the
+https://opentelemetry.io/docs/zero-code/java/agent/[OpenTelemetry Java Agent]
to your application.
+This provides deep instrumentation beyond Camel routes, including JVM metrics,
HTTP clients, database
+drivers, and other libraries — all with zero configuration and no external
collector required.
+
+[source,bash]
+----
+camel run my-route.yaml --open-telemetry-agent
+----
+
+This single flag handles the entire setup:
+
+- Downloads and attaches the OpenTelemetry Java Agent to the JVM
+- Adds `camel-opentelemetry2` and `camel-platform-http-main` as dependencies
+- Starts an embedded OTLP receiver on the Vert.x HTTP server (at `/v1/traces`)
+- Configures the agent to export traces to the embedded receiver
+- Starts an HTTP server on port 8080 (if not already specified)
+
+The agent sends protobuf-encoded spans to the embedded OTLP receiver, which
parses them and feeds
+them into the in-memory `DevSpanExporter`. The captured spans are then visible
in the Camel TUI
+under the OTel Spans tab, or through the dev console.
+
+==== Exporting to Jaeger
+
+Instead of viewing traces in the TUI, you can export them to
https://www.jaegertracing.io/[Jaeger]
+for a full tracing UI experience. Start Jaeger first, then run your
application with the `jaeger` export target:
+
+[source,bash]
+----
+camel infra run jaeger
+camel run my-route.yaml --open-telemetry-agent
--open-telemetry-agent-export=jaeger
+----
+
+This configures the Java Agent to export traces directly to Jaeger's OTLP
endpoint on port 4318.
+No embedded OTLP receiver is started — the agent handles all export. Open the
Jaeger UI at
+`http://localhost:16686` to browse traces for the `camel` service.
+
+==== When to use `--observe` vs `--open-telemetry-agent` vs Jaeger
+
+All three modes enable tracing during development, but they cover different
scopes:
+
+[width="100%",cols="25%,25%,25%,25%",options="header"]
+|=======================================================================
+| | `--observe` | `--open-telemetry-agent` | `--open-telemetry-agent` + Jaeger
+| *Instrumentation* | Camel routes only | Camel + JVM, HTTP, DB, etc. | Camel
+ JVM, HTTP, DB, etc.
+| *Setup* | Built-in SDK | Downloads OTel Java Agent | Downloads OTel Java
Agent
+| *Collector* | Not needed | Not needed (embedded receiver) | Jaeger (via
`camel infra run jaeger`)
+| *View traces* | TUI / dev console | TUI / dev console | Jaeger UI
(localhost:16686)
+| *Use case* | Quick Camel route debugging | Full-stack observability in TUI |
Full tracing UI with search and timeline
+|=======================================================================
+
+NOTE: The embedded OTLP receiver is only activated in `dev` profile mode and
is intended for local
+development. For production deployments, configure the OpenTelemetry Java
Agent to export to an
+external collector such as Jaeger or an OpenTelemetry Collector.
+
[[OpenTelemetry-JavaAgent]]
=== Using the OpenTelemetry Java Agent
diff --git
a/components/camel-opentelemetry2/src/main/java/org/apache/camel/opentelemetry2/OpenTelemetryDevConsole.java
b/components/camel-opentelemetry2/src/main/java/org/apache/camel/opentelemetry2/OpenTelemetryDevConsole.java
index d84fd020c850..6cdfa8a33af9 100644
---
a/components/camel-opentelemetry2/src/main/java/org/apache/camel/opentelemetry2/OpenTelemetryDevConsole.java
+++
b/components/camel-opentelemetry2/src/main/java/org/apache/camel/opentelemetry2/OpenTelemetryDevConsole.java
@@ -165,6 +165,11 @@ public class OpenTelemetryDevConsole extends
AbstractDevConsole {
jo.put("endEpochNanos", span.getEndEpochNanos());
jo.put("durationMs", (span.getEndEpochNanos() -
span.getStartEpochNanos()) / 1_000_000);
+ String scopeName = span.getInstrumentationScopeInfo().getName();
+ if (scopeName != null && !scopeName.isEmpty()) {
+ jo.put("scopeName", scopeName);
+ }
+
JsonObject attrs = new JsonObject();
span.getAttributes().forEach((key, value) -> attrs.put(key.getKey(),
value));
if (!attrs.isEmpty()) {
diff --git
a/components/camel-opentelemetry2/src/main/java/org/apache/camel/opentelemetry2/OpenTelemetryTracer.java
b/components/camel-opentelemetry2/src/main/java/org/apache/camel/opentelemetry2/OpenTelemetryTracer.java
index 3996e8a24ad6..0c6db886484c 100644
---
a/components/camel-opentelemetry2/src/main/java/org/apache/camel/opentelemetry2/OpenTelemetryTracer.java
+++
b/components/camel-opentelemetry2/src/main/java/org/apache/camel/opentelemetry2/OpenTelemetryTracer.java
@@ -16,6 +16,7 @@
*/
package org.apache.camel.opentelemetry2;
+import java.lang.management.ManagementFactory;
import java.util.Iterator;
import java.util.Map;
@@ -33,6 +34,7 @@ import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.api.management.ManagedResource;
+import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.spi.Configurer;
import org.apache.camel.spi.InterceptStrategy;
import org.apache.camel.spi.annotations.JdkService;
@@ -54,6 +56,7 @@ public class OpenTelemetryTracer extends
org.apache.camel.telemetry.Tracer {
private Tracer tracer;
private ContextPropagators contextPropagators;
private OpenTelemetrySdk devSdk;
+ private String exportTarget;
@Override
protected void initTracer() {
@@ -91,6 +94,15 @@ public class OpenTelemetryTracer extends
org.apache.camel.telemetry.Tracer {
}
private void initDevSpanExporter() {
+ if (isOpenTelemetryAgentPresent()) {
+ if ("jaeger".equals(exportTarget)) {
+ LOG.info("OpenTelemetry Java Agent detected, exporting traces
to Jaeger");
+ return;
+ }
+ LOG.info("OpenTelemetry Java Agent detected, using embedded OTLP
receiver");
+ initOtlpReceiver();
+ return;
+ }
DevSpanExporter exporter = new DevSpanExporter();
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(SimpleSpanProcessor.create(exporter))
@@ -99,12 +111,41 @@ public class OpenTelemetryTracer extends
org.apache.camel.telemetry.Tracer {
.setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
.setTracerProvider(tracerProvider)
.build();
+ GlobalOpenTelemetry.set(devSdk);
this.tracer = devSdk.getTracer("camel");
this.contextPropagators = devSdk.getPropagators();
getCamelContext().getRegistry().bind("DevSpanExporter", exporter);
LOG.info("OpenTelemetry in-memory span exporter enabled (dev
profile)");
}
+ private void initOtlpReceiver() {
+ DevSpanExporter exporter = new DevSpanExporter();
+ getCamelContext().getRegistry().bind("DevSpanExporter", exporter);
+
+ // exclude the receiver route from tracing to avoid self-tracing loop
+ String ep = getExcludePatterns();
+ setExcludePatterns(ep != null ? ep + ",platform-http:/v1/traces*" :
"platform-http:/v1/traces*");
+
+ try {
+ getCamelContext().addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("platform-http:/v1/traces?httpMethodRestrict=POST")
+ .routeId("otlp-receiver")
+ .process(new OtlpReceiverProcessor(exporter));
+ }
+ });
+ LOG.info("Embedded OTLP receiver enabled on /v1/traces for Java
Agent span collection");
+ } catch (Exception e) {
+ LOG.warn("Failed to start embedded OTLP receiver: {}",
e.getMessage());
+ }
+ }
+
+ private boolean isOpenTelemetryAgentPresent() {
+ return
ManagementFactory.getRuntimeMXBean().getInputArguments().stream()
+ .anyMatch(arg -> arg.startsWith("-javaagent") &&
arg.contains("opentelemetry"));
+ }
+
void setTracer(Tracer tracer) {
this.tracer = tracer;
}
@@ -113,6 +154,14 @@ public class OpenTelemetryTracer extends
org.apache.camel.telemetry.Tracer {
this.contextPropagators = cp;
}
+ public String getExportTarget() {
+ return exportTarget;
+ }
+
+ public void setExportTarget(String exportTarget) {
+ this.exportTarget = exportTarget;
+ }
+
@Override
protected void doStart() throws Exception {
super.doStart();
diff --git
a/components/camel-opentelemetry2/src/main/java/org/apache/camel/opentelemetry2/OtlpProtobufSpanData.java
b/components/camel-opentelemetry2/src/main/java/org/apache/camel/opentelemetry2/OtlpProtobufSpanData.java
new file mode 100644
index 000000000000..31d7f62413a4
--- /dev/null
+++
b/components/camel-opentelemetry2/src/main/java/org/apache/camel/opentelemetry2/OtlpProtobufSpanData.java
@@ -0,0 +1,280 @@
+/*
+ * 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.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import com.google.protobuf.ByteString;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.common.AttributesBuilder;
+import io.opentelemetry.api.trace.SpanContext;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.api.trace.StatusCode;
+import io.opentelemetry.api.trace.TraceFlags;
+import io.opentelemetry.api.trace.TraceState;
+import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest;
+import io.opentelemetry.proto.common.v1.AnyValue;
+import io.opentelemetry.proto.common.v1.InstrumentationScope;
+import io.opentelemetry.proto.common.v1.KeyValue;
+import io.opentelemetry.proto.trace.v1.ResourceSpans;
+import io.opentelemetry.proto.trace.v1.ScopeSpans;
+import io.opentelemetry.proto.trace.v1.Span;
+import io.opentelemetry.proto.trace.v1.Status;
+import io.opentelemetry.sdk.common.InstrumentationLibraryInfo;
+import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
+import io.opentelemetry.sdk.resources.Resource;
+import io.opentelemetry.sdk.trace.data.EventData;
+import io.opentelemetry.sdk.trace.data.LinkData;
+import io.opentelemetry.sdk.trace.data.SpanData;
+import io.opentelemetry.sdk.trace.data.StatusData;
+
+/**
+ * Lightweight {@link SpanData} adapter that wraps an OTLP protobuf span. Used
by the embedded OTLP receiver to feed
+ * spans from the OpenTelemetry Java Agent into {@link DevSpanExporter}.
+ */
+final class OtlpProtobufSpanData implements SpanData {
+
+ private final SpanContext spanContext;
+ private final SpanContext parentSpanContext;
+ private final String name;
+ private final SpanKind kind;
+ private final StatusData status;
+ private final long startEpochNanos;
+ private final long endEpochNanos;
+ private final Attributes attributes;
+ private final InstrumentationScopeInfo scopeInfo;
+ private final Resource resource;
+
+ private OtlpProtobufSpanData(SpanContext spanContext, SpanContext
parentSpanContext, String name,
+ SpanKind kind, StatusData status, long
startEpochNanos, long endEpochNanos,
+ Attributes attributes,
InstrumentationScopeInfo scopeInfo, Resource resource) {
+ this.spanContext = spanContext;
+ this.parentSpanContext = parentSpanContext;
+ this.name = name;
+ this.kind = kind;
+ this.status = status;
+ this.startEpochNanos = startEpochNanos;
+ this.endEpochNanos = endEpochNanos;
+ this.attributes = attributes;
+ this.scopeInfo = scopeInfo;
+ this.resource = resource;
+ }
+
+ static List<SpanData> fromProtobuf(byte[] body) throws Exception {
+ ExportTraceServiceRequest request =
ExportTraceServiceRequest.parseFrom(body);
+ List<SpanData> result = new ArrayList<>();
+
+ for (ResourceSpans rs : request.getResourceSpansList()) {
+ Resource resource = parseResource(rs.getResource());
+ for (ScopeSpans ss : rs.getScopeSpansList()) {
+ InstrumentationScopeInfo scopeInfo = parseScope(ss.getScope());
+ for (Span span : ss.getSpansList()) {
+ result.add(fromSpan(span, resource, scopeInfo));
+ }
+ }
+ }
+ return result;
+ }
+
+ private static OtlpProtobufSpanData fromSpan(
+ Span span, Resource resource, InstrumentationScopeInfo scopeInfo) {
+
+ String traceId = hex(span.getTraceId());
+ String spanId = hex(span.getSpanId());
+ String parentSpanId = hex(span.getParentSpanId());
+
+ SpanContext sc = SpanContext.create(traceId, spanId,
TraceFlags.getSampled(), TraceState.getDefault());
+ SpanContext parentSc;
+ if (parentSpanId.isEmpty()) {
+ parentSc = SpanContext.getInvalid();
+ } else {
+ parentSc = SpanContext.create(traceId, parentSpanId,
TraceFlags.getSampled(), TraceState.getDefault());
+ }
+
+ return new OtlpProtobufSpanData(
+ sc, parentSc, span.getName(),
+ toSpanKind(span.getKind()),
+ toStatusData(span.getStatus()),
+ span.getStartTimeUnixNano(),
+ span.getEndTimeUnixNano(),
+ toAttributes(span.getAttributesList()),
+ scopeInfo, resource);
+ }
+
+ private static Resource
parseResource(io.opentelemetry.proto.resource.v1.Resource res) {
+ if (res == null) {
+ return Resource.getDefault();
+ }
+ return Resource.create(toAttributes(res.getAttributesList()));
+ }
+
+ private static InstrumentationScopeInfo parseScope(InstrumentationScope
scope) {
+ if (scope == null) {
+ return InstrumentationScopeInfo.create("unknown");
+ }
+ String name = scope.getName().isEmpty() ? "unknown" : scope.getName();
+ String version = scope.getVersion().isEmpty() ? null :
scope.getVersion();
+ if (version != null) {
+ return
InstrumentationScopeInfo.builder(name).setVersion(version).build();
+ }
+ return InstrumentationScopeInfo.create(name);
+ }
+
+ private static SpanKind toSpanKind(Span.SpanKind kind) {
+ return switch (kind) {
+ case SPAN_KIND_SERVER -> SpanKind.SERVER;
+ case SPAN_KIND_CLIENT -> SpanKind.CLIENT;
+ case SPAN_KIND_PRODUCER -> SpanKind.PRODUCER;
+ case SPAN_KIND_CONSUMER -> SpanKind.CONSUMER;
+ default -> SpanKind.INTERNAL;
+ };
+ }
+
+ private static StatusData toStatusData(Status status) {
+ if (status == null) {
+ return StatusData.unset();
+ }
+ return switch (status.getCode()) {
+ case STATUS_CODE_OK -> StatusData.ok();
+ case STATUS_CODE_ERROR -> StatusData.create(StatusCode.ERROR,
status.getMessage());
+ default -> StatusData.unset();
+ };
+ }
+
+ private static Attributes toAttributes(List<KeyValue> kvs) {
+ if (kvs == null || kvs.isEmpty()) {
+ return Attributes.empty();
+ }
+ AttributesBuilder ab = Attributes.builder();
+ for (KeyValue kv : kvs) {
+ String key = kv.getKey();
+ AnyValue value = kv.getValue();
+ if (value.hasStringValue()) {
+ ab.put(AttributeKey.stringKey(key), value.getStringValue());
+ } else if (value.hasIntValue()) {
+ ab.put(AttributeKey.longKey(key), value.getIntValue());
+ } else if (value.hasDoubleValue()) {
+ ab.put(AttributeKey.doubleKey(key), value.getDoubleValue());
+ } else if (value.hasBoolValue()) {
+ ab.put(AttributeKey.booleanKey(key), value.getBoolValue());
+ }
+ }
+ return ab.build();
+ }
+
+ private static String hex(ByteString bytes) {
+ if (bytes == null || bytes.isEmpty()) {
+ return "";
+ }
+ StringBuilder sb = new StringBuilder(bytes.size() * 2);
+ for (int i = 0; i < bytes.size(); i++) {
+ int b = bytes.byteAt(i) & 0xFF;
+ sb.append(Character.forDigit(b >>> 4, 16));
+ sb.append(Character.forDigit(b & 0x0F, 16));
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public SpanContext getSpanContext() {
+ return spanContext;
+ }
+
+ @Override
+ public SpanContext getParentSpanContext() {
+ return parentSpanContext;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public SpanKind getKind() {
+ return kind;
+ }
+
+ @Override
+ public StatusData getStatus() {
+ return status;
+ }
+
+ @Override
+ public long getStartEpochNanos() {
+ return startEpochNanos;
+ }
+
+ @Override
+ public long getEndEpochNanos() {
+ return endEpochNanos;
+ }
+
+ @Override
+ public Attributes getAttributes() {
+ return attributes;
+ }
+
+ @Override
+ public List<EventData> getEvents() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List<LinkData> getLinks() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public boolean hasEnded() {
+ return true;
+ }
+
+ @Override
+ public int getTotalRecordedEvents() {
+ return 0;
+ }
+
+ @Override
+ public int getTotalRecordedLinks() {
+ return 0;
+ }
+
+ @Override
+ public int getTotalAttributeCount() {
+ return attributes.size();
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public InstrumentationLibraryInfo getInstrumentationLibraryInfo() {
+ return InstrumentationLibraryInfo.create(scopeInfo.getName(),
scopeInfo.getVersion());
+ }
+
+ @Override
+ public InstrumentationScopeInfo getInstrumentationScopeInfo() {
+ return scopeInfo;
+ }
+
+ @Override
+ public Resource getResource() {
+ return resource;
+ }
+}
diff --git
a/components/camel-opentelemetry2/src/main/java/org/apache/camel/opentelemetry2/OtlpReceiverProcessor.java
b/components/camel-opentelemetry2/src/main/java/org/apache/camel/opentelemetry2/OtlpReceiverProcessor.java
new file mode 100644
index 000000000000..85508d7956f3
--- /dev/null
+++
b/components/camel-opentelemetry2/src/main/java/org/apache/camel/opentelemetry2/OtlpReceiverProcessor.java
@@ -0,0 +1,68 @@
+/*
+ * 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.util.List;
+
+import io.opentelemetry.sdk.trace.data.SpanData;
+import org.apache.camel.Exchange;
+import org.apache.camel.Processor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Processes incoming OTLP protobuf trace export requests and feeds parsed
spans into the {@link DevSpanExporter}.
+ */
+final class OtlpReceiverProcessor implements Processor {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(OtlpReceiverProcessor.class);
+
+ private final DevSpanExporter exporter;
+
+ OtlpReceiverProcessor(DevSpanExporter exporter) {
+ this.exporter = exporter;
+ }
+
+ @Override
+ public void process(Exchange exchange) throws Exception {
+ byte[] body = exchange.getMessage().getBody(byte[].class);
+ if (body == null || body.length == 0) {
+ exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 200);
+ exchange.getMessage().setBody(new byte[0]);
+ return;
+ }
+
+ try {
+ List<SpanData> spans = OtlpProtobufSpanData.fromProtobuf(body);
+ // filter out spans for the OTLP receiver itself to avoid
self-tracing noise
+ spans = spans.stream()
+ .filter(s -> !s.getName().contains("/v1/traces"))
+ .toList();
+ if (!spans.isEmpty()) {
+ exporter.export(spans);
+ if (LOG.isTraceEnabled()) {
+ LOG.trace("Received {} spans from OTLP exporter",
spans.size());
+ }
+ }
+ } catch (Exception e) {
+ LOG.debug("Failed to parse OTLP protobuf: {}", e.getMessage());
+ }
+
+ exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 200);
+ exchange.getMessage().setBody(new byte[0]);
+ }
+}
diff --git
a/components/camel-opentelemetry2/src/test/java/org/apache/camel/opentelemetry2/OtlpProtobufSpanDataTest.java
b/components/camel-opentelemetry2/src/test/java/org/apache/camel/opentelemetry2/OtlpProtobufSpanDataTest.java
new file mode 100644
index 000000000000..1418193c2608
--- /dev/null
+++
b/components/camel-opentelemetry2/src/test/java/org/apache/camel/opentelemetry2/OtlpProtobufSpanDataTest.java
@@ -0,0 +1,303 @@
+/*
+ * 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.util.List;
+
+import com.google.protobuf.ByteString;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.api.trace.StatusCode;
+import io.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest;
+import io.opentelemetry.proto.common.v1.AnyValue;
+import io.opentelemetry.proto.common.v1.InstrumentationScope;
+import io.opentelemetry.proto.common.v1.KeyValue;
+import io.opentelemetry.proto.resource.v1.Resource;
+import io.opentelemetry.proto.trace.v1.ResourceSpans;
+import io.opentelemetry.proto.trace.v1.ScopeSpans;
+import io.opentelemetry.proto.trace.v1.Span;
+import io.opentelemetry.proto.trace.v1.Status;
+import io.opentelemetry.sdk.trace.data.SpanData;
+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.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class OtlpProtobufSpanDataTest {
+
+ @Test
+ void testFromProtobufBasicSpan() throws Exception {
+ byte[] traceIdBytes = hexToBytes("0af7651916cd43dd8448eb211c80319c");
+ byte[] spanIdBytes = hexToBytes("00f067aa0ba902b7");
+ byte[] parentSpanIdBytes = hexToBytes("b7ad6b7169203331");
+
+ Span span = Span.newBuilder()
+ .setTraceId(ByteString.copyFrom(traceIdBytes))
+ .setSpanId(ByteString.copyFrom(spanIdBytes))
+ .setParentSpanId(ByteString.copyFrom(parentSpanIdBytes))
+ .setName("GET /api/users")
+ .setKind(Span.SpanKind.SPAN_KIND_SERVER)
+ .setStartTimeUnixNano(1_000_000_000L)
+ .setEndTimeUnixNano(2_000_000_000L)
+
.setStatus(Status.newBuilder().setCode(Status.StatusCode.STATUS_CODE_OK).build())
+ .build();
+
+ ExportTraceServiceRequest request = buildRequest("camel", "1.0.0",
span);
+ List<SpanData> spans =
OtlpProtobufSpanData.fromProtobuf(request.toByteArray());
+
+ assertEquals(1, spans.size());
+ SpanData sd = spans.get(0);
+ assertEquals("GET /api/users", sd.getName());
+ assertEquals(SpanKind.SERVER, sd.getKind());
+ assertEquals("0af7651916cd43dd8448eb211c80319c",
sd.getSpanContext().getTraceId());
+ assertEquals("00f067aa0ba902b7", sd.getSpanContext().getSpanId());
+ assertEquals("0af7651916cd43dd8448eb211c80319c",
sd.getParentSpanContext().getTraceId());
+ assertEquals("b7ad6b7169203331",
sd.getParentSpanContext().getSpanId());
+ assertEquals(1_000_000_000L, sd.getStartEpochNanos());
+ assertEquals(2_000_000_000L, sd.getEndEpochNanos());
+ assertEquals(StatusCode.OK, sd.getStatus().getStatusCode());
+ assertTrue(sd.hasEnded());
+ }
+
+ @Test
+ void testFromProtobufScopeInfo() throws Exception {
+ Span span = buildSimpleSpan("test-span");
+ ExportTraceServiceRequest request =
buildRequest("io.opentelemetry.jdk-http-client", "2.12.0", span);
+
+ List<SpanData> spans =
OtlpProtobufSpanData.fromProtobuf(request.toByteArray());
+
+ assertEquals(1, spans.size());
+ assertEquals("io.opentelemetry.jdk-http-client",
spans.get(0).getInstrumentationScopeInfo().getName());
+ assertEquals("2.12.0",
spans.get(0).getInstrumentationScopeInfo().getVersion());
+ }
+
+ @Test
+ void testFromProtobufCamelScope() throws Exception {
+ Span span = buildSimpleSpan("camel-route");
+ ExportTraceServiceRequest request = buildRequest("camel", null, span);
+
+ List<SpanData> spans =
OtlpProtobufSpanData.fromProtobuf(request.toByteArray());
+
+ assertEquals(1, spans.size());
+ assertEquals("camel",
spans.get(0).getInstrumentationScopeInfo().getName());
+ }
+
+ @Test
+ void testFromProtobufAttributes() throws Exception {
+ Span span = Span.newBuilder()
+
.setTraceId(ByteString.copyFrom(hexToBytes("0af7651916cd43dd8448eb211c80319c")))
+ .setSpanId(ByteString.copyFrom(hexToBytes("00f067aa0ba902b7")))
+ .setName("test")
+ .addAttributes(stringAttr("http.method", "GET"))
+ .addAttributes(longAttr("http.status_code", 200))
+ .addAttributes(doubleAttr("response.time", 1.5))
+ .addAttributes(boolAttr("error", false))
+ .build();
+
+ ExportTraceServiceRequest request = buildRequest("camel", null, span);
+ List<SpanData> spans =
OtlpProtobufSpanData.fromProtobuf(request.toByteArray());
+
+ SpanData sd = spans.get(0);
+ assertEquals("GET",
sd.getAttributes().get(AttributeKey.stringKey("http.method")));
+ assertEquals(200L,
sd.getAttributes().get(AttributeKey.longKey("http.status_code")));
+ assertEquals(1.5,
sd.getAttributes().get(AttributeKey.doubleKey("response.time")));
+ assertEquals(false,
sd.getAttributes().get(AttributeKey.booleanKey("error")));
+ }
+
+ @Test
+ void testFromProtobufSpanKindMapping() throws Exception {
+ for (var entry : List.of(
+ new Object[] { Span.SpanKind.SPAN_KIND_CLIENT, SpanKind.CLIENT
},
+ new Object[] { Span.SpanKind.SPAN_KIND_SERVER, SpanKind.SERVER
},
+ new Object[] { Span.SpanKind.SPAN_KIND_PRODUCER,
SpanKind.PRODUCER },
+ new Object[] { Span.SpanKind.SPAN_KIND_CONSUMER,
SpanKind.CONSUMER },
+ new Object[] { Span.SpanKind.SPAN_KIND_INTERNAL,
SpanKind.INTERNAL })) {
+
+ Span span = Span.newBuilder()
+
.setTraceId(ByteString.copyFrom(hexToBytes("0af7651916cd43dd8448eb211c80319c")))
+
.setSpanId(ByteString.copyFrom(hexToBytes("00f067aa0ba902b7")))
+ .setName("test")
+ .setKind((Span.SpanKind) entry[0])
+ .build();
+
+ ExportTraceServiceRequest request = buildRequest("camel", null,
span);
+ List<SpanData> spans =
OtlpProtobufSpanData.fromProtobuf(request.toByteArray());
+ assertEquals(entry[1], spans.get(0).getKind(), "Kind mapping
failed for " + entry[0]);
+ }
+ }
+
+ @Test
+ void testFromProtobufErrorStatus() throws Exception {
+ Span span = Span.newBuilder()
+
.setTraceId(ByteString.copyFrom(hexToBytes("0af7651916cd43dd8448eb211c80319c")))
+ .setSpanId(ByteString.copyFrom(hexToBytes("00f067aa0ba902b7")))
+ .setName("failing-span")
+ .setStatus(Status.newBuilder()
+ .setCode(Status.StatusCode.STATUS_CODE_ERROR)
+ .setMessage("Connection refused")
+ .build())
+ .build();
+
+ ExportTraceServiceRequest request = buildRequest("camel", null, span);
+ List<SpanData> spans =
OtlpProtobufSpanData.fromProtobuf(request.toByteArray());
+
+ assertEquals(StatusCode.ERROR,
spans.get(0).getStatus().getStatusCode());
+ assertEquals("Connection refused",
spans.get(0).getStatus().getDescription());
+ }
+
+ @Test
+ void testFromProtobufRootSpan() throws Exception {
+ Span span = Span.newBuilder()
+
.setTraceId(ByteString.copyFrom(hexToBytes("0af7651916cd43dd8448eb211c80319c")))
+ .setSpanId(ByteString.copyFrom(hexToBytes("00f067aa0ba902b7")))
+ .setName("root")
+ .build();
+
+ ExportTraceServiceRequest request = buildRequest("camel", null, span);
+ List<SpanData> spans =
OtlpProtobufSpanData.fromProtobuf(request.toByteArray());
+
+ assertFalse(spans.get(0).getParentSpanContext().isValid());
+ }
+
+ @Test
+ void testFromProtobufMultipleSpans() throws Exception {
+ Span span1 = Span.newBuilder()
+
.setTraceId(ByteString.copyFrom(hexToBytes("0af7651916cd43dd8448eb211c80319c")))
+ .setSpanId(ByteString.copyFrom(hexToBytes("00f067aa0ba902b7")))
+ .setName("span-1")
+ .build();
+ Span span2 = Span.newBuilder()
+
.setTraceId(ByteString.copyFrom(hexToBytes("0af7651916cd43dd8448eb211c80319c")))
+ .setSpanId(ByteString.copyFrom(hexToBytes("b7ad6b7169203331")))
+ .setName("span-2")
+ .build();
+
+ ScopeSpans scopeSpans = ScopeSpans.newBuilder()
+
.setScope(InstrumentationScope.newBuilder().setName("camel").build())
+ .addSpans(span1)
+ .addSpans(span2)
+ .build();
+
+ ExportTraceServiceRequest request =
ExportTraceServiceRequest.newBuilder()
+ .addResourceSpans(ResourceSpans.newBuilder()
+ .setResource(Resource.newBuilder().build())
+ .addScopeSpans(scopeSpans)
+ .build())
+ .build();
+
+ List<SpanData> spans =
OtlpProtobufSpanData.fromProtobuf(request.toByteArray());
+ assertEquals(2, spans.size());
+ assertEquals("span-1", spans.get(0).getName());
+ assertEquals("span-2", spans.get(1).getName());
+ }
+
+ @Test
+ void testFromProtobufResourceAttributes() throws Exception {
+ Span span = buildSimpleSpan("test");
+ ExportTraceServiceRequest request =
ExportTraceServiceRequest.newBuilder()
+ .addResourceSpans(ResourceSpans.newBuilder()
+ .setResource(Resource.newBuilder()
+ .addAttributes(stringAttr("service.name",
"my-camel-app"))
+ .build())
+ .addScopeSpans(ScopeSpans.newBuilder()
+
.setScope(InstrumentationScope.newBuilder().setName("camel").build())
+ .addSpans(span)
+ .build())
+ .build())
+ .build();
+
+ List<SpanData> spans =
OtlpProtobufSpanData.fromProtobuf(request.toByteArray());
+ assertNotNull(spans.get(0).getResource());
+ assertEquals("my-camel-app",
+
spans.get(0).getResource().getAttributes().get(AttributeKey.stringKey("service.name")));
+ }
+
+ @Test
+ void testSpanDataDefaults() throws Exception {
+ Span span = buildSimpleSpan("test");
+ ExportTraceServiceRequest request = buildRequest("camel", null, span);
+ List<SpanData> spans =
OtlpProtobufSpanData.fromProtobuf(request.toByteArray());
+
+ SpanData sd = spans.get(0);
+ assertTrue(sd.getEvents().isEmpty());
+ assertTrue(sd.getLinks().isEmpty());
+ assertEquals(0, sd.getTotalRecordedEvents());
+ assertEquals(0, sd.getTotalRecordedLinks());
+ }
+
+ private static Span buildSimpleSpan(String name) {
+ return Span.newBuilder()
+
.setTraceId(ByteString.copyFrom(hexToBytes("0af7651916cd43dd8448eb211c80319c")))
+ .setSpanId(ByteString.copyFrom(hexToBytes("00f067aa0ba902b7")))
+ .setName(name)
+ .build();
+ }
+
+ private static ExportTraceServiceRequest buildRequest(String scopeName,
String scopeVersion, Span span) {
+ InstrumentationScope.Builder scopeBuilder =
InstrumentationScope.newBuilder().setName(scopeName);
+ if (scopeVersion != null) {
+ scopeBuilder.setVersion(scopeVersion);
+ }
+ return ExportTraceServiceRequest.newBuilder()
+ .addResourceSpans(ResourceSpans.newBuilder()
+ .setResource(Resource.newBuilder().build())
+ .addScopeSpans(ScopeSpans.newBuilder()
+ .setScope(scopeBuilder.build())
+ .addSpans(span)
+ .build())
+ .build())
+ .build();
+ }
+
+ private static KeyValue stringAttr(String key, String value) {
+ return KeyValue.newBuilder()
+ .setKey(key)
+ .setValue(AnyValue.newBuilder().setStringValue(value).build())
+ .build();
+ }
+
+ private static KeyValue longAttr(String key, long value) {
+ return KeyValue.newBuilder()
+ .setKey(key)
+ .setValue(AnyValue.newBuilder().setIntValue(value).build())
+ .build();
+ }
+
+ private static KeyValue doubleAttr(String key, double value) {
+ return KeyValue.newBuilder()
+ .setKey(key)
+ .setValue(AnyValue.newBuilder().setDoubleValue(value).build())
+ .build();
+ }
+
+ private static KeyValue boolAttr(String key, boolean value) {
+ return KeyValue.newBuilder()
+ .setKey(key)
+ .setValue(AnyValue.newBuilder().setBoolValue(value).build())
+ .build();
+ }
+
+ private static byte[] hexToBytes(String hex) {
+ byte[] bytes = new byte[hex.length() / 2];
+ for (int i = 0; i < bytes.length; i++) {
+ bytes[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 +
2), 16);
+ }
+ return bytes;
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
index 1f3a32cd01bf..732a9e42ad79 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_21.adoc
@@ -243,6 +243,26 @@ Camel version, the plugin GAV, or the effective
`--repos`/`--repo` value
changes. No user action is required; existing plugin entries are populated on
first use after upgrade.
+=== camel-opentelemetry2 — Embedded OTLP receiver for OpenTelemetry Java Agent
+
+A new `exportTarget` option has been added to `OpenTelemetryTracer`. When
using the OpenTelemetry Java Agent
+via Camel JBang, this option controls where spans are exported:
+
+* `tui` (default) — an embedded OTLP receiver is started inside the Camel
process; the agent exports spans
+ to this receiver, which feeds them into the TUI Spans tab. No external
collector is needed.
+* `jaeger` — the agent exports spans directly to a Jaeger instance (OTLP on
port 4318). No embedded
+ receiver is created.
+
+Two new CLI flags have been added to `camel run`, `camel dev`, and `camel
debug`:
+
+* `--open-telemetry-agent` — attaches the OpenTelemetry Java Agent to the
Camel process. The agent
+ auto-instruments HTTP clients, JDBC, Kafka clients, gRPC, and other
libraries.
+* `--open-telemetry-agent-export=<tui|jaeger>` — selects the export target
(default: `tui`).
+
+The `camel-opentelemetry2` module now has an additional `provided`-scope
dependency on
+`io.opentelemetry.proto:opentelemetry-proto` for the embedded OTLP receiver.
This dependency is not
+pulled transitively into applications using `camel-opentelemetry2` at runtime.
+
=== camel-yaml-dsl
A new canonical JSON Schema variant (`camelYamlDsl-canonical.json`) has been
added alongside the existing classic
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-debug.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-debug.adoc
index 9b1701b5cdd2..ee5772481702 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-debug.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-debug.adoc
@@ -63,8 +63,10 @@ camel debug [options]
| `--metrics` _(deprecated)_ | Deprecated: use --observe instead. Metrics
(Micrometer and Prometheus) at /q/metrics on local HTTP server (port 8080 by
default) | false | boolean
| `--modeline` | Whether to support JBang style //DEPS to specify additional
dependencies | true | boolean
| `--name` | The name of the Camel application | CamelJBang | String
-| `--observe` | Enable observability services | false | boolean
+| `--observe` | Enable observability services (health, metrics, dev console,
and lightweight Camel-only tracing in the TUI Spans tab) | false | boolean
| `--open-api` | Adds an OpenAPI spec from the given file (json or yaml file)
| | String
+| `--open-telemetry-agent` | Enable OpenTelemetry Java Agent for
auto-instrumentation of third-party libraries (HTTP clients, JDBC, Kafka
clients, gRPC, etc.). Traces are shown in the TUI Spans tab with Camel spans in
cyan and 3rd-party agent spans in magenta. | false | boolean
+| `--open-telemetry-agent-export` | Where to export OpenTelemetry Agent
traces: tui (embedded receiver in TUI) or jaeger (external Jaeger). With
jaeger, start Jaeger first via 'camel infra run jaeger' and view traces at
\http://localhost:16686 | tui | String
| `--output` | File to store the current message body (will override). This
allows for manual inspecting the message later. | | String
| `--package-scan-jars` | Whether to automatic package scan JARs for custom
Spring or Quarkus beans making them available for Camel JBang | false | boolean
| `--port` | Embeds a local HTTP server on this port (port 8080 by default;
use 0 to dynamic assign a free random port number) | | int
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-dev.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-dev.adoc
index 2d3e8e603461..6924e69eb390 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-dev.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-dev.adoc
@@ -59,8 +59,10 @@ camel dev [options]
| `--metrics` _(deprecated)_ | Deprecated: use --observe instead. Metrics
(Micrometer and Prometheus) at /q/metrics on local HTTP server (port 8080 by
default) | false | boolean
| `--modeline` | Whether to support JBang style //DEPS to specify additional
dependencies | true | boolean
| `--name` | The name of the Camel application | CamelJBang | String
-| `--observe` | Enable observability services | false | boolean
+| `--observe` | Enable observability services (health, metrics, dev console,
and lightweight Camel-only tracing in the TUI Spans tab) | false | boolean
| `--open-api` | Adds an OpenAPI spec from the given file (json or yaml file)
| | String
+| `--open-telemetry-agent` | Enable OpenTelemetry Java Agent for
auto-instrumentation of third-party libraries (HTTP clients, JDBC, Kafka
clients, gRPC, etc.). Traces are shown in the TUI Spans tab with Camel spans in
cyan and 3rd-party agent spans in magenta. | false | boolean
+| `--open-telemetry-agent-export` | Where to export OpenTelemetry Agent
traces: tui (embedded receiver in TUI) or jaeger (external Jaeger). With
jaeger, start Jaeger first via 'camel infra run jaeger' and view traces at
\http://localhost:16686 | tui | String
| `--package-scan-jars` | Whether to automatic package scan JARs for custom
Spring or Quarkus beans making them available for Camel JBang | false | boolean
| `--port` | Embeds a local HTTP server on this port (port 8080 by default;
use 0 to dynamic assign a free random port number) | | int
| `--profile` | Profile to run (dev, test, prod). | dev | String
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-run.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-run.adoc
index 18c48842fba5..90da88541a66 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-run.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-run.adoc
@@ -59,8 +59,10 @@ camel run [options]
| `--metrics` _(deprecated)_ | Deprecated: use --observe instead. Metrics
(Micrometer and Prometheus) at /q/metrics on local HTTP server (port 8080 by
default) | false | boolean
| `--modeline` | Whether to support JBang style //DEPS to specify additional
dependencies | true | boolean
| `--name` | The name of the Camel application | CamelJBang | String
-| `--observe` | Enable observability services | false | boolean
+| `--observe` | Enable observability services (health, metrics, dev console,
and lightweight Camel-only tracing in the TUI Spans tab) | false | boolean
| `--open-api` | Adds an OpenAPI spec from the given file (json or yaml file)
| | String
+| `--open-telemetry-agent` | Enable OpenTelemetry Java Agent for
auto-instrumentation of third-party libraries (HTTP clients, JDBC, Kafka
clients, gRPC, etc.). Traces are shown in the TUI Spans tab with Camel spans in
cyan and 3rd-party agent spans in magenta. | false | boolean
+| `--open-telemetry-agent-export` | Where to export OpenTelemetry Agent
traces: tui (embedded receiver in TUI) or jaeger (external Jaeger). With
jaeger, start Jaeger first via 'camel infra run jaeger' and view traces at
\http://localhost:16686 | tui | String
| `--package-scan-jars` | Whether to automatic package scan JARs for custom
Spring or Quarkus beans making them available for Camel JBang | false | boolean
| `--port` | Embeds a local HTTP server on this port (port 8080 by default;
use 0 to dynamic assign a free random port number) | | int
| `--profile` | Profile to run (dev, test, prod). | dev | String
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index 817a2a70d40c..14cb9d3c20ae 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -6,9 +6,9 @@
{ "name": "cmd", "fullName": "cmd", "description": "Performs commands in
the running Camel integrations, such as start\/stop route, or change logging
levels.", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"browse", "fullName": "cmd browse", "description": "Browse pending messages on
endpoints [...]
{ "name": "completion", "fullName": "completion", "description": "Generate
completion script for bash\/zsh", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ] },
{ "name": "config", "fullName": "config", "description": "Get and set user
configuration values", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get",
"fullName": "config get", "description": "Display user configuration value",
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...]
- { "name": "debug", "fullName": "debug", "description": "Debug local Camel
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug",
"options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd
HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names":
"--background", "description": "Run in the background", "defaultValue":
"false", "javaType": "boolean", "type": "boolean" }, { "names":
"--background-wait", "description": "To [...]
+ { "name": "debug", "fullName": "debug", "description": "Debug local Camel
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug",
"options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd
HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names":
"--background", "description": "Run in the background", "defaultValue":
"false", "javaType": "boolean", "type": "boolean" }, { "names":
"--background-wait", "description": "To [...]
{ "name": "dependency", "fullName": "dependency", "description": "Displays
all Camel dependencies required to run", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.DependencyCommand", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"copy", "fullName": "dependency copy", "description": "Copies all Camel
dependencies required to run to a specific directory", "sourc [...]
- { "name": "dev", "fullName": "dev", "description": "Run in dev mode with
live reload", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Dev",
"options": [ { "names": "--background", "description": "Run in the background",
"defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names":
"--background-wait", "description": "To wait for run in background to startup
successfully, before returning", "defaultValue": "true", "javaType": "boolean",
"type": "boolean" }, [...]
+ { "name": "dev", "fullName": "dev", "description": "Run in dev mode with
live reload", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Dev",
"options": [ { "names": "--background", "description": "Run in the background",
"defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names":
"--background-wait", "description": "To wait for run in background to startup
successfully, before returning", "defaultValue": "true", "javaType": "boolean",
"type": "boolean" }, [...]
{ "name": "dirty", "fullName": "dirty", "description": "Check if there are
dirty files from previous Camel runs that did not terminate gracefully",
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.Dirty",
"options": [ { "names": "--clean", "description": "Clean dirty files which are
no longer in use", "defaultValue": "false", "javaType": "boolean", "type":
"boolean" }, { "names": "-h,--help", "description": "Display the help and
sub-commands", "javaType": "boolean", " [...]
{ "name": "doc", "fullName": "doc", "description": "Shows documentation
for kamelet, component, and other Camel resources", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogDoc", "options": [ {
"names": "--camel-version", "description": "To use a different Camel version
than the default version", "javaType": "java.lang.String", "type": "string" },
{ "names": "--download", "description": "Whether to allow automatic downloading
JAR dependencies (over the internet [...]
{ "name": "doctor", "fullName": "doctor", "description": "Checks the
environment and reports potential issues", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Doctor", "options": [ { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ] },
@@ -26,7 +26,7 @@
{ "name": "plugin", "fullName": "plugin", "description": "Manage plugins
that add sub-commands to this CLI", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.plugin.PluginCommand", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "add",
"fullName": "plugin add", "description": "Add new plugin", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.plugin.PluginA [...]
{ "name": "ps", "fullName": "ps", "description": "List running Camel
integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.ListProcess", "options": [ {
"names": "--json", "description": "Output in JSON Format", "javaType":
"boolean", "type": "boolean" }, { "names": "--pid", "description": "List only
pid in the output", "javaType": "boolean", "type": "boolean" }, { "names":
"--remote", "description": "Break down counters into remote\/total pairs",
"javaType": [...]
{ "name": "restart", "fullName": "restart", "description": "Restarts
running Camel integrations (stop + re-launch)", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.RestartProcess", "options": [
{ "names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ] },
- { "name": "run", "fullName": "run", "description": "Run as local Camel
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Run",
"options": [ { "names": "--background", "description": "Run in the background",
"defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names":
"--background-wait", "description": "To wait for run in background to startup
successfully, before returning", "defaultValue": "true", "javaType": "boolean",
"type": "boolean" }, { [...]
+ { "name": "run", "fullName": "run", "description": "Run as local Camel
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Run",
"options": [ { "names": "--background", "description": "Run in the background",
"defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names":
"--background-wait", "description": "To wait for run in background to startup
successfully, before returning", "defaultValue": "true", "javaType": "boolean",
"type": "boolean" }, { [...]
{ "name": "sbom", "fullName": "sbom", "description": "Generate a CycloneDX
or SPDX SBOM for a specific project", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.SBOMGenerator", "options": [ {
"names": "--build-property", "description": "Maven build properties, ex.
--build-property=prop1=foo", "javaType": "java.util.List", "type": "array" }, {
"names": "--camel-spring-boot-version", "description": "Camel version to use
with Spring Boot", "javaType": "java.lang.String", "type" [...]
{ "name": "script", "fullName": "script", "description": "Run Camel
integration as shell script for terminal scripting", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Script", "options": [ { "names":
"--logging", "description": "Can be used to turn on logging (logs to file in
<user home>\/.camel directory)", "defaultValue": "false", "javaType":
"boolean", "type": "boolean" }, { "names": "--logging-level", "description":
"Logging level (ERROR, WARN, INFO, DEBUG, TRACE)", "d [...]
{ "name": "shell", "fullName": "shell", "description": "Interactive Camel
JBang shell.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Shell",
"options": [ { "names": "-h,--help", "description": "Display the help and
sub-commands", "javaType": "boolean", "type": "boolean" } ] },
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
index f3cd2c2385cc..324fe9a55b6c 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
@@ -558,7 +558,7 @@ public class Run extends CamelCommand {
}
private boolean isDebugMode() {
- return jvmDebugPort > 0;
+ return jvmDebugPort > 0 || debugOptions.openTelemetryAgent;
}
private void writeSetting(KameletMain main, Properties existing, String
key, String value) {
@@ -1186,6 +1186,17 @@ public class Run extends CamelCommand {
dependencies.add("camel:observability-services");
main.addOverrideProperty("camel.metrics.logMetricsOnShutdown",
"false");
}
+ if (debugOptions.openTelemetryAgent) {
+ dependencies.add("camel:opentelemetry2");
+ boolean jaegerExport =
"jaeger".equals(debugOptions.openTelemetryAgentExport);
+ if (!jaegerExport) {
+ dependencies.add("camel:platform-http-main");
+ if (serverOptions.port == -1) {
+ serverOptions.port = 8080;
+ }
+ writeSetting(main, profileProperties, "camel.server.enabled",
"true");
+ }
+ }
if (!dependencies.isEmpty()) {
var joined = String.join(",", dependencies);
main.addInitialProperty(DEPENDENCIES, joined);
@@ -1688,10 +1699,32 @@ public class Run extends CamelCommand {
if (debugSuspend != null) {
jbangArgs.add("-D" + BacklogDebugger.SUSPEND_MODE_SYSTEM_PROP_NAME
+ "=" + debugSuspend);
}
- if (isDebugMode()) {
+ if (jvmDebugPort > 0) {
jbangArgs.add("--debug=" + jvmDebugPort); // jbang --debug=port
cmds.removeIf(arg -> arg.startsWith("--jvm-debug"));
}
+ if (debugOptions.openTelemetryAgent) {
+ boolean jaegerExport =
"jaeger".equals(debugOptions.openTelemetryAgentExport);
+
jbangArgs.add("--javaagent=io.opentelemetry.javaagent:opentelemetry-javaagent:RELEASE");
+ jbangArgs.add("-Dotel.metrics.exporter=none");
+ jbangArgs.add("-Dotel.logs.exporter=none");
+ jbangArgs.add("-Dotel.service.name=camel");
+ cmds.removeIf(arg -> arg.startsWith("--open-telemetry-agent"));
+ cmds.add("--dep=camel:opentelemetry2");
+ cmds.add("--prop=camel.opentelemetry2.enabled=true");
+ if (jaegerExport) {
+
jbangArgs.add("-Dotel.exporter.otlp.traces.endpoint=http://localhost:4318/v1/traces");
+ cmds.add("--prop=camel.opentelemetry2.exportTarget=jaeger");
+ } else {
+ int port = serverOptions.port > 0 ? serverOptions.port : 8080;
+
jbangArgs.add("-Dotel.exporter.otlp.traces.endpoint=http://localhost:" + port +
"/v1/traces");
+ cmds.add("--dep=camel:platform-http-main");
+
cmds.add("--dep=mvn:io.opentelemetry.proto:opentelemetry-proto:RELEASE");
+ if (cmds.stream().noneMatch(a -> a.startsWith("--port"))) {
+ cmds.add("--port=" + port);
+ }
+ }
+ }
if (javaVersion != null) {
jbangArgs.add("--java=" + javaVersion);
@@ -2412,6 +2445,16 @@ public class Run extends CamelCommand {
@Option(names = { "--backlog-trace" }, defaultValue = "false",
description = "Enables backlog tracing of the routed messages")
boolean backlogTrace;
+
+ @Option(names = { "--open-telemetry-agent" }, defaultValue = "false",
+ description = "Enable OpenTelemetry Java Agent for
auto-instrumentation of third-party libraries (HTTP clients, JDBC, Kafka
clients, gRPC, etc.). "
+ + "Traces are shown in the TUI Spans tab with
Camel spans in cyan and 3rd-party agent spans in magenta.")
+ boolean openTelemetryAgent;
+
+ @Option(names = { "--open-telemetry-agent-export" }, defaultValue =
"tui",
+ description = "Where to export OpenTelemetry Agent traces: tui
(embedded receiver in TUI) or jaeger (external Jaeger). "
+ + "With jaeger, start Jaeger first via 'camel
infra run jaeger' and view traces at http://localhost:16686")
+ String openTelemetryAgentExport = "tui";
}
public static class ExecutionLimitOptions {
@@ -2451,7 +2494,7 @@ public class Run extends CamelCommand {
boolean metrics;
@Option(names = { "--observe" }, defaultValue = "false",
- description = "Enable observability services")
+ description = "Enable observability services (health, metrics,
dev console, and lightweight Camel-only tracing in the TUI Spans tab)")
boolean observe;
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
index c42a4624e5ce..11891d63a588 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ActionsPopup.java
@@ -152,7 +152,7 @@ class ActionsPopup {
private ScheduledExecutorService scheduler;
private final List<PendingLaunch> pendingLaunches = new ArrayList<>();
- private DeferredExampleLaunch deferredLaunch;
+ private DeferredLaunch deferredLaunch;
private String launchNotification;
private boolean launchNotificationError;
private long launchNotificationExpiry;
@@ -1349,8 +1349,19 @@ class ActionsPopup {
displayName = Path.of(folder).getFileName().toString();
}
List<String> extraArgs = runOptionsForm.buildArgs();
+ boolean jaegerExport = runOptionsForm.isJaegerExport();
runOptionsForm.close();
selectedFolder = null;
+
+ if (jaegerExport && !isJaegerRunning()) {
+ if (!isContainerRuntimeAvailable()) {
+ setNotification("Docker/Podman required for Jaeger. Run Doctor
for details", true);
+ return;
+ }
+ startMissingInfraAndDeferFolder(folder, displayName, extraArgs);
+ return;
+ }
+
doLaunchFolder(folder, displayName, extraArgs);
}
@@ -1418,10 +1429,15 @@ class ActionsPopup {
}
List<String> extraArgs = runOptionsForm.buildArgs();
boolean stub = runOptionsForm.isStubMode();
+ boolean jaegerExport = runOptionsForm.isJaegerExport();
runOptionsForm.close();
if (!stub) {
List<String> missing = findMissingInfraServices(selectedExample);
+ if (jaegerExport && !isJaegerRunning()) {
+ missing = new ArrayList<>(missing);
+ missing.add("jaeger");
+ }
if (!missing.isEmpty()) {
if (!isContainerRuntimeAvailable()) {
setNotification("Docker/Podman required for infra
services. Run Doctor for details", true);
@@ -1947,8 +1963,23 @@ class ActionsPopup {
return missing;
}
+ private boolean isJaegerRunning() {
+ return infraServices.get().stream()
+ .anyMatch(i -> i.alive && "jaeger".equals(i.alias));
+ }
+
private void startMissingInfraAndDeferExample(
List<String> missingInfra, String exampleName, String displayName,
List<String> extraArgs) {
+ startMissingInfraAndDefer(
+ missingInfra, displayName, () -> doLaunchExample(exampleName,
displayName, extraArgs));
+ }
+
+ private void startMissingInfraAndDeferFolder(String folder, String
displayName, List<String> extraArgs) {
+ startMissingInfraAndDefer(
+ List.of("jaeger"), displayName, () -> doLaunchFolder(folder,
displayName, extraArgs));
+ }
+
+ private void startMissingInfraAndDefer(List<String> missingInfra, String
displayName, Runnable launchAction) {
for (String alias : missingInfra) {
try {
List<String> cmd = new
ArrayList<>(LauncherHelper.getCamelCommand());
@@ -1968,29 +1999,26 @@ class ActionsPopup {
return;
}
}
- deferredLaunch = new DeferredExampleLaunch(
- exampleName, displayName, extraArgs, missingInfra,
System.currentTimeMillis());
+ deferredLaunch = new DeferredLaunch(displayName, missingInfra,
System.currentTimeMillis(), launchAction);
infraCatalog = null;
String infraList = String.join(", ", missingInfra);
setNotification("Starting infra: " + infraList + " → then: " +
displayName, false);
}
private void checkDeferredLaunch(long now) {
- if (deferredLaunch == null) {
- return;
- }
- Set<String> runningAliases = infraServices.get().stream()
- .filter(i -> i.alive)
- .map(i -> i.alias)
- .collect(Collectors.toSet());
- boolean allReady =
runningAliases.containsAll(deferredLaunch.requiredInfra);
- if (allReady) {
- DeferredExampleLaunch dl = deferredLaunch;
- deferredLaunch = null;
- doLaunchExample(dl.exampleName, dl.displayName, dl.extraArgs);
- } else if (now - deferredLaunch.startTime > 120_000) {
- deferredLaunch = null;
- setNotification("Timeout waiting for infra services to start",
true);
+ if (deferredLaunch != null) {
+ Set<String> runningAliases = infraServices.get().stream()
+ .filter(i -> i.alive)
+ .map(i -> i.alias)
+ .collect(Collectors.toSet());
+ if (runningAliases.containsAll(deferredLaunch.requiredInfra)) {
+ DeferredLaunch dl = deferredLaunch;
+ deferredLaunch = null;
+ dl.launchAction.run();
+ } else if (now - deferredLaunch.startTime > 120_000) {
+ deferredLaunch = null;
+ setNotification("Timeout waiting for infra services to start",
true);
+ }
}
}
@@ -2146,8 +2174,8 @@ class ActionsPopup {
private record PendingLaunch(String name, Process process, Path
outputFile, long startTime) {
}
- private record DeferredExampleLaunch(
- String exampleName, String displayName, List<String> extraArgs,
- List<String> requiredInfra, long startTime) {
+ private record DeferredLaunch(
+ String displayName, List<String> requiredInfra, long startTime,
+ Runnable launchAction) {
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
index 3437eee08aaf..c6b2aba4ef4a 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CamelMonitor.java
@@ -220,6 +220,7 @@ public class CamelMonitor extends CamelCommand {
private MemoryTab memoryTab;
private ThreadsTab threadsTab;
private SpansTab spansTab;
+ private ProcessTab processTab;
private OverviewTab overviewTab;
// "Switch integration" popup state
@@ -289,6 +290,7 @@ public class CamelMonitor extends CamelCommand {
memoryTab = new MemoryTab(ctx, metrics);
threadsTab = new ThreadsTab(ctx);
spansTab = new SpansTab(ctx, otelSpans);
+ processTab = new ProcessTab(ctx);
overviewTab = new OverviewTab(
ctx, metrics, stoppingPids,
this::resetIntegrationTabState);
@@ -417,7 +419,7 @@ public class CamelMonitor extends CamelCommand {
return true;
}
if (ke.isDown()) {
- morePopupState.selectNext(12);
+ morePopupState.selectNext(13);
return true;
}
int shortcutSel = morePopupShortcut(ke);
@@ -440,8 +442,9 @@ public class CamelMonitor extends CamelCommand {
case 7 -> memoryTab;
case 8 -> metricsTab;
case 9 -> spansTab;
- case 10 -> startupTab;
- case 11 -> threadsTab;
+ case 10 -> processTab;
+ case 11 -> startupTab;
+ case 12 -> threadsTab;
default -> null;
};
if (activeMoreTab != null) {
@@ -906,6 +909,9 @@ public class CamelMonitor extends CamelCommand {
consumersTab.onIntegrationChanged();
circuitBreakerTab.onIntegrationChanged();
inflightTab.onIntegrationChanged();
+ spansTab.onIntegrationChanged();
+ processTab.onIntegrationChanged();
+ otelSpans.set(List.of());
filesBrowser.reset();
@@ -1191,7 +1197,7 @@ public class CamelMonitor extends CamelCommand {
private void renderMorePopup(Frame frame, Rect area) {
int popupW = 22;
- int popupH = 12;
+ int popupH = 15;
// Position just below the "0 More▾" tab label
int dividerW = CharWidth.of(" | ");
int tabBarX = 0;
@@ -1223,6 +1229,7 @@ public class CamelMonitor extends CamelCommand {
ListItem.from(Line.from(Span.raw(" "), Span.styled("M",
keyStyle), Span.raw("emory"))),
ListItem.from(Line.from(Span.raw(" M"), Span.styled("e",
keyStyle), Span.raw("trics"))),
ListItem.from(Line.from(Span.raw(" "), Span.styled("O",
keyStyle), Span.raw("Tel Spans"))),
+ ListItem.from(Line.from(Span.raw(" "), Span.styled("P",
keyStyle), Span.raw("rocess"))),
ListItem.from(Line.from(Span.raw(" "), Span.styled("S",
keyStyle), Span.raw("tartup"))),
ListItem.from(Line.from(Span.raw(" "), Span.styled("T",
keyStyle), Span.raw("hreads"))),
};
@@ -1332,12 +1339,15 @@ public class CamelMonitor extends CamelCommand {
if (ke.isChar('o')) {
return 9;
}
- if (ke.isChar('s')) {
+ if (ke.isChar('p')) {
return 10;
}
- if (ke.isChar('t')) {
+ if (ke.isChar('s')) {
return 11;
}
+ if (ke.isChar('t')) {
+ return 12;
+ }
return -1;
}
@@ -2414,7 +2424,7 @@ public class CamelMonitor extends CamelCommand {
private static final String[] MORE_TAB_NAMES = {
"Beans", "Browse", "Circuit Breaker", "Classpath", "Configuration",
- "Consumers", "Inflight", "Memory", "Metrics", "Spans", "Startup",
"Threads"
+ "Consumers", "Inflight", "Memory", "Metrics", "Spans", "Process",
"Startup", "Threads"
};
String navigateToTab(String tabName) {
@@ -2440,8 +2450,9 @@ public class CamelMonitor extends CamelCommand {
case 7 -> memoryTab;
case 8 -> metricsTab;
case 9 -> spansTab;
- case 10 -> startupTab;
- case 11 -> threadsTab;
+ case 10 -> processTab;
+ case 11 -> startupTab;
+ case 12 -> threadsTab;
default -> null;
};
if (activeMoreTab != null) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTab.java
new file mode 100644
index 000000000000..a9366549b413
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ProcessTab.java
@@ -0,0 +1,252 @@
+/*
+ * 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.dsl.jbang.core.commands.tui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.layout.Constraint;
+import dev.tamboui.layout.Layout;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.style.Color;
+import dev.tamboui.style.Overflow;
+import dev.tamboui.style.Style;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.text.Line;
+import dev.tamboui.text.Span;
+import dev.tamboui.text.Text;
+import dev.tamboui.tui.event.KeyEvent;
+import dev.tamboui.widgets.block.Block;
+import dev.tamboui.widgets.block.BorderType;
+import dev.tamboui.widgets.paragraph.Paragraph;
+import dev.tamboui.widgets.scrollbar.Scrollbar;
+import dev.tamboui.widgets.scrollbar.ScrollbarState;
+import org.apache.camel.util.json.JsonObject;
+
+import static org.apache.camel.dsl.jbang.core.commands.tui.MonitorContext.*;
+
+class ProcessTab implements MonitorTab {
+
+ private final MonitorContext ctx;
+ private boolean wrap;
+ private int scroll;
+ private final ScrollbarState scrollState = new ScrollbarState();
+
+ ProcessTab(MonitorContext ctx) {
+ this.ctx = ctx;
+ }
+
+ @Override
+ public boolean handleKeyEvent(KeyEvent ke) {
+ if (ke.isChar('w')) {
+ wrap = !wrap;
+ scroll = 0;
+ return true;
+ }
+ if (ke.isPageUp()) {
+ scroll = Math.max(0, scroll - 5);
+ return true;
+ }
+ if (ke.isPageDown()) {
+ scroll += 5;
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean handleEscape() {
+ return false;
+ }
+
+ @Override
+ public void navigateUp() {
+ scroll = Math.max(0, scroll - 1);
+ }
+
+ @Override
+ public void navigateDown() {
+ scroll += 1;
+ }
+
+ @Override
+ public void render(Frame frame, Rect area) {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null) {
+ renderNoSelection(frame, area);
+ return;
+ }
+
+ List<Line> lines = new ArrayList<>();
+
+ addField(lines, "PID", info.pid);
+ addField(lines, "Name", info.name);
+ addField(lines, "Camel", info.camelVersion);
+
+ String platform = info.platform;
+ if (platform != null && info.platformVersion != null) {
+ platform = platform + " " + info.platformVersion;
+ }
+ addField(lines, "Platform", platform);
+ addField(lines, "Profile", info.profile);
+
+ String java = info.javaVersion;
+ if (java != null) {
+ StringBuilder sb = new StringBuilder(java);
+ if (info.javaVendor != null) {
+ sb.append(" (").append(info.javaVendor);
+ if (info.javaVmName != null) {
+ sb.append(", ").append(info.javaVmName);
+ }
+ sb.append(")");
+ }
+ java = sb.toString();
+ }
+ addField(lines, "Java", java);
+ addField(lines, "Directory", info.directory);
+ addField(lines, "Uptime", info.ago);
+
+ lines.add(Line.from(Span.raw("")));
+
+ String cmdLine = getCommandLine(info.pid);
+ if (cmdLine != null) {
+ lines.add(Line.from(
+ Span.styled(" Command Line",
Style.EMPTY.fg(Color.CYAN).bold())));
+ lines.add(Line.from(Span.raw("")));
+ if (wrap) {
+ lines.add(Line.from(Span.raw(" " + cmdLine)));
+ } else {
+ for (String part : splitCommandLine(cmdLine)) {
+ lines.add(Line.from(Span.raw(" " + part)));
+ }
+ }
+ }
+
+ Block block = Block.builder().borderType(BorderType.ROUNDED).title("
Process ").build();
+ frame.renderWidget(block, area);
+
+ Rect inner = block.inner(area);
+ int visibleHeight = Math.max(1, inner.height());
+ int visibleWidth = Math.max(1, inner.width() - 1);
+ int contentHeight;
+ if (wrap) {
+ contentHeight = 0;
+ for (Line l : lines) {
+ int w = l.width();
+ contentHeight += Math.max(1, (w + visibleWidth - 1) /
visibleWidth);
+ }
+ contentHeight += visibleHeight;
+ } else {
+ contentHeight = lines.size();
+ }
+ int maxScroll = Math.max(0, contentHeight - visibleHeight);
+ if (scroll > maxScroll) {
+ scroll = maxScroll;
+ }
+
+ List<Rect> hChunks = Layout.horizontal()
+ .constraints(Constraint.fill(), Constraint.length(1))
+ .split(inner);
+
+ Paragraph paragraph = Paragraph.builder()
+ .text(Text.from(lines))
+ .overflow(wrap ? Overflow.WRAP_WORD : Overflow.CLIP)
+ .scroll(scroll)
+ .build();
+ frame.renderWidget(paragraph, hChunks.get(0));
+
+ if (contentHeight > visibleHeight) {
+ scrollState.contentLength(contentHeight);
+ scrollState.viewportContentLength(visibleHeight);
+ scrollState.position(scroll);
+ frame.renderStatefulWidget(
+ Scrollbar.builder().build(),
+ hChunks.get(1), scrollState);
+ }
+ }
+
+ @Override
+ public void renderFooter(List<Span> spans) {
+ hint(spans, "↑↓", "scroll");
+ hint(spans, "w", "wrap [" + (wrap ? "on" : "off") + "]");
+ hintLast(spans, "Esc", "back");
+ }
+
+ @Override
+ public JsonObject getTableDataAsJson() {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ if (info == null) {
+ return null;
+ }
+ JsonObject result = new JsonObject();
+ result.put("tab", "Process");
+ JsonObject data = new JsonObject();
+ data.put("pid", info.pid);
+ data.put("name", info.name);
+ data.put("camelVersion", info.camelVersion);
+ data.put("platform", info.platform);
+ data.put("platformVersion", info.platformVersion);
+ data.put("profile", info.profile);
+ data.put("javaVersion", info.javaVersion);
+ data.put("javaVendor", info.javaVendor);
+ data.put("javaVmName", info.javaVmName);
+ data.put("directory", info.directory);
+ data.put("uptime", info.ago);
+ String cmdLine = getCommandLine(info.pid);
+ if (cmdLine != null) {
+ data.put("commandLine", cmdLine);
+ }
+ result.put("process", data);
+ return result;
+ }
+
+ private void addField(List<Line> lines, String label, String value) {
+ if (value == null || value.isEmpty()) {
+ return;
+ }
+ String padded = String.format(" %-12s", label + ":");
+ lines.add(Line.from(
+ Span.styled(padded, Style.EMPTY.dim()),
+ Span.styled(value, Style.EMPTY.fg(Color.WHITE).bold())));
+ }
+
+ private static String getCommandLine(String pid) {
+ try {
+ long p = Long.parseLong(pid);
+ return ProcessHandle.of(p)
+ .flatMap(ph -> ph.info().commandLine())
+ .orElse(null);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private static List<String> splitCommandLine(String cmdLine) {
+ List<String> parts = new ArrayList<>();
+ for (String token : cmdLine.split("\\s+")) {
+ if (token.startsWith("-") && !parts.isEmpty()) {
+ parts.add(token);
+ } else if (parts.isEmpty()) {
+ parts.add(token);
+ } else {
+ String last = parts.get(parts.size() - 1);
+ parts.set(parts.size() - 1, last + " " + token);
+ }
+ }
+ return parts;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
index 3e4f6b0ae5af..2331f15640a8 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/RunOptionsForm.java
@@ -50,7 +50,8 @@ class RunOptionsForm {
private static final int ROW_OBSERVE = 4;
private static final int ROW_TRACE = 5;
private static final int ROW_STUB = 6;
- private static final int ROW_COUNT = 7;
+ private static final int ROW_OTEL_AGENT = 7;
+ private static final int ROW_COUNT = 8;
private boolean visible;
private int page;
@@ -70,6 +71,8 @@ class RunOptionsForm {
private boolean observe;
private boolean backlogTrace;
private boolean stubMode;
+ private boolean otelAgent;
+ private int otelExportTarget; // 0=TUI, 1=Jaeger
private String exampleTitle;
@@ -95,6 +98,8 @@ class RunOptionsForm {
observe = false;
backlogTrace = false;
stubMode = false;
+ otelAgent = false;
+ otelExportTarget = 0;
selectedRow = ROW_NAME;
page = PAGE_OPTIONS;
selectedProperty = 0;
@@ -115,6 +120,10 @@ class RunOptionsForm {
return stubMode;
}
+ boolean isJaegerExport() {
+ return otelAgent && otelExportTarget == 1;
+ }
+
boolean handleKeyEvent(KeyEvent ke) {
if (!visible) {
return false;
@@ -192,6 +201,12 @@ class RunOptionsForm {
if (stubMode) {
args.add("--stub=remote");
}
+ if (otelAgent) {
+ args.add("--open-telemetry-agent");
+ if (otelExportTarget == 1) {
+ args.add("--open-telemetry-agent-export=jaeger");
+ }
+ }
if (properties != null) {
for (PropertyEntry pe : properties) {
String current = pe.valueInput().text();
@@ -215,7 +230,7 @@ class RunOptionsForm {
return true;
}
if (ke.isDown()) {
- if (selectedRow == ROW_STUB && hasProperties()) {
+ if (selectedRow == ROW_OTEL_AGENT && hasProperties()) {
page = PAGE_PROPERTIES;
selectedProperty = 0;
} else {
@@ -224,7 +239,7 @@ class RunOptionsForm {
return true;
}
if (ke.isFocusNext()) {
- if (selectedRow == ROW_STUB && hasProperties()) {
+ if (selectedRow == ROW_OTEL_AGENT && hasProperties()) {
page = PAGE_PROPERTIES;
selectedProperty = 0;
} else {
@@ -236,7 +251,15 @@ class RunOptionsForm {
selectedRow = (selectedRow - 1 + ROW_COUNT) % ROW_COUNT;
return true;
}
- if (ke.isRight() && hasProperties() && selectedRow >= ROW_STUB) {
+ if (ke.isRight() && selectedRow == ROW_OTEL_AGENT && otelAgent) {
+ otelExportTarget = (otelExportTarget + 1) % 2;
+ return true;
+ }
+ if (ke.isLeft() && selectedRow == ROW_OTEL_AGENT && otelAgent) {
+ otelExportTarget = (otelExportTarget + 1) % 2;
+ return true;
+ }
+ if (ke.isRight() && hasProperties() && selectedRow >= ROW_OTEL_AGENT) {
page = PAGE_PROPERTIES;
selectedProperty = 0;
return true;
@@ -261,6 +284,7 @@ class RunOptionsForm {
case ROW_OBSERVE -> observe = !observe;
case ROW_TRACE -> backlogTrace = !backlogTrace;
case ROW_STUB -> stubMode = !stubMode;
+ case ROW_OTEL_AGENT -> otelAgent = !otelAgent;
}
return true;
}
@@ -287,7 +311,7 @@ class RunOptionsForm {
editingKey = false;
if (selectedProperty == 0) {
page = PAGE_OPTIONS;
- selectedRow = ROW_STUB;
+ selectedRow = ROW_OTEL_AGENT;
} else {
selectedProperty--;
}
@@ -317,7 +341,7 @@ class RunOptionsForm {
} else if (selectedProperty == 0) {
editingKey = false;
page = PAGE_OPTIONS;
- selectedRow = ROW_STUB;
+ selectedRow = ROW_OTEL_AGENT;
} else {
editingKey = false;
selectedProperty--;
@@ -333,7 +357,7 @@ class RunOptionsForm {
return true;
}
page = PAGE_OPTIONS;
- selectedRow = ROW_STUB;
+ selectedRow = ROW_OTEL_AGENT;
editingKey = false;
return true;
}
@@ -376,7 +400,7 @@ class RunOptionsForm {
private void renderOptionsPage(Frame frame, Rect area) {
int popupW = Math.min(56, area.width() - 4);
- int popupH = 11;
+ int popupH = 12;
int x = area.left() + Math.max(0, (area.width() - popupW) / 2);
int y = area.top() + Math.max(0, (area.height() - popupH) / 4);
Rect popup = new Rect(x, y, Math.min(popupW, area.width()),
Math.min(popupH, area.height()));
@@ -441,6 +465,22 @@ class RunOptionsForm {
rowY++;
renderCheckbox(frame, innerX, rowY, innerW, "Stub (no Docker needed)",
stubMode, selectedRow == ROW_STUB);
+ rowY++;
+
+ renderCheckbox(frame, innerX, rowY, innerW, "OTel Java Agent
(auto-instrument)", otelAgent,
+ selectedRow == ROW_OTEL_AGENT);
+ if (otelAgent) {
+ String tuiLabel = otelExportTarget == 0 ? "[TUI]" : " TUI ";
+ String jaegerLabel = otelExportTarget == 1 ? "[Jaeger]" : " Jaeger
";
+ Style tuiStyle = otelExportTarget == 0 ? Style.EMPTY.bold() :
Style.EMPTY.dim();
+ Style jaegerStyle = otelExportTarget == 1 ? Style.EMPTY.bold() :
Style.EMPTY.dim();
+ int exportX = innerX + 38;
+ Rect exportArea = new Rect(exportX, rowY, innerW - 36, 1);
+ frame.renderWidget(Paragraph.from(Line.from(
+ Span.styled(tuiLabel, tuiStyle),
+ Span.styled(" ", Style.EMPTY),
+ Span.styled(jaegerLabel, jaegerStyle))), exportArea);
+ }
}
private void renderPropertiesPage(Frame frame, Rect area) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpanEntry.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpanEntry.java
index 8825d21f89e6..b877a2a6ef80 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpanEntry.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpanEntry.java
@@ -32,6 +32,7 @@ record SpanEntry(
long durationMs,
String routeId,
String processorId,
+ String scopeName,
Map<String, Object> attributes) {
@SuppressWarnings("unchecked")
@@ -53,6 +54,7 @@ record SpanEntry(
jo.getLongOrDefault("durationMs", 0),
jo.getString("routeId"),
jo.getString("processorId"),
+ jo.getString("scopeName"),
attrs);
}
@@ -63,4 +65,8 @@ record SpanEntry(
boolean isError() {
return "ERROR".equals(status);
}
+
+ boolean isCamelSpan() {
+ return scopeName == null || "camel".equals(scopeName);
+ }
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpansTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpansTab.java
index 065da540a47a..cc314bedba23 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpansTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SpansTab.java
@@ -67,6 +67,7 @@ class SpansTab implements MonitorTab {
private int waterfallScroll;
private int waterfallSelected;
private boolean showProcessors = true;
+ private boolean camelOnly;
private String sortColumn = "trace-id";
private int sortIndex;
private boolean sortReversed;
@@ -184,6 +185,12 @@ class SpansTab implements MonitorTab {
waterfallScroll = 0;
return true;
}
+ if (ke.isChar('c')) {
+ camelOnly = !camelOnly;
+ waterfallSelected = 0;
+ waterfallScroll = 0;
+ return true;
+ }
if (ke.isKey(KeyCode.F5)) {
spanRefreshRequested = true;
return true;
@@ -498,11 +505,15 @@ class SpansTab implements MonitorTab {
int pad = Math.max(1, 8 - durationStr.length());
boolean error = node.span.isError();
+ boolean camelSpan = node.span.isCamelSpan();
Style labelStyle;
Style bandStyle;
if (error) {
labelStyle = selected ? Style.EMPTY.fg(Color.LIGHT_RED).bold() :
Style.EMPTY.fg(Color.LIGHT_RED);
bandStyle = Style.EMPTY.fg(Color.LIGHT_RED);
+ } else if (!camelSpan) {
+ labelStyle = selected ? Style.EMPTY.fg(Color.LIGHT_MAGENTA).bold()
: Style.EMPTY.fg(Color.LIGHT_MAGENTA);
+ bandStyle = Style.EMPTY.fg(Color.LIGHT_MAGENTA);
} else {
labelStyle = selected ? Style.EMPTY.fg(Color.CYAN).bold() :
Style.EMPTY.fg(Color.CYAN);
bandStyle = TuiHelper.colorForDuration(node.span.durationMs(),
minDuration, maxDuration);
@@ -547,8 +558,8 @@ class SpansTab implements MonitorTab {
Span.styled(" Duration: ", Style.EMPTY.dim()),
Span.raw(span.durationMs() + "ms")));
- // Row 3: route and processor context
- if (span.routeId() != null || span.processorId() != null) {
+ // Row 3: route, processor context, and scope (for 3rd-party spans)
+ if (span.routeId() != null || span.processorId() != null ||
!span.isCamelSpan()) {
List<Span> ctx = new ArrayList<>();
if (span.routeId() != null) {
ctx.add(Span.styled(" Route: ", Style.EMPTY.dim()));
@@ -558,6 +569,10 @@ class SpansTab implements MonitorTab {
ctx.add(Span.styled(" Processor: ", Style.EMPTY.dim()));
ctx.add(Span.styled(span.processorId(),
Style.EMPTY.fg(Color.YELLOW)));
}
+ if (!span.isCamelSpan()) {
+ ctx.add(Span.styled(" Source: ", Style.EMPTY.dim()));
+ ctx.add(Span.styled(span.scopeName(),
Style.EMPTY.fg(Color.LIGHT_MAGENTA)));
+ }
lines.add(Line.from(ctx));
}
@@ -588,6 +603,7 @@ class SpansTab implements MonitorTab {
if (waterfallView) {
MonitorContext.hint(spans, "Esc", "back");
MonitorContext.hint(spans, "F5", "refresh");
+ MonitorContext.hint(spans, "c", camelOnly ? "camel-only [on]" :
"camel-only [off]");
MonitorContext.hint(spans, "p", showProcessors ? "processors [on]"
: "processors [off]");
MonitorContext.hint(spans, "↑↓", "navigate");
MonitorContext.hintLast(spans, "PgUp/Dn", "page");
@@ -765,6 +781,15 @@ class SpansTab implements MonitorTab {
addToWaterfall(result, children.get(0), childrenMap, depth,
included, spanIdToDepth);
return;
}
+ // Hide 3rd-party agent spans when camelOnly is on — promote children
to same depth
+ if (camelOnly && !span.isError() && !span.isCamelSpan()) {
+ if (children != null) {
+ for (SpanEntry child : children) {
+ addToWaterfall(result, child, childrenMap, depth,
included, spanIdToDepth);
+ }
+ }
+ return;
+ }
// Hide processor spans when toggle is off — promote children to same
depth
// Keep error processors visible so errors aren't hidden
if (!showProcessors && !span.isError() && isEventProcess(span)) {
@@ -924,7 +949,9 @@ class SpansTab implements MonitorTab {
# OTel Spans
The Spans tab shows OpenTelemetry traces captured from the
running
- integration. Requires the `--observe` flag when starting the
integration.
+ integration. Use `--observe` for lightweight Camel-only
tracing, or
+ `--open-telemetry-agent` for full auto-instrumentation (HTTP
clients,
+ JDBC, Kafka clients, etc.) via the OpenTelemetry Java Agent.
## Trace List
@@ -951,6 +978,17 @@ class SpansTab implements MonitorTab {
where time is spent. Colors indicate relative duration: green
(fast),
yellow (medium), red (slow).
+ ### Span Colors
+
+ - **Cyan** — Camel spans (route execution, processors,
endpoints)
+ - **Magenta** — 3rd-party spans from the OTel Java Agent
+ (HTTP clients, JDBC, Kafka clients, gRPC, etc.)
+ - **Red** — Error spans (regardless of source)
+
+ The 3rd-party spans are only visible when using
`--open-telemetry-agent`.
+ The detail panel shows the **Source** field for
agent-instrumented spans
+ (e.g., `io.opentelemetry.jdk-http-client`).
+
Processor spans (setBody, log, etc.) are shown by default.
Press **p**
to toggle them off for a cleaner view focused on
endpoint-to-endpoint
flow. Error spans are always shown regardless of the toggle.
@@ -988,6 +1026,7 @@ class SpansTab implements MonitorTab {
| s | Cycle sort column (trace-id, route, from, spans, routes,
status, duration) |
| S | Reverse sort direction |
| p | Toggle processor spans in waterfall |
+ | c | Toggle camel-only (hide 3rd-party agent spans) |
| F5 | Refresh span data |
## Filtering
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
index 66d85ad5f26d..fb871183452f 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiMcpServer.java
@@ -263,7 +263,7 @@ class TuiMcpServer {
"Navigates the TUI: switch tabs and/or select an integration. "
+ "All parameters are optional — set whichever
you want to change. "
+ "Tab names: Overview, Log, Diagram, Routes,
Endpoints, HTTP, Health, Inspect, "
- + "Circuit Breaker. "
+ + "Circuit Breaker, Spans, Process. "
+ "Use 'route' to select a route in the
Diagram topology, "
+ "and 'node' to drill down into a route and
select a specific processor/EIP node. "
+ "Returns screen content and selection
metadata after navigating.",
diff --git a/parent/pom.xml b/parent/pom.xml
index 6db3cb995f0a..93317e6d675c 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -413,6 +413,7 @@
<openstack4j-version>3.12</openstack4j-version>
<opentelemetry-version>1.63.0</opentelemetry-version>
<opentelemetry-alpha-version>1.63.0-alpha</opentelemetry-alpha-version>
+ <opentelemetry-proto-version>1.9.0-alpha</opentelemetry-proto-version>
<opentelemetry-log4j2-version>2.28.1-alpha</opentelemetry-log4j2-version>
<opentelemetry-incubator-version>1.43.0-alpha</opentelemetry-incubator-version>
<opentelemetry-semconv-version>1.30.1-alpha</opentelemetry-semconv-version>
@@ -3495,6 +3496,11 @@
</exclusion>
</exclusions>
</dependency>
+ <dependency>
+ <groupId>io.opentelemetry.proto</groupId>
+ <artifactId>opentelemetry-proto</artifactId>
+ <version>${opentelemetry-proto-version}</version>
+ </dependency>
<dependency>
<groupId>com.openai</groupId>
<artifactId>openai-java</artifactId>
diff --git
a/test-infra/camel-test-infra-jaeger/src/main/java/org/apache/camel/test/infra/jaeger/services/JaegerLocalContainerInfraService.java
b/test-infra/camel-test-infra-jaeger/src/main/java/org/apache/camel/test/infra/jaeger/services/JaegerLocalContainerInfraService.java
index 5c0c51a31bed..ff1e59defcdc 100644
---
a/test-infra/camel-test-infra-jaeger/src/main/java/org/apache/camel/test/infra/jaeger/services/JaegerLocalContainerInfraService.java
+++
b/test-infra/camel-test-infra-jaeger/src/main/java/org/apache/camel/test/infra/jaeger/services/JaegerLocalContainerInfraService.java
@@ -33,18 +33,26 @@ public class JaegerLocalContainerInfraService implements
JaegerInfraService, Con
public JaegerLocalContainerInfraService() {
container = new JaegerContainer();
- String name = ContainerEnvironmentUtil.containerName(this.getClass());
- if (name != null) {
- container.withCreateContainerCmdModifier(cmd ->
cmd.withName(name));
- }
+ initContainer();
}
public JaegerLocalContainerInfraService(String imageName) {
container = JaegerContainer.initContainer(imageName,
JaegerContainer.CONTAINER_NAME);
+ initContainer();
+ }
+
+ private void initContainer() {
String name = ContainerEnvironmentUtil.containerName(this.getClass());
if (name != null) {
container.withCreateContainerCmdModifier(cmd ->
cmd.withName(name));
}
+ boolean fixedPort =
ContainerEnvironmentUtil.isFixedPort(this.getClass());
+ if (fixedPort) {
+ ContainerEnvironmentUtil.configurePorts(container, true,
+
ContainerEnvironmentUtil.PortConfig.primary(JaegerProperties.DEFAULT_COLLECTOR_HTTP_PORT),
+
ContainerEnvironmentUtil.PortConfig.secondary(JaegerProperties.DEFAULT_COLLECTOR_GRPC_PORT),
+
ContainerEnvironmentUtil.PortConfig.secondary(JaegerProperties.DEFAULT_QUERY_UI_PORT));
+ }
}
@Override