How it works
There are two independent directions, and you can use either, both, or neither:
Direction 1 — Export: Dunetrace SDK ──► OTLP spans ──► Datadog / Grafana / Honeycomb / Signoz
Direction 2 — Receive: your OTel tracer ──► gen_ai.* spans ──► Dunetrace detectors ──► dashboard
- Export is additive: the same events still ship to Dunetrace's own ingest, and a copy is emitted as OpenTelemetry spans so a run shows up in your existing tracing backend too.
- Receive is for agents already instrumented with an OTel-based tracer (OpenLLMetry, Traceloop, OpenLIT) or a no-code platform that emits OTLP — Dunetrace translates the spans into events and runs the full detector suite, no
dt.run()calls required.
gen_ai.provider.name (not the deprecated gen_ai.system), and HTTP-shaped tool calls use the stable HTTP conventions (url.full, server.address, http.response.status_code).Prerequisites
- Dunetrace backend running (
docker compose up -d) - Python:
pip install 'dunetrace[otel]'— pulls in the OpenTelemetry SDK and OTLP exporter - TypeScript:
npm install @opentelemetry/api @opentelemetry/sdk-trace-basealongsidedunetrace(optional peer dependencies)
Direction 1 · Export Dunetrace events to your OTel backend
Python — env-driven (zero code)
When DUNETRACE_OTEL_ENABLED is set, the client builds the exporter automatically. Export runs on a bounded background pipeline with a circuit breaker, so a slow or dead collector never blocks the agent thread or raises into your code.
export DUNETRACE_OTEL_ENABLED=1
export DUNETRACE_OTEL_ENDPOINT=https://otlp.example:4317
export DUNETRACE_OTEL_HEADERS="DD-API-KEY=xxxxx" # provider auth, "k1=v1,k2=v2"
export DUNETRACE_OTEL_PROTOCOL=grpc # or http/protobuf
| Variable | Default | Description |
|---|---|---|
DUNETRACE_OTEL_ENABLED | off | 1/true turns export on |
DUNETRACE_OTEL_ENDPOINT | — | OTLP endpoint URL (required when enabled) |
DUNETRACE_OTEL_HEADERS | — | Auth headers, k1=v1,k2=v2 |
DUNETRACE_OTEL_PROTOCOL | grpc | grpc or http/protobuf |
DUNETRACE_OTEL_SERVICE_NAME | dunetrace | service.name resource attribute |
DUNETRACE_OTEL_SAMPLING_RATIO | 1.0 | Head sampling ratio, 0.0–1.0 |
DUNETRACE_OTEL_CAPTURE_CONTENT | true | Include content attributes (tool args, URL, retrieval query) |
Python — programmatic (your own TracerProvider)
from opentelemetry.sdk.trace import TracerProvider
from dunetrace import Dunetrace
from dunetrace.integrations.otel import DunetraceOTelExporter
provider = TracerProvider() # your pipeline, with whatever exporters you like
dt = Dunetrace(otel_exporter=DunetraceOTelExporter(tracer_provider=provider))
TypeScript
import { Dunetrace } from "dunetrace";
import { DunetraceOtelExporter } from "dunetrace/integrations/otel";
import { trace } from "@opentelemetry/api";
const dt = new Dunetrace({
exporter: new DunetraceOtelExporter({ tracer: trace.getTracer("dunetrace") }),
});
Every emitted event is also handed to the exporter. It never throws — a failure in the exporter is caught and never touches the agent's own event path.
What gets exported
Trace (trace_id derived from run_id, so a Dunetrace run is exactly one trace)
└── span "dunetrace.run" [dunetrace.run.id, .agent_id, .status, ...]
├── span "chat {model}" [gen_ai.provider.name, gen_ai.request.model,
│ gen_ai.usage.input_tokens/output_tokens, ...]
├── span "dunetrace.tool.{name}" [url.full / server.address / http.request.method if HTTP]
├── span "dunetrace.retrieval" [dunetrace.retrieval.vector_store, .document_count]
├── span "dunetrace.voice.transcription" / ".tts"
└── event "dunetrace.voice.{vad}" [silence / speech_start / barge_in] on the run span
Direction 2 · Send your OpenTelemetry traces to Dunetrace
Option A — Zero-code OTLP/HTTP endpoint
If your agent or platform already emits OpenTelemetry traces, point its exporter at Dunetrace's receiver — no SDK, no code change.
POST http://<dunetrace-host>:8001/v1/otlp/traces
Authorization: Bearer <api_key> # optional in dev mode
The endpoint accepts both application/json and application/x-protobuf (the default for most OTel Collectors and OTLPSpanExporter), gzip-compressed or not.
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
provider = TracerProvider(resource=Resource.create({"service.name": "my-agent"}))
provider.add_span_processor(BatchSpanProcessor(
OTLPSpanExporter(
endpoint="http://localhost:8001/v1/otlp/traces",
headers={"Authorization": "Bearer YOUR_API_KEY"},
)
))
No-code platforms (Langdock, Dify, and similar): set the OTLP/tracing endpoint in your workspace settings. Cloud platforms cannot reach localhost — use a tunnel (ngrok) or your deployed host.
Option B — In-process receiver (Python)
Already running the Dunetrace SDK next to an OTel pipeline? Attach the receiver as a second span processor — same result as the HTTP endpoint, without leaving the process.
from opentelemetry.sdk.trace import TracerProvider
from dunetrace import Dunetrace
from dunetrace.integrations.otel_receiver import DunetraceOTelReceiver
dt = Dunetrace(api_key="dt_live_...")
provider = TracerProvider() # your existing pipeline, unchanged
DunetraceOTelReceiver.attach(provider, dt, agent_id="my-agent")
Option B — In-process receiver (TypeScript)
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { Dunetrace } from "dunetrace";
import { DunetraceOtelReceiver } from "dunetrace/integrations/otel-receiver";
const dt = new Dunetrace({ apiKey: "dt_live_..." });
const provider = new NodeTracerProvider({
spanProcessors: [new SimpleSpanProcessor(new DunetraceOtelReceiver(dt, "my-agent"))],
});
provider.register();
Attributes read
The receiver reads the current GenAI semantic conventions first, then the OpenLLMetry / vector-store keys real emitters (OpenLIT, Traceloop) also use — including Traceloop's structured gen_ai.output.messages and the legacy prompt_tokens / completion_tokens naming.
| Span kind | Detected by | Emits |
|---|---|---|
| Root (no parent) | — | run.started + run.completed / run.errored |
| LLM | gen_ai.request.model, gen_ai.provider.name, llm.request.model | llm.called + llm.responded |
| Tool | gen_ai.tool.name, tool.name | tool.called + tool.responded |
| Retrieval | retrieval.index_name, vector_db.collection_name, db.name | retrieval.called + retrieval.responded |
Agent identity comes from the service.name resource attribute (override per-request with the X-Dunetrace-Agent-Id header on the HTTP endpoint, or the constructor label on the in-process receiver). All detectors activate automatically on each completed trace.
Semantic conventions
- LLM spans follow the OpenTelemetry GenAI conventions (
gen_ai.*). Provider isgen_ai.provider.name, tokens aregen_ai.usage.input_tokens/output_tokens, and finish reasons aregen_ai.response.finish_reasons(an array). - HTTP-shaped tool calls use the current stable HTTP conventions (
url.full,server.address,http.request.method,http.response.status_code), not the deprecatedhttp.url/http.method/http.status_code. - Agent runs, tools, retrievals, and voice have no standard OTel operation type, so those fields are namespaced under
dunetrace.*.
PII and content capture
Content-bearing attributes (tool arguments, request URL, retrieval query) are emitted only when content capture is on — the default. Disable it with DUNETRACE_OTEL_CAPTURE_CONTENT=false (Python) or new DunetraceOtelExporter({ captureContent: false }) (TypeScript). Voice spans never carry raw transcript or TTS text, only character counts and metadata.
Correlation
A run's trace id is derived deterministically from its run_id (the UUID as a 128-bit integer), and the root span id is its lower 64 bits. A Dunetrace run therefore maps to one stable, addressable trace that both the Dunetrace dashboard and your backend can deep-link to. A child run (with a parent) gets its own trace plus a span link back to the parent run's root span — the OTel-correct way to express causally related spans across traces.
Troubleshooting
Nothing appears in my OTel backend
- Confirm
DUNETRACE_OTEL_ENABLEDis set andDUNETRACE_OTEL_ENDPOINTis reachable from the agent host. - Check the protocol matches your collector:
grpc(port 4317) vshttp/protobuf(port 4318). - Export failures are swallowed by design — enable debug logging on the
dunetrace.otellogger to see circuit-breaker events.
Traces arrive but no signals fire
- Signals are decided on the Dunetrace side after a trace completes — confirm the root span reaches the receiver (it closes the run).
- Check the span attributes match the keys in the "Attributes read" table; a span with none of them is treated as a lifecycle span and skipped.