Docs / Instrument · All

All integrations

FastAPI, Flask, Django, OpenTelemetry, OpenLLMetry, Langfuse, Grafana Loki, Tempo, Honeycomb, Datadog. Side-by-side setup for each transport.

Langfuse

Root-cause analysis needs no Langfuse credentials at all. POST /v1/signals/{id}/explain takes no request body and runs entirely against Dunetrace's own stored events. Connecting Langfuse is a separate, optional step to pull its own evaluation results into the same dashboard.

Connect the integration

POST /v1/orgs/integrations/langfuse
Authorization: Bearer dt_live_...
Content-Type: application/json

{
  "endpoint_url": "https://cloud.langfuse.com",
  "public_key": "pk-lf-...",
  "secret_key": "sk-lf-...",
  "poll_interval_secs": 60
}

A background poller checks every 60 seconds (configurable) for new Langfuse evaluation results and writes them alongside your structural and semantic signals, tagged source: "langfuse", correlated to the same runs via trace_id. Credentials are encrypted at rest. GET/DELETE /v1/orgs/integrations/langfuse to check status or disconnect.

Root-cause analysis (native, works with or without Langfuse)

POST /v1/signals/{signal_id}/explain
Authorization: Bearer <your-key>

Returns root_cause, fix_category, fix_type, and apply_blocked.

fix_typeMeaningApply path
policyRuntime guardrail (TOOL_LOOP, RETRY_STORM, etc.)POST /v1/policies — Dunetrace enforces it directly
prompt_additionOne sentence to append to the system promptNo automated apply path — copy manually
code_changeCode or infrastructure fix (CONTEXT_BLOAT, SLOW_STEP, etc.)POST /v1/signals/{id}/open-pr — draft GitHub PR
no_auto_applySecurity signal (PROMPT_INJECTION_SIGNAL)Never auto-apply — blocked at the API level

Track fix effectiveness

GET /v1/signals/{signal_id}/fix-status
Authorization: Bearer <your-key>

Returns a verdict: verified (≥10 runs, 0 recurrences), likely_fixed (≥5 runs, 0 recurrences), still_occurring, or insufficient_data.

What each tool sees

ConcernDunetraceLangfuse
Raw prompts & completionsSelf-hosted — stays in your own PostgresYes — full content, on Langfuse's servers (unless self-hosted)
Structural failures (loops, stalls…)Automatic, 29 detectorsManual inspection
Proactive alertingSlack / webhook in <15sNo — passive
Root-cause fix pathNative explain + policy apply / draft PRManual editing
Trace timelineStep graph + event logFull span tree with content

See the LangChain guide for how to connect this integration alongside LangChain instrumentation.


OpenTelemetry

pip install 'dunetrace[otel]' opentelemetry-exporter-otlp-proto-grpc
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from dunetrace.integrations.otel import DunetraceOTelExporter

resource = Resource.create({
    "service.name": "my-agent-service",
    "deployment.environment": "production",
})
provider = TracerProvider(resource=resource)
provider.add_span_processor(SimpleSpanProcessor(OTLPSpanExporter()))

dt = Dunetrace(otel_exporter=DunetraceOTelExporter(provider))

Each agent run produces a trace with a deterministic trace_id derived from run_id. Failure signals at run end are written as indexed attributes on the root span (dunetrace.signal.0.failure_type, .severity, .confidence). HIGH / CRITICAL signals set span.status = ERROR.

See the dedicated OpenTelemetry guide for both directions (export and receive) across the Python and TypeScript SDKs, the full env-var reference, and the semantic conventions.

OpenLLMetry / OTel receiver

OpenLLMetry instruments 40+ AI frameworks and emits standard gen_ai.* OTel spans. Add DunetraceOTelReceiver as a second exporter and get structural detection with zero agent-code changes.

pip install 'dunetrace[otel]'
from dunetrace.integrations.otel_receiver import DunetraceOTelReceiver

DunetraceOTelReceiver.attach(provider, dt, agent_id="my-agent")

from traceloop.sdk import Traceloop
Traceloop.init(app_name="my-agent", tracer_provider=provider)

Each OTel trace becomes one Dunetrace run. Spans with gen_ai.request.model translate to llm_called / llm_responded. Spans with gen_ai.tool.name become tool_called / tool_responded.

gen_ai.* attribute handling

AttributeHandling
gen_ai.request.modelPassed as-is (not sensitive)
gen_ai.usage.prompt_tokensPassed as-is
gen_ai.usage.completion_tokensPassed as-is
gen_ai.completion.0.finish_reasonPassed as-is
gen_ai.tool.namePassed as-is
gen_ai.prompt / gen_ai.completionNot read — the OTel mapper only extracts structural attributes (model, tokens, finish reasons, tool names), not content fields
gen_ai.prompt.0.contentNot read — same as above

This is narrower than the SDK's own instrumentation, which does capture prompt/tool/output content directly (see Privacy Policy) — the OTel receiver exists for agents that only emit spans, and today only maps the structural subset of gen_ai.* attributes.

FastAPI / ASGI

from fastapi import FastAPI
from dunetrace import Dunetrace, DunetraceASGIMiddleware, get_current_run

dt = Dunetrace()
dt.auto_instrument()

app = FastAPI()
app.add_middleware(
    DunetraceASGIMiddleware,
    dt=dt, agent_id="my-api", model="gpt-4o",
)

@app.post("/chat")
async def chat(query: str):
    run = get_current_run()        # run opened by middleware
    resp = await openai_client.chat.completions.create(
        model="gpt-4o", messages=[{"role": "user", "content": query}],
    )
    return resp.choices[0].message.content

The run is also available on request.state.dunetrace_run.

Flask / WSGI

from flask import Flask
from dunetrace import Dunetrace, DunetraceWSGIMiddleware

dt = Dunetrace()
dt.auto_instrument()

app = Flask(__name__)
app.wsgi_app = DunetraceWSGIMiddleware(app.wsgi_app, dt=dt, agent_id="my-api")

Django

# wsgi.py
from dunetrace import Dunetrace, DunetraceWSGIMiddleware
from django.core.wsgi import get_wsgi_application

dt = Dunetrace()
dt.auto_instrument()
application = DunetraceWSGIMiddleware(get_wsgi_application(), dt=dt, agent_id="django-api")

Auto-instrumentation

dt.auto_instrument() patches supported AI framework clients at the class level so every LLM call made inside a dt.run() context (or inside a @dt.agent() function or middleware-wrapped request) is tracked automatically.

Supported frameworks: openai, anthropic, httpx, requests. Uninstalled frameworks are silently skipped. Calling auto_instrument() more than once is safe.

dt.auto_instrument()                         # patch all installed
dt.auto_instrument(["openai", "anthropic"])  # LLM clients only
dt.auto_instrument(["httpx", "requests"])    # HTTP clients only

Grafana / Loki

dt = Dunetrace(emit_as_json=True)

Writes every event to stdout as a Loki-compatible NDJSON line. Each line includes ts, level, logger, event_type, agent_id, run_id, step_index, payload.

Minimal Promtail pipeline stage:

pipeline_stages:
  - json:
      expressions: {ts: ts, event_type: event_type, agent_id: agent_id}
  - timestamp:
      source: ts
      format: RFC3339Nano
  - labels:
      agent_id:
      event_type:

get_current_run()

Returns the active RunContext for the current async task or thread, or None. Works inside @dt.agent(), ASGI middleware, WSGI middleware, and direct dt.run().

from dunetrace import get_current_run

def some_helper():
    run = get_current_run()
    if run:
        run.tool_called("cache_lookup")
        result = cache.get(key)
        run.tool_responded("cache_lookup", success=result is not None)
        return result

Policies

Runtime guardrails evaluated mid-run after every tool_called, llm_responded, and tool_responded event. Policies fire at most once per run (except log policies, which fire every time).

Local policies (no backend required)

from dunetrace import Dunetrace, PolicyViolation

dt = Dunetrace()

# Stop the run if tool calls exceed 5
dt.add_policy(
    name="cap tool calls",
    condition={"trigger": "tool_call_count", "operator": "gt", "value": 5},
    action={"type": "stop"},
)

# Downgrade model when estimated cost exceeds $0.50
dt.add_policy(
    name="cost cap",
    condition={"trigger": "cost_usd", "operator": "gt", "value": 0.50},
    action={"type": "switch_model", "params": {"model": "gpt-4o-mini"}},
)

# Inject a corrective prompt when a loop is detected mid-run
dt.add_policy(
    name="loop fix",
    condition={"trigger": "signal", "operator": "eq", "value": "TOOL_LOOP"},
    action={"type": "inject_prompt", "params": {"prompt": "Stop repeating tool calls. Summarise what you know and answer directly."}},
)

Remote policies (dashboard-managed)

When api_key and endpoint are set, the SDK fetches policies from the backend at run start and caches them for 60 seconds. Policies defined in the dashboard apply automatically — no code changes needed.

dt = Dunetrace(api_key="dt_live_...", endpoint="https://ingest.dunetrace.com")
# Policies defined in the Policies page are pulled at run start.

Local policies (added via add_policy()) take priority over remote ones at the same priority level.

Condition reference

TriggerTypeWhat it measures
tool_call_countintTotal tool calls in the run so far
step_countintCurrent step index
cost_usdfloatAccumulated LLM cost in USD (model-aware pricing)
error_countintFailed tool calls (success=False)
finish_reasonstrLatest LLM finish_reason (e.g. "length", "stop")
llm_latency_msintLatest LLM call latency in milliseconds
signalstrDetector signal name — runs the full detector suite lazily (e.g. "TOOL_LOOP")

Supported operators: gt gte lt lte eq neq contains

Action reference

Action typeEffect
stopRaises PolicyViolation; run exits with exit_reason="policy_violation"
switch_modelSets run.model_override — read it between steps to switch the model
inject_promptAppends to run.prompt_additions — pop with run.pop_prompt_addition() and prepend to next LLM call
logEmits policy.triggered event; no interruption; fires on every matching event

Dashboard CRUD

Policies can be created, edited, toggled, and deleted from the Policies page at http://localhost:3000. The backend REST API:

EndpointDescription
GET /v1/policiesList all policies
POST /v1/policiesCreate a policy
PUT /v1/policies/{id}Replace a policy
DELETE /v1/policies/{id}Delete a policy
PATCH /v1/policies/{id}/toggleEnable / disable

LangSmith

Pull LangSmith evaluation results in and correlate them to Dunetrace runs via trace_id — same OTel trace_id your agent already sends, no extra instrumentation needed.

POST /v1/orgs/integrations/langsmith
Authorization: Bearer dt_live_...

{
  "endpoint_url": "https://api.smith.langchain.com",
  "api_key": "ls-...",
  "project_name": "my-project",
  "poll_interval_secs": 60
}

A background poller checks every 60 seconds (configurable) for new evaluation results and writes them alongside your structural and semantic signals.

Braintrust

POST /v1/orgs/integrations/braintrust
Authorization: Bearer dt_live_...

{
  "endpoint_url": "https://api.braintrust.dev",
  "api_key": "bt-...",
  "project_id": "proj-...",
  "poll_interval_secs": 60
}

Same shape as Langfuse and LangSmith above. GET/DELETE /v1/orgs/integrations/braintrust to check status or disconnect. Credentials are encrypted at rest.


Slack & Linear

Route signals to Slack, Linear, or both. Every alert includes failure type, severity, root cause, a suggested fix, and a link back to the run.

SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...
SLACK_MIN_SEVERITY=LOW   # LOW | MEDIUM | HIGH | CRITICAL

Slack alerts include three action buttons: Mark false positive, Snooze this pattern, and View run (deep-links to the run detail panel). Linear issues are created with the root cause and trace excerpt, and automatically closed when the underlying signal is resolved in Dunetrace.

Configure which failure types route to which destination per detector — see Alerts.

GitHub App

For code_change fixes (CONTEXT_BLOAT, SLOW_STEP, and similar), Dunetrace can open a draft PR with the fix already applied.

  • Install the Dunetrace GitHub App on the repos you want it to touch — minimal scopes (contents, pull requests)
  • Two-tier source mapping resolves which file a signal corresponds to: explicit per-agent config, or the SDK's own auto-detected file path matched against your repo's real file tree
  • When source mapping resolves, the PR edits the real file — the diff is computed from the actual before/after file content, never LLM-authored freehand
  • Security guardrails block CI config, secrets, and infra files from ever being a write target, regardless of what any LLM suggests
  • PRs are always opened as drafts — you promote to ready

Claude Code, Cursor & Codex

The Dunetrace MCP server exposes agent signals, run details, and health scores directly to your coding agent — ask it things like "is my agent healthy?" or "walk me through this run step by step" without leaving your editor.

pip install dunetrace-mcp

Claude Code registers it automatically in ~/.claude.json. Cursor needs a .cursor/mcp.json entry pointing at dunetrace-mcp. Codex and other SSE-compatible clients run dunetrace-mcp --sse. Full tool reference at MCP server.