Ratel Docs
TypeScript SDK

Telemetry & OTel

Wire local trace sinks onto your catalogs and export the same funnel 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 TypeScript 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.

import { ToolCatalog } from "@ratel-ai/sdk";

const catalog = new ToolCatalog({
  trace: { kind: "jsonl", sessionId: "session-1", path: "/tmp/ratel.jsonl" },
});
// Every catalog.invoke, capability-tool call, and registerMcpServer call now appends one JSON line per event to /tmp/ratel.jsonl.

TraceSinkConfig is a discriminated union: { kind: "noop" }, { kind: "memory"; sessionId }, or { kind: "jsonl"; sessionId; path }. Misconfiguration throws (for example "jsonl sink requires path").

  • { kind: "noop" } — drop everything (the default when no trace is passed).
  • { kind: "memory"; sessionId } — keep events in memory; drain via catalog.drainTraceEvents(). Useful in tests.
  • { kind: "jsonl"; sessionId; path } — append one JSON line per event to path (mode 0600 on Unix). Best-effort, lossy on backpressure.

Both catalogs also expose the sink directly. recordEvent(event) injects an event into the active sink, validating it against the core trace schema: an unrecognized shape throws invalid trace event: .... drainTraceEvents() removes and returns the captured envelopes, [] unless the active sink is memory.

searchCapabilitiesTool 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"). The full event set is in Telemetry.

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 SDK's only OpenTelemetry dependency is @opentelemetry/api, never the OTel SDK. Until a provider is registered, every span is a no-op NonRecordingSpan. The ratel.* semantic-convention vocabulary itself ships as the OTel-free package @ratel-ai/telemetry.

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. The exporter is the optional peer @ratel-ai/telemetry-otlp (without it, configureTelemetry throws with this install hint):

npm install @ratel-ai/telemetry-otlp
import { configureTelemetry } from "@ratel-ai/sdk";

const handle = await configureTelemetry({
  endpoint: process.env.RATEL_URL ?? "https://cloud.ratel.sh/v1/traces",
  apiKey: process.env.RATEL_API_KEY,
});
// ... run your agent: every ratel.* / execute_tool span now exports ...
await handle.shutdown(); // flushes and stops the exporter

configureTelemetry(options?: ConfigureTelemetryOptions): Promise<TelemetryHandle>ConfigureTelemetryOptions extends InitOptions with captureContent / includeSpanAndEvents, in-code overrides of the content-capture gate. All optional:

  • endpoint — the full OTLP traces URL including /v1/traces; falls back to the RATEL_URL env var. With neither set, the call fails before anything registers.
  • apiKey — sent as Authorization: Bearer <apiKey>; when omitted it falls back to the RATEL_API_KEY env var (unless you already set an Authorization header explicitly).
  • headers — merged in before the Bearer header.
  • serviceName — defaults to "ratel".
  • captureContent / includeSpanAndEvents — set the content-capture mode in code, winning over the env var.

configureTelemetry owns the global provider. If one is already registered it fails loudly, pointing at the span processor, rather than silently no-op'ing. That error means you are on the wrong path: dual-export instead.

Already on OpenTelemetry: dual-export

If Langfuse, the Vercel AI SDK, or your own collector already owns the provider, the SDK's spans are already flowing to it. Skip configureTelemetry and add the Ratel span processor to send a copy to Ratel Cloud:

import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { LangfuseSpanProcessor } from "@langfuse/otel";
import { ratelSpanProcessor } from "@ratel-ai/telemetry-otlp";

const provider = new NodeTracerProvider({
  spanProcessors: [
    new LangfuseSpanProcessor(),                                // Langfuse keeps every span
    ratelSpanProcessor({                                        // Ratel takes gen_ai.*/ratel.* only
      endpoint: process.env.RATEL_URL ?? "https://cloud.ratel.sh/v1/traces",
      apiKey: process.env.RATEL_API_KEY,
    }),
  ],
});
provider.register();

ratelSpanProcessor(opts) accepts the endpoint/auth options (endpoint, apiKey, headers) plus spanFilter and enabled; ratelTraceExporter(opts) 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 spanFilter: () => 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 via configureTelemetry({ captureContent }); it 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