Docs Integrations
OpenTelemetry recipes
Send CrewAI, LlamaIndex, Pydantic AI or Google ADK traces to Runback with the standard OpenTelemetry exporter, and no Runback package in your code. About ten minutes, plus a minute for the first run to appear.
Before you begin
- A Runback account and a Send only key. In the app, open Settings › Keys and access › API key, keep Send only selected, select Generate API key and copy it (it is shown once and starts
rb_cloud_). The quickstart walks through it. - Python 3.9 or later (
python3 --version), and your framework already installed.
Steps
Install the OpenTelemetry SDK and exporter
terminalShell pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-httpThen install your framework's instrumentor from the table in step 4. For LlamaIndex, for example:
terminalShell pip install openinference-instrumentation-llama-indexExpected outputpip ends with aSuccessfully installedline and no errors.Point the exporter at Runback
Two standard variables send traces to Runback, authenticated with your key.
terminalShell export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://runback.dev/api/otel/v1/traces" export OTEL_EXPORTER_OTLP_TRACES_HEADERS="authorization=Bearer <RUNBACK_API_KEY>"Expected outputNothing is printed. To check, runenv | grep OTEL_: it should list both variables, with your key in the second.Add the tracer setup
Save this as
otel_setup.pyand import it once, before your agent runs. It creates the tracer providertpthat the instrumentor in step 4 uses.otel_setup.pyPython # otel_setup.py: pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http # The exporter reads OTEL_EXPORTER_OTLP_TRACES_ENDPOINT and _HEADERS from the environment. from opentelemetry import trace from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor tp = TracerProvider() tp.add_span_processor(BatchSpanProcessor(OTLPSpanExporter())) trace.set_tracer_provider(tp) # Then instrument your framework with tp, for example Pydantic AI: # from pydantic_ai import Agent # from pydantic_ai.models.instrumented import InstrumentationSettings # Agent.instrument_all(InstrumentationSettings(tracer_provider=tp))Instrument your framework
Add the line for your framework after the setup, passing
tp:Framework Instrumentation Still missing from the stored run Pydantic AI Agent.instrument_all(InstrumentationSettings(tracer_provider=tp))Nothing known LlamaIndex OpenInference LlamaIndexInstrumentor().instrument(tracer_provider=tp)A duplicate model step per call, without tokens or tools CrewAI OpenInference CrewAIInstrumentorandOpenAIInstrumentor, both withtracer_provider=tp; crewai 1.10.1 or laterNothing known Google ADK OpenInference GoogleADKInstrumentor().instrument(tracer_provider=tp); google-adk 2.10 or laterProvider shows as gcp.vertex.agentEach gap in the last column is checked by Runback's test suite, so this table changes the day the behaviour does.
Run your agent and check the run arrived
Run your agent as usual. Runback writes the run once the trace has been quiet for about a minute, so wait a minute, then open Runs in the app. The run is named after your agent's root span.
Troubleshooting
| You see | What it means | Fix |
|---|---|---|
The exporter logs 401 or Failed to export | The authorization header is missing or the key is wrong. | Export both OTEL_EXPORTER_OTLP_TRACES_* variables in this terminal; the key starts rb_cloud_, with a space after Bearer. |
ModuleNotFoundError: No module named 'opentelemetry' | The packages went into a different Python environment. | Activate the environment you installed into, or install with the same python you run the agent with. |
| The agent finished, but the run is not in Runs yet | Runback writes an OpenTelemetry run once the trace has been quiet for about a minute. | Wait a minute and refresh. If it is still missing, check the workspace name in the top right of the app is the one the key came from. |
What Runback records
The endpoint takes protobuf or JSON, gzipped or not. A trace exported in several batches is still one run: spans are buffered and the run is written once the trace goes quiet.
Pydantic AI's own instrumentation, exported over OTLP, scripted model. 5 events, captured 9 Oct 2026.
Runstart · agent
{ "schema_version": 1, "run_id": "e9d431b7baf6d0e86430ab8373b43b49", "span_id": "run:e9d431b7baf6d0e86430ab8373b43b49", "parent_span_id": null, "seq": 0, "ts_start": "2026-10-09T10:56:14.447Z", "ts_end": null, "type": "run", "phase": "start", "name": "agent", "input": null, "output": null, "status": "running", "error": null, "metadata": { "source": "otel", "trace_id": "e9d431b7baf6d0e86430ab8373b43b49" } }Modelgpt-4o-mini → weather
{ "schema_version": 1, "run_id": "e9d431b7baf6d0e86430ab8373b43b49", "span_id": "2f7dbc4b853f9c26", "parent_span_id": "run:e9d431b7baf6d0e86430ab8373b43b49", "seq": 1, "ts_start": "2026-10-09T10:56:14.450Z", "ts_end": "2026-10-09T10:56:14.494Z", "type": "llm", "model": { "provider": "openai", "model_id": "gpt-4o-mini" }, "request": { "system": "You are a weather bot.", "messages": [ { "role": "user", "content": "What is the weather in Paris? cc [redacted:email]" } ], "tools": [ { "name": "weather", "description": "Weather for a city.", "parameters": { "additionalProperties": false, "properties": { "city": { "type": "string" } }, "required": [ "city" ], "type": "object" } } ], "params": {} }, "response": { "text": null, "reasoning": null, "finish_reason": "tool_call", "tool_calls": [ { "tool_call_id": "call_40", "tool_name": "weather", "input": { "city": "Paris" } } ] }, "usage": { "input_tokens": 12, "output_tokens": 5, "total_tokens": 17 }, "latency_ms": 44, "error": null }Toolweather
{ "schema_version": 1, "run_id": "e9d431b7baf6d0e86430ab8373b43b49", "span_id": "b5929b8cfe877dd6", "parent_span_id": "run:e9d431b7baf6d0e86430ab8373b43b49", "seq": 2, "ts_start": "2026-10-09T10:56:14.498Z", "ts_end": "2026-10-09T10:56:14.499Z", "type": "tool", "tool_name": "weather", "tool_call_id": "call_40", "input": { "city": "Paris" }, "output": { "city": "Paris", "forecast": "sunny" }, "latency_ms": 1, "error": null }Modelgpt-4o-mini
{ "schema_version": 1, "run_id": "e9d431b7baf6d0e86430ab8373b43b49", "span_id": "231f2135b08ec268", "parent_span_id": "run:e9d431b7baf6d0e86430ab8373b43b49", "seq": 3, "ts_start": "2026-10-09T10:56:14.500Z", "ts_end": "2026-10-09T10:56:14.504Z", "type": "llm", "model": { "provider": "openai", "model_id": "gpt-4o-mini" }, "request": { "system": "You are a weather bot.", "messages": [ { "role": "user", "content": "What is the weather in Paris? cc [redacted:email]" }, { "role": "assistant", "content": [ { "type": "tool_call", "id": "call_40", "name": "weather", "arguments": { "city": "Paris" } } ] }, { "role": "user", "content": [ { "type": "tool_call_response", "id": "call_40", "name": "weather", "result": "{\"city\": \"Paris\", \"forecast\": \"sunny\"}" } ] } ], "tools": [ { "name": "weather", "description": "Weather for a city.", "parameters": { "additionalProperties": false, "properties": { "city": { "type": "string" } }, "required": [ "city" ], "type": "object" } } ], "params": {} }, "response": { "text": "It is sunny in Paris.", "reasoning": null, "finish_reason": "stop", "tool_calls": [] }, "usage": { "input_tokens": 20, "output_tokens": 8, "total_tokens": 28 }, "latency_ms": 3, "error": null }Runend · agent
{ "schema_version": 1, "run_id": "e9d431b7baf6d0e86430ab8373b43b49", "span_id": "run:e9d431b7baf6d0e86430ab8373b43b49~end", "parent_span_id": "run:e9d431b7baf6d0e86430ab8373b43b49", "seq": 4, "ts_start": "2026-10-09T10:56:14.505Z", "ts_end": "2026-10-09T10:56:14.505Z", "type": "run", "phase": "end", "name": "agent", "input": null, "output": null, "status": "success", "error": null, "metadata": {} }
A real OpenTelemetry export, as Runback stores it after mapping and redaction. Runback's test suite re-checks it on every change.
What it can and cannot do
OpenTelemetry
| Question | Answer | Why |
|---|---|---|
| Code changes | None | Your framework's own instrumentation sends the spans; no Runback package. |
| Context | What's exported | Messages, system prompt and tool definitions when the instrumentor exports them (GenAI, OpenLLMetry, OpenInference). |
| Redaction | On arrival | Redacted by Runback when the spans arrive, not inside your process. |
| Stop before it runs | No | Spans arrive after the call ran, so nothing can stop it. |
| Replay | Re-issue only | A model step can be re-sent to a live model. The whole run can't be re-run with the same tool results: they aren't kept. |
| Arrives | About a minute | A run is written once its trace goes quiet, about a minute after the root span. |
| Signed record | Yes | Each run is hash-chained and can be exported as a signed record. |
Next steps
- Want to stop calls, or redact inside your process? Use the Python SDK instead.
- See what a run holds: Capture.