Langfuse
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_type | Meaning | Apply path |
|---|---|---|
policy | Runtime guardrail (TOOL_LOOP, RETRY_STORM, etc.) | POST /v1/policies — Dunetrace enforces it directly |
prompt_addition | One sentence to append to the system prompt | No automated apply path — copy manually |
code_change | Code or infrastructure fix (CONTEXT_BLOAT, SLOW_STEP, etc.) | POST /v1/signals/{id}/open-pr — draft GitHub PR |
no_auto_apply | Security 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
| Concern | Dunetrace | Langfuse |
|---|---|---|
| Raw prompts & completions | Self-hosted — stays in your own Postgres | Yes — full content, on Langfuse's servers (unless self-hosted) |
| Structural failures (loops, stalls…) | Automatic, 29 detectors | Manual inspection |
| Proactive alerting | Slack / webhook in <15s | No — passive |
| Root-cause fix path | Native explain + policy apply / draft PR | Manual editing |
| Trace timeline | Step graph + event log | Full 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
| Attribute | Handling |
|---|---|
gen_ai.request.model | Passed as-is (not sensitive) |
gen_ai.usage.prompt_tokens | Passed as-is |
gen_ai.usage.completion_tokens | Passed as-is |
gen_ai.completion.0.finish_reason | Passed as-is |
gen_ai.tool.name | Passed as-is |
gen_ai.prompt / gen_ai.completion | Not read — the OTel mapper only extracts structural attributes (model, tokens, finish reasons, tool names), not content fields |
gen_ai.prompt.0.content | Not 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
| Trigger | Type | What it measures |
|---|---|---|
tool_call_count | int | Total tool calls in the run so far |
step_count | int | Current step index |
cost_usd | float | Accumulated LLM cost in USD (model-aware pricing) |
error_count | int | Failed tool calls (success=False) |
finish_reason | str | Latest LLM finish_reason (e.g. "length", "stop") |
llm_latency_ms | int | Latest LLM call latency in milliseconds |
signal | str | Detector signal name — runs the full detector suite lazily (e.g. "TOOL_LOOP") |
Supported operators: gt gte lt lte eq neq contains
Action reference
| Action type | Effect |
|---|---|
stop | Raises PolicyViolation; run exits with exit_reason="policy_violation" |
switch_model | Sets run.model_override — read it between steps to switch the model |
inject_prompt | Appends to run.prompt_additions — pop with run.pop_prompt_addition() and prepend to next LLM call |
log | Emits 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:
| Endpoint | Description |
|---|---|
GET /v1/policies | List all policies |
POST /v1/policies | Create a policy |
PUT /v1/policies/{id} | Replace a policy |
DELETE /v1/policies/{id} | Delete a policy |
PATCH /v1/policies/{id}/toggle | Enable / 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.