Ratel Docs
TypeScript SDK

Telemetry & OTel

Wire a local trace sink through ratel({ trace }) 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 ratel() and the core records every search, invoke, capability-tool, upstream-MCP, and auth event into the sink. The one config is forwarded to both internal catalogs, so tool and skill events land in the same sink.

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

const r = ratel({
  trace: { kind: "jsonl", sessionId: "session-1", path: "/tmp/ratel.jsonl" },
});
// Every r.tools.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 r.tools.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.

Recording and draining live on the raw catalogs — r.tools.catalog (the ToolCatalog under the handle) and r.skills (the raw SkillCatalog). 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.

search_capabilities records its own gateway_search event and tags the underlying catalog search's search event with origin: "agent"; direct callers (r.tools.search(query, k)) default to "direct". Override per call on the raw catalog: r.tools.catalog.search(query, k, "agent"). The full event set is in Telemetry.

Assembling the funnel piecemeal? new ToolCatalog({ trace: ... }) and new SkillCatalog({ trace: ... }) take the same config directly, and the searchCapabilitiesTool factory emits the same gateway_search event.

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). Under an event content-capture mode, gated payloads also go out as OpenTelemetry Logs EventRecords.

The SDK's OpenTelemetry dependencies are API-only — @opentelemetry/api and @opentelemetry/api-logs — never the OTel SDK. It registers no provider of its own: until you register one, every span and EventRecord is a no-op. The ratel.* semantic-convention vocabulary itself ships as the OTel-free package @ratel-ai/telemetry.

Greenfield: ship telemetry to Ratel Cloud

No OpenTelemetry in your stack yet? The SDK ships no bootstrap: you build the providers, register them globally, and own flush and shutdown. Install the OTel SDK packages:

npm install @opentelemetry/sdk-trace-node @opentelemetry/sdk-trace-base @opentelemetry/sdk-logs \
  @opentelemetry/exporter-trace-otlp-proto @opentelemetry/exporter-logs-otlp-proto \
  @opentelemetry/resources @opentelemetry/api-logs

Wire a NodeTracerProvider for the spans and a LoggerProvider for the content EventRecords, each with an OTLP batch exporter. The Logs URL is the traces URL's sibling: replace the terminal /v1/traces with /v1/logs.

// otel.ts — import this before any module that imports @ratel-ai/sdk
import { logs } from "@opentelemetry/api-logs";
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs";
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";

const endpoint = process.env.RATEL_OTLP_ENDPOINT ?? "https://cloud.ratel.sh/v1/traces";
const logsUrl = endpoint.replace(/\/v1\/traces$/, "/v1/logs");
const apiKey = process.env.RATEL_API_KEY;
const headers = apiKey ? { Authorization: `Bearer ${apiKey}` } : {};
const resource = resourceFromAttributes({ "service.name": "my-agent" });

export const tracerProvider = new NodeTracerProvider({
  resource,
  spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter({ url: endpoint, headers }))],
});
export const loggerProvider = new LoggerProvider({
  resource,
  processors: [new BatchLogRecordProcessor({ exporter: new OTLPLogExporter({ url: logsUrl, headers }) })],
});

tracerProvider.register(); // every ratel.* / execute_tool span now exports
logs.setGlobalLoggerProvider(loggerProvider);
  • Register both providers before importing the SDK — keep the wiring in its own module and import it first.
  • RATEL_OTLP_ENDPOINT and RATEL_API_KEY are plain host configuration; no Ratel package reads them. Swap in your own config source — the API key rides as Authorization: Bearer.
  • You own shutdown: await tracerProvider.shutdown() and await loggerProvider.shutdown() on exit flush both batch processors.

examples/telemetry-ts is the runnable version of this wiring.

Already running a provider — Langfuse, the Vercel AI SDK, your own collector? Don't register a second one: dual-export from the provider you have.

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. To send Ratel Cloud a copy, add a filtered span processor of your own — there is no packaged one:

import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
import { BatchSpanProcessor, type ReadableSpan } from "@opentelemetry/sdk-trace-base";
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
import { LangfuseSpanProcessor } from "@langfuse/otel";

const isRatelSpan = (span: ReadableSpan) =>
  span.name.startsWith("ratel.") ||
  Object.keys(span.attributes).some((k) => k.startsWith("gen_ai.") || k.startsWith("ratel."));

class RatelSpanProcessor extends BatchSpanProcessor {
  onEnd(span: ReadableSpan) {
    if (isRatelSpan(span)) super.onEnd(span);
  }
}

const provider = new NodeTracerProvider({
  spanProcessors: [
    new LangfuseSpanProcessor(), // your existing destination
    new RatelSpanProcessor(new OTLPTraceExporter({ url: endpoint, headers })), // same endpoint/headers resolution as the greenfield snippet
  ],
});
provider.register();

The filter forwards a span if its name starts with ratel. or any attribute key starts with gen_ai. or ratel.; drop the onEnd override to forward everything.

  • Content EventRecords need the Logs half too: add a BatchLogRecordProcessor + OTLPLogExporter at the sibling /v1/logs URL to the LoggerProvider you register, or captured content never reaches Ratel Cloud.
  • Check your existing destination's own filter: a stock LangfuseSpanProcessor drops the ratel.*-named spans (they carry no gen_ai.* attribute). Widen it with shouldExportSpan, keyed on scope name @ratel-ai/sdk.

Content capture

Message and tool content never leaves the process by default (NO_CONTENT). Opting in opens up to two channels:

  • Span modes (SPAN_ONLY, SPAN_AND_EVENT): the gated payloads (ratel.search.query, gen_ai.tool.call.arguments / gen_ai.tool.call.result) ride span attributes.
  • Event modes (EVENT_ONLY, SPAN_AND_EVENT): content rides structured OpenTelemetry Logs EventRecords — ratel.tool.execution.details and ratel.search.results — through your registered LoggerProvider. EVENT_ONLY puts nothing on span attributes.

Set the gate in code with setContentCapture(mode) (ContentCapture, setContentCapture, and clearContentCapture are re-exported from @ratel-ai/sdk); 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.

import { ContentCapture, setContentCapture } from "@ratel-ai/sdk";

setContentCapture(ContentCapture.SpanAndEvent);

Next steps

On this page