Ratel Docs
Python SDK

Telemetry & OTel

Capture Ratel's search and invoke funnel as local trace events, or export it as OpenTelemetry traces and Logs, 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 signals exported as stock OTLP — funnel spans, plus gated content on Logs EventRecords. 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 no trace is passed).
  • kind="memory": keep events in memory; drain via catalog.drain_trace_events(), which returns the buffered events and removes them. Useful in tests.
  • kind="jsonl": append one JSON line per event to path (mode 0600 on 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). With content capture in an event mode, it also emits structured OpenTelemetry Logs EventRecords.

The base ratel-ai install carries one dependency: ratel-ai-telemetry>=0.1.3, the OTel-free ratel.* vocabulary and EventRecord contract. There is still no OpenTelemetry in the base install — without it, every telemetry helper is a straight pass-through: zero overhead, no spans. The [otlp] extra pulls ratel-ai-telemetry[otlp] (the OpenTelemetry SDK plus OTLP exporters); with OpenTelemetry present, telemetry goes to whatever providers are registered.

Greenfield: ship spans to Ratel Cloud

No OpenTelemetry in your stack yet? One call at startup registers matched global tracer and logger providers, with batch OTLP trace and Logs exporters pointed at Ratel Cloud. The exporters ship 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_OTLP_ENDPOINT", "https://cloud.ratel.sh/v1/traces"),
    api_key=os.environ["RATEL_API_KEY"],
)
# ... run your agent: every ratel.* / execute_tool span (and gated content
# EventRecord) now exports ...
handle.shutdown()  # flush both exporters on exit (handle.force_flush() mid-run)

The full keyword surface is configure_telemetry(*, api_key=None, endpoint=None, logs_endpoint=None, headers=None, service_name=None, capture_content=None, include_span_and_events=None, export_all_spans=False): capture_content / include_span_and_events set the content-capture gate in code. It returns a per-call shutdown handle (handle.shutdown() / handle.force_flush()) over the shared Ratel-owned providers; shutting any handle down stops export for every caller. Resolution rules:

  • An explicit endpoint= beats RATEL_OTLP_ENDPOINT, which beats the superseded RATEL_URL (still works, raises a DeprecationWarning; the fallback is slated for removal in the next minor — RATEL_URL also names the SDK's catalog source). Either way it is the full OTLP traces URL including /v1/traces; with none set it raises ValueError naming RATEL_OTLP_ENDPOINT.
  • logs_endpoint defaults to the sibling Logs URL: the traces URL with /v1/traces swapped for /v1/logs.
  • api_key is sent as Authorization: Bearer <key> on top of any headers. When omitted it falls back to the RATEL_API_KEY env var (unless you already set an Authorization header explicitly).
  • service_name defaults to "ratel".
  • export_all_spans=False (the default) exports only the gen_ai.* / ratel.* signal; pass True to forward every span.

configure_telemetry owns the global tracer and logger providers, and shutdown() flushes both. Calling it again once Ratel's providers are registered is idempotent: you get a handle to the same shared providers. It raises RuntimeError in two cases: a foreign TracerProvider or LoggerProvider is already registered (you are on the wrong path — dual-export instead, with both processors), 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 providers, the SDK's telemetry is already flowing to them. Skip configure_telemetry and add both processors from ratel-ai-telemetry to send a copy of the Ratel cut to Cloud: ratel_span_processor on the TracerProvider for spans, ratel_log_record_processor on the LoggerProvider for the content EventRecords. Without the log-record processor, captured content never reaches Cloud.

import os

from opentelemetry import _logs, trace
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk.trace import TracerProvider
from ratel_ai_telemetry.otlp import ratel_log_record_processor, ratel_span_processor

endpoint = os.environ.get("RATEL_OTLP_ENDPOINT", "https://cloud.ratel.sh/v1/traces")
api_key = os.environ["RATEL_API_KEY"]

tracer_provider = trace.get_tracer_provider()  # the provider your existing setup registered
if not isinstance(tracer_provider, TracerProvider):
    raise RuntimeError("register your OpenTelemetry SDK provider before adding processors")
tracer_provider.add_span_processor(ratel_span_processor(endpoint=endpoint, api_key=api_key))

logger_provider = _logs.get_logger_provider()  # likewise for the Logs half
if not isinstance(logger_provider, LoggerProvider):
    raise RuntimeError("register an OpenTelemetry LoggerProvider before adding processors")
logger_provider.add_log_record_processor(
    ratel_log_record_processor(endpoint=endpoint, api_key=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.

ratel_log_record_processor(*, api_key=None, endpoint=None, logs_endpoint=None, headers=None, log_filter=None, enabled=True) is the Logs twin: it batches to ratel_log_exporter(...), and logs_endpoint defaults to the sibling /v1/logs URL. The default ratel_event_filter forwards EventRecords named gen_ai.* / ratel.*.

Content capture

Message and tool content never leaves the process by default. When the gate opens, content rides two independent channels:

  • Span attributes (SPAN_ONLY / SPAN_AND_EVENT): ratel.search.query, gen_ai.tool.call.arguments / gen_ai.tool.call.result on the funnel spans.
  • EventRecords (EVENT_ONLY / SPAN_AND_EVENT): structured OpenTelemetry Logs records — ratel.tool.execution.details (tool arguments, plus the result on success) and ratel.search.results (the search query). This is the Logs data model, not span events, so it needs a Logs pipeline: the greenfield path wires one; dual-export needs ratel_log_record_processor.

gen_ai.output.messages stays reserved for model outputs with a finish_reason; tool results are never encoded there.

Set the gate in code:

  • Greenfield path: configure_telemetry(capture_content=...).
  • Dual-export path: set_content_capture from ratel_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.

Next steps

On this page