This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch feature/CAMEL-23718-otel-agent-embedded-receiver in repository https://gitbox.apache.org/repos/asf/camel.git
commit fee014942e18622db4949a1208fd83c97e5b2c1d Author: Claus Ibsen <[email protected]> AuthorDate: Mon Jun 8 22:03:46 2026 +0200 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]> --- components/camel-opentelemetry2/pom.xml | 9 + .../OpenTelemetryTracerConfigurer.java | 6 + .../src/main/docs/opentelemetry2.adoc | 41 +++ .../camel/opentelemetry2/OpenTelemetryTracer.java | 32 +++ .../camel/opentelemetry2/OtlpProtobufSpanData.java | 280 +++++++++++++++++++++ .../opentelemetry2/OtlpReceiverProcessor.java | 64 +++++ .../pages/jbang-commands/camel-jbang-debug.adoc | 1 + .../ROOT/pages/jbang-commands/camel-jbang-dev.adoc | 1 + .../ROOT/pages/jbang-commands/camel-jbang-run.adoc | 1 + .../META-INF/camel-jbang-commands-metadata.json | 6 +- .../apache/camel/dsl/jbang/core/commands/Run.java | 32 ++- .../dsl/jbang/core/commands/tui/CamelMonitor.java | 20 +- .../dsl/jbang/core/commands/tui/ProcessTab.java | 252 +++++++++++++++++++ .../jbang/core/commands/tui/RunOptionsForm.java | 27 +- parent/pom.xml | 6 + 15 files changed, 760 insertions(+), 18 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..16a6995a0166 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 @@ -33,6 +33,8 @@ public class OpenTelemetryTracerConfigurer extends org.apache.camel.support.comp case "includePatterns": target.setIncludePatterns(property(camelContext, java.lang.String.class, value)); return true; case "spanlifecyclemanager": case "spanLifecycleManager": target.setSpanLifecycleManager(property(camelContext, org.apache.camel.telemetry.SpanLifecycleManager.class, value)); return true; + case "tracecustomidonly": + case "traceCustomIdOnly": target.setTraceCustomIdOnly(property(camelContext, boolean.class, value)); return true; case "traceheadersinclusion": case "traceHeadersInclusion": target.setTraceHeadersInclusion(property(camelContext, boolean.class, value)); return true; case "traceprocessors": @@ -54,6 +56,8 @@ public class OpenTelemetryTracerConfigurer extends org.apache.camel.support.comp case "includePatterns": return java.lang.String.class; case "spanlifecyclemanager": case "spanLifecycleManager": return org.apache.camel.telemetry.SpanLifecycleManager.class; + case "tracecustomidonly": + case "traceCustomIdOnly": return boolean.class; case "traceheadersinclusion": case "traceHeadersInclusion": return boolean.class; case "traceprocessors": @@ -76,6 +80,8 @@ public class OpenTelemetryTracerConfigurer extends org.apache.camel.support.comp case "includePatterns": return target.getIncludePatterns(); case "spanlifecyclemanager": case "spanLifecycleManager": return target.getSpanLifecycleManager(); + case "tracecustomidonly": + case "traceCustomIdOnly": return target.isTraceCustomIdOnly(); case "traceheadersinclusion": case "traceHeadersInclusion": return target.isTraceHeadersInclusion(); case "traceprocessors": diff --git a/components/camel-opentelemetry2/src/main/docs/opentelemetry2.adoc b/components/camel-opentelemetry2/src/main/docs/opentelemetry2.adoc index 9179c7bb1547..0d418abc3ed6 100644 --- a/components/camel-opentelemetry2/src/main/docs/opentelemetry2.adoc +++ b/components/camel-opentelemetry2/src/main/docs/opentelemetry2.adoc @@ -89,6 +89,47 @@ 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. + +==== When to use `--open-telemetry-agent` vs `--observe` + +Both flags enable tracing during development, but they cover different scopes: + +[width="100%",cols="30%,35%,35%",options="header"] +|======================================================================= +| | `--observe` | `--open-telemetry-agent` +| *Instrumentation* | Camel routes only | Camel routes + JVM, HTTP clients, databases, messaging, and other libraries +| *Setup* | Built-in SDK | Downloads OTel Java Agent +| *Collector* | Not needed | Not needed (embedded OTLP receiver) +| *Use case* | Quick Camel route debugging | Full-stack observability during development +|======================================================================= + +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/OpenTelemetryTracer.java b/components/camel-opentelemetry2/src/main/java/org/apache/camel/opentelemetry2/OpenTelemetryTracer.java index 3996e8a24ad6..5e518a695c1c 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; @@ -91,6 +93,11 @@ public class OpenTelemetryTracer extends org.apache.camel.telemetry.Tracer { } private void initDevSpanExporter() { + if (isOpenTelemetryAgentPresent()) { + LOG.info("OpenTelemetry Java Agent detected, using agent's tracer for proper trace context propagation"); + initOtlpReceiver(); + return; + } DevSpanExporter exporter = new DevSpanExporter(); SdkTracerProvider tracerProvider = SdkTracerProvider.builder() .addSpanProcessor(SimpleSpanProcessor.create(exporter)) @@ -99,12 +106,37 @@ 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); + + 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; } 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..a01a18159f66 --- /dev/null +++ b/components/camel-opentelemetry2/src/main/java/org/apache/camel/opentelemetry2/OtlpReceiverProcessor.java @@ -0,0 +1,64 @@ +/* + * 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); + 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/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..e1a8bcc3d221 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 @@ -65,6 +65,7 @@ camel debug [options] | `--name` | The name of the Camel application | CamelJBang | String | `--observe` | Enable observability services | 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 | false | boolean | `--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..51b2298612c6 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 @@ -61,6 +61,7 @@ camel dev [options] | `--name` | The name of the Camel application | CamelJBang | String | `--observe` | Enable observability services | 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 | false | boolean | `--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..115c6420d7e8 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 @@ -61,6 +61,7 @@ camel run [options] | `--name` | The name of the Camel application | CamelJBang | String | `--observe` | Enable observability services | 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 | false | boolean | `--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..ed56b7ae15f5 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..a856a0a8e829 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,14 @@ public class Run extends CamelCommand { dependencies.add("camel:observability-services"); main.addOverrideProperty("camel.metrics.logMetricsOnShutdown", "false"); } + if (debugOptions.openTelemetryAgent) { + dependencies.add("camel:opentelemetry2"); + 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 +1696,26 @@ 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) { + jbangArgs.add("--javaagent=io.opentelemetry.javaagent:opentelemetry-javaagent:RELEASE"); + int port = serverOptions.port > 0 ? serverOptions.port : 8080; + jbangArgs.add("-Dotel.exporter.otlp.traces.endpoint=http://localhost:" + port + "/v1/traces"); + 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("--dep=camel:platform-http-main"); + cmds.add("--dep=mvn:io.opentelemetry.proto:opentelemetry-proto:RELEASE"); + cmds.add("--prop=camel.opentelemetry2.enabled=true"); + if (cmds.stream().noneMatch(a -> a.startsWith("--port"))) { + cmds.add("--port=" + port); + } + } if (javaVersion != null) { jbangArgs.add("--java=" + javaVersion); @@ -2412,6 +2436,10 @@ 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") + boolean openTelemetryAgent; } public static class ExecutionLimitOptions { 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..9afb360b667e 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); @@ -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; } 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..6ddcce8d9b05 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,7 @@ class RunOptionsForm { private boolean observe; private boolean backlogTrace; private boolean stubMode; + private boolean otelAgent; private String exampleTitle; @@ -95,6 +97,7 @@ class RunOptionsForm { observe = false; backlogTrace = false; stubMode = false; + otelAgent = false; selectedRow = ROW_NAME; page = PAGE_OPTIONS; selectedProperty = 0; @@ -192,6 +195,9 @@ class RunOptionsForm { if (stubMode) { args.add("--stub=remote"); } + if (otelAgent) { + args.add("--open-telemetry-agent"); + } if (properties != null) { for (PropertyEntry pe : properties) { String current = pe.valueInput().text(); @@ -215,7 +221,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 +230,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 +242,7 @@ class RunOptionsForm { selectedRow = (selectedRow - 1 + ROW_COUNT) % ROW_COUNT; return true; } - if (ke.isRight() && hasProperties() && selectedRow >= ROW_STUB) { + if (ke.isRight() && hasProperties() && selectedRow >= ROW_OTEL_AGENT) { page = PAGE_PROPERTIES; selectedProperty = 0; return true; @@ -261,6 +267,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 +294,7 @@ class RunOptionsForm { editingKey = false; if (selectedProperty == 0) { page = PAGE_OPTIONS; - selectedRow = ROW_STUB; + selectedRow = ROW_OTEL_AGENT; } else { selectedProperty--; } @@ -317,7 +324,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 +340,7 @@ class RunOptionsForm { return true; } page = PAGE_OPTIONS; - selectedRow = ROW_STUB; + selectedRow = ROW_OTEL_AGENT; editingKey = false; return true; } @@ -376,7 +383,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 +448,10 @@ 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); } private void renderPropertiesPage(Frame frame, Rect area) { diff --git a/parent/pom.xml b/parent/pom.xml index 2e6e63c36427..a830731e4a38 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>
