This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch feature/CAMEL-23706-otel-dev-exporter in repository https://gitbox.apache.org/repos/asf/camel.git
commit 2a911d4b9c509987ddd8290955330c3758f693c6 Author: Claus Ibsen <[email protected]> AuthorDate: Sun Jun 7 11:26:28 2026 +0200 CAMEL-23706: camel-opentelemetry2 - Auto-configure in-memory span exporter for dev profile When the Camel profile is "dev" (default for JBang), auto-configure an in-memory SpanExporter so OTel spans are captured locally without requiring an external collector. A new dev console endpoint exposes the captured spans as JSON for tooling access. Co-Authored-By: Claude <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../apache/camel/dev-console/opentelemetry.json | 15 +++ .../org/apache/camel/dev-console/opentelemetry | 2 + .../org/apache/camel/dev-consoles.properties | 7 ++ .../camel/opentelemetry2/DevSpanExporter.java | 91 ++++++++++++++ .../opentelemetry2/OpenTelemetryDevConsole.java | 131 +++++++++++++++++++++ .../camel/opentelemetry2/OpenTelemetryTracer.java | 35 ++++++ 6 files changed, 281 insertions(+) diff --git a/components/camel-opentelemetry2/src/generated/resources/META-INF/org/apache/camel/dev-console/opentelemetry.json b/components/camel-opentelemetry2/src/generated/resources/META-INF/org/apache/camel/dev-console/opentelemetry.json new file mode 100644 index 000000000000..a29fc885c3d8 --- /dev/null +++ b/components/camel-opentelemetry2/src/generated/resources/META-INF/org/apache/camel/dev-console/opentelemetry.json @@ -0,0 +1,15 @@ +{ + "console": { + "kind": "console", + "group": "camel", + "name": "opentelemetry", + "title": "OpenTelemetry Spans", + "description": "OpenTelemetry span data captured in dev mode", + "deprecated": false, + "javaType": "org.apache.camel.opentelemetry2.OpenTelemetryDevConsole", + "groupId": "org.apache.camel", + "artifactId": "camel-opentelemetry2", + "version": "4.21.0-SNAPSHOT" + } +} + diff --git a/components/camel-opentelemetry2/src/generated/resources/META-INF/services/org/apache/camel/dev-console/opentelemetry b/components/camel-opentelemetry2/src/generated/resources/META-INF/services/org/apache/camel/dev-console/opentelemetry new file mode 100644 index 000000000000..578d2605e15f --- /dev/null +++ b/components/camel-opentelemetry2/src/generated/resources/META-INF/services/org/apache/camel/dev-console/opentelemetry @@ -0,0 +1,2 @@ +# Generated by camel build tools - do NOT edit this file! +class=org.apache.camel.opentelemetry2.OpenTelemetryDevConsole diff --git a/components/camel-opentelemetry2/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties b/components/camel-opentelemetry2/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties new file mode 100644 index 000000000000..ab2c1c412824 --- /dev/null +++ b/components/camel-opentelemetry2/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties @@ -0,0 +1,7 @@ +# Generated by camel build tools - do NOT edit this file! +dev-consoles=opentelemetry +groupId=org.apache.camel +artifactId=camel-opentelemetry2 +version=4.21.0-SNAPSHOT +projectName=Camel :: Opentelemetry 2 +projectDescription=Implementation of Camel Opentelemetry based on the Camel Telemetry spec diff --git a/components/camel-opentelemetry2/src/main/java/org/apache/camel/opentelemetry2/DevSpanExporter.java b/components/camel-opentelemetry2/src/main/java/org/apache/camel/opentelemetry2/DevSpanExporter.java new file mode 100644 index 000000000000..3dfca59782a5 --- /dev/null +++ b/components/camel-opentelemetry2/src/main/java/org/apache/camel/opentelemetry2/DevSpanExporter.java @@ -0,0 +1,91 @@ +/* + * 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.Collection; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; + +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; + +/** + * In-memory {@link SpanExporter} for development use. Stores finished spans in a bounded queue so they can be queried + * by the dev console and other local tooling. + * + * Auto-configured by {@link OpenTelemetryTracer} when the Camel profile is "dev". + */ +final class DevSpanExporter implements SpanExporter { + + static final int DEFAULT_CAPACITY = 500; + + private final Queue<SpanData> spans; + private final int capacity; + private volatile boolean stopped; + + DevSpanExporter() { + this(DEFAULT_CAPACITY); + } + + DevSpanExporter(int capacity) { + this.capacity = capacity; + this.spans = new LinkedBlockingQueue<>(capacity); + } + + @Override + public CompletableResultCode export(Collection<SpanData> spanDataList) { + if (stopped) { + return CompletableResultCode.ofSuccess(); + } + for (SpanData span : spanDataList) { + while (!spans.offer(span)) { + spans.poll(); + } + } + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + stopped = true; + return CompletableResultCode.ofSuccess(); + } + + List<SpanData> getFinishedSpans() { + return new ArrayList<>(spans); + } + + int getSpanCount() { + return spans.size(); + } + + int getCapacity() { + return capacity; + } + + void reset() { + spans.clear(); + } +} 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 new file mode 100644 index 000000000000..42059cbe85c1 --- /dev/null +++ b/components/camel-opentelemetry2/src/main/java/org/apache/camel/opentelemetry2/OpenTelemetryDevConsole.java @@ -0,0 +1,131 @@ +/* + * 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 java.util.Map; + +import io.opentelemetry.sdk.trace.data.SpanData; +import org.apache.camel.spi.annotations.DevConsole; +import org.apache.camel.support.CamelContextHelper; +import org.apache.camel.support.console.AbstractDevConsole; +import org.apache.camel.util.json.JsonArray; +import org.apache.camel.util.json.JsonObject; + +@DevConsole(name = "opentelemetry", displayName = "OpenTelemetry Spans", + description = "OpenTelemetry span data captured in dev mode") +public class OpenTelemetryDevConsole extends AbstractDevConsole { + + public static final String DUMP = "dump"; + public static final String LIMIT = "limit"; + + public OpenTelemetryDevConsole() { + super("camel", "opentelemetry", "OpenTelemetry Spans", + "OpenTelemetry span data captured in dev mode"); + } + + @Override + protected String doCallText(Map<String, Object> options) { + DevSpanExporter exporter = findExporter(); + if (exporter == null) { + return "OpenTelemetry in-memory exporter is not enabled (requires dev profile)\n"; + } + + String dump = (String) options.get(DUMP); + if (dump != null) { + int limit = Integer.parseInt((String) options.getOrDefault(LIMIT, "100")); + List<SpanData> spans = exporter.getFinishedSpans(); + int start = Math.max(0, spans.size() - limit); + StringBuilder sb = new StringBuilder(); + for (int i = start; i < spans.size(); i++) { + SpanData span = spans.get(i); + long durationMs = (span.getEndEpochNanos() - span.getStartEpochNanos()) / 1_000_000; + sb.append(String.format(" TraceId: %s SpanId: %s Name: %s Kind: %s Status: %s Duration: %dms%n", + span.getTraceId(), span.getSpanId(), span.getName(), + span.getKind(), span.getStatus().getStatusCode(), durationMs)); + } + return sb.toString(); + } + + StringBuilder sb = new StringBuilder(); + sb.append("Enabled: true\n"); + sb.append(String.format("Span Count: %d%n", exporter.getSpanCount())); + sb.append(String.format("Capacity: %d%n", exporter.getCapacity())); + return sb.toString(); + } + + @Override + protected JsonObject doCallJson(Map<String, Object> options) { + JsonObject root = new JsonObject(); + + DevSpanExporter exporter = findExporter(); + if (exporter == null) { + root.put("enabled", false); + return root; + } + + root.put("enabled", true); + + String dump = (String) options.get(DUMP); + if (dump != null) { + int limit = Integer.parseInt((String) options.getOrDefault(LIMIT, "100")); + List<SpanData> spans = exporter.getFinishedSpans(); + int start = Math.max(0, spans.size() - limit); + + JsonArray arr = new JsonArray(); + for (int i = start; i < spans.size(); i++) { + arr.add(spanToJson(spans.get(i))); + } + root.put("spans", arr); + } else { + root.put("spanCount", exporter.getSpanCount()); + root.put("capacity", exporter.getCapacity()); + } + + return root; + } + + private DevSpanExporter findExporter() { + return CamelContextHelper.findSingleByType(getCamelContext(), DevSpanExporter.class); + } + + @SuppressWarnings("unchecked") + private static JsonObject spanToJson(SpanData span) { + JsonObject jo = new JsonObject(); + jo.put("traceId", span.getTraceId()); + jo.put("spanId", span.getSpanId()); + String parentSpanId = span.getParentSpanId(); + if (parentSpanId != null && !parentSpanId.isEmpty() + && !"0000000000000000".equals(parentSpanId)) { + jo.put("parentSpanId", parentSpanId); + } + jo.put("name", span.getName()); + jo.put("kind", span.getKind().name()); + jo.put("status", span.getStatus().getStatusCode().name()); + jo.put("startEpochNanos", span.getStartEpochNanos()); + jo.put("endEpochNanos", span.getEndEpochNanos()); + jo.put("durationMs", (span.getEndEpochNanos() - span.getStartEpochNanos()) / 1_000_000); + + JsonObject attrs = new JsonObject(); + span.getAttributes().forEach((key, value) -> attrs.put(key.getKey(), value)); + if (!attrs.isEmpty()) { + jo.put("attributes", attrs); + } + + return jo; + } +} 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 ed810c4f66e4..3996e8a24ad6 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 @@ -24,9 +24,13 @@ import io.opentelemetry.api.baggage.Baggage; import io.opentelemetry.api.trace.SpanBuilder; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; import io.opentelemetry.context.Context; import io.opentelemetry.context.propagation.ContextPropagators; import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +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.spi.Configurer; @@ -49,12 +53,19 @@ public class OpenTelemetryTracer extends org.apache.camel.telemetry.Tracer { private Tracer tracer; private ContextPropagators contextPropagators; + private OpenTelemetrySdk devSdk; @Override protected void initTracer() { if (tracer == null) { this.tracer = CamelContextHelper.findSingleByType(getCamelContext(), Tracer.class); } + if (tracer == null) { + String profile = getCamelContext().getCamelContextExtension().getProfile(); + if ("dev".equals(profile)) { + initDevSpanExporter(); + } + } if (tracer == null) { this.tracer = GlobalOpenTelemetry.get().getTracer("camel"); } @@ -79,6 +90,21 @@ public class OpenTelemetryTracer extends org.apache.camel.telemetry.Tracer { getCamelContext().getCamelContextExtension().addInterceptStrategy(interceptStrategy); } + private void initDevSpanExporter() { + DevSpanExporter exporter = new DevSpanExporter(); + SdkTracerProvider tracerProvider = SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(exporter)) + .build(); + devSdk = OpenTelemetrySdk.builder() + .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance())) + .setTracerProvider(tracerProvider) + .build(); + this.tracer = devSdk.getTracer("camel"); + this.contextPropagators = devSdk.getPropagators(); + getCamelContext().getRegistry().bind("DevSpanExporter", exporter); + LOG.info("OpenTelemetry in-memory span exporter enabled (dev profile)"); + } + void setTracer(Tracer tracer) { this.tracer = tracer; } @@ -93,6 +119,15 @@ public class OpenTelemetryTracer extends org.apache.camel.telemetry.Tracer { LOG.info("Opentelemetry2 enabled"); } + @Override + protected void doShutdown() { + super.doShutdown(); + if (devSdk != null) { + devSdk.close(); + devSdk = null; + } + } + private class OpentelemetrySpanLifecycleManager implements SpanLifecycleManager { private final static String BAGGAGE_VAR_PREFIX = "OTEL_BAGGAGE_";
