Telemetry & OTel
Capture Ratel's search and invoke funnel as local trace events, or export it as OpenTelemetry spans — greenfield or alongside an existing provider.
Ratel emits telemetry on two deliberately separate streams (ADR 0007): local trace events recorded into a sink you configure, and OpenTelemetry spans exported as stock OTLP. Both are off by default.
This page is the Python wiring. The full event-type vocabulary and span table live on Telemetry.
Local trace sinks
Pass trace to the ToolCatalog (or SkillCatalog) constructor and the core records every search, invoke, capability-tool, upstream-MCP, and auth event into the sink:
from ratel_ai import ToolCatalog, TraceSinkConfig
catalog = ToolCatalog(
trace=TraceSinkConfig(kind="jsonl", session_id="session-1", path="/tmp/ratel.jsonl"),
)
# Every catalog.invoke, capability-tool call, and register_mcp_server call now
# appends one JSON line per event to /tmp/ratel.jsonl.TraceSinkConfig is a dataclass: kind is "noop" | "memory" | "jsonl"; session_id is required for memory and jsonl, path for jsonl. Misconfiguration raises ValueError (for example "jsonl sink requires path").
Sink kinds:
kind="noop"— drop everything (the default when notraceis passed).kind="memory"— keep events in memory; drain viacatalog.drain_trace_events(), which returns the buffered events and removes them. Useful in tests.kind="jsonl"— append one JSON line per event topath(mode0600on Unix). Best-effort, lossy on backpressure.
Inject your own events with catalog.record_event(event): the dict must deserialize to a core trace event, else the native layer raises ValueError ("invalid trace event: ...").
search_capabilities_tool records its own gateway_search event and tags the underlying catalog search's search event with origin="agent"; direct callers (catalog.search(query, k)) default to "direct". Override per call via catalog.search(query, k, "agent").
OpenTelemetry export
Independently of the local sink, the SDK emits OpenTelemetry spans for the same funnel — execute_tool, ratel.search, ratel.skill.load, ratel.upstream.register, ratel.auth.flow (the gen_ai.* / ratel.* vocabulary, catalogued in Telemetry).
The base ratel-ai install has zero dependencies. If opentelemetry or ratel_ai_telemetry is missing, every telemetry helper is a straight pass-through — zero overhead, no spans. The [otlp] extra brings both; with them installed, spans go to whatever provider is registered.
Greenfield: ship spans to Ratel Cloud
No OpenTelemetry in your stack yet? One call at startup registers a global tracer provider with a batch OTLP exporter pointed at Ratel Cloud. The exporter ships behind the [otlp] extra (without it, configure_telemetry raises ModuleNotFoundError with this install hint):
pip install 'ratel-ai[otlp]'import os
from ratel_ai import configure_telemetry
handle = configure_telemetry(
endpoint=os.environ.get("RATEL_URL", "https://cloud.ratel.sh/v1/traces"),
api_key=os.environ["RATEL_API_KEY"],
)
# ... run your agent: every ratel.* / execute_tool span now exports ...
handle.shutdown() # flush the exporter on exit (handle.force_flush() mid-run)The full keyword surface is configure_telemetry(*, api_key=None, endpoint=None, headers=None, service_name=None, capture_content=None, include_span_and_events=None) — the last two set the content-capture gate in code. It returns a per-call shutdown handle (handle.shutdown() / handle.force_flush()) over the shared Ratel-owned provider; shutting any handle down stops export for every caller. Resolution rules:
- An explicit
endpoint=beats theRATEL_URLenv var. Either way it is the full OTLP traces URL including/v1/traces; with neither set it raisesValueError. api_keyis sent asAuthorization: Bearer <key>on top of anyheaders. When omitted it falls back to theRATEL_API_KEYenv var (unless you already set anAuthorizationheader explicitly).service_namedefaults to"ratel".
configure_telemetry owns the global provider. Calling it again once Ratel's provider is registered is idempotent — you get a handle to the same shared provider. It raises RuntimeError in two cases: a foreign tracer provider is already registered (you are on the wrong path — dual-export instead), or telemetry was already shut down in this process (shutdown is terminal).
Already on OpenTelemetry: dual-export
If Langfuse or your own collector already owns the provider, the SDK's spans are already flowing to it. Skip configure_telemetry and add the span processor from ratel-ai-telemetry to send a copy of the Ratel cut to Cloud:
import os
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from ratel_ai_telemetry.otlp import ratel_span_processor
provider = trace.get_tracer_provider() # the provider your existing setup registered
if not isinstance(provider, TracerProvider):
raise RuntimeError("register your OpenTelemetry SDK provider before adding processors")
provider.add_span_processor(
ratel_span_processor(
endpoint=os.environ.get("RATEL_URL", "https://cloud.ratel.sh/v1/traces"),
api_key=os.environ["RATEL_API_KEY"],
)
)ratel_span_processor(*, api_key=None, endpoint=None, headers=None, span_filter=None, enabled=True) (pass enabled=False for a no-op processor); ratel_span_exporter(...) is the bare OTLP exporter if you wire your own processor. The default filter forwards a span if its name starts with ratel. or any attribute key starts with gen_ai. or ratel.; pass span_filter=lambda _s: True to forward everything.
Content capture
Message and tool content never leaves the process by default. The gated payloads (ratel.search.query, gen_ai.tool.call.arguments / gen_ai.tool.call.result) ride span attributes only when the capture gate selects a span mode (SPAN_ONLY or SPAN_AND_EVENT); EVENT_ONLY does not put content on spans.
Set the gate in code:
- Greenfield path:
configure_telemetry(capture_content=...). - Dual-export path:
set_content_capturefromratel_ai_telemetry.
Code wins over the OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT env var, the fallback when nothing is set in code. The full mode table is on Telemetry.