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 notraceis passed).{ kind: "memory"; sessionId }: keep events in memory; drain viar.tools.catalog.drainTraceEvents(). Useful in tests.{ kind: "jsonl"; sessionId; path }: append one JSON line per event topath(mode0600on 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-logsWire 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_ENDPOINTandRATEL_API_KEYare plain host configuration; no Ratel package reads them. Swap in your own config source — the API key rides asAuthorization: Bearer.- You own shutdown:
await tracerProvider.shutdown()andawait 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 aBatchLogRecordProcessor+OTLPLogExporterat the sibling/v1/logsURL to theLoggerProvideryou register, or captured content never reaches Ratel Cloud. - Check your existing destination's own filter: a stock
LangfuseSpanProcessordrops theratel.*-named spans (they carry nogen_ai.*attribute). Widen it withshouldExportSpan, 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 LogsEventRecords —ratel.tool.execution.detailsandratel.search.results— through your registeredLoggerProvider.EVENT_ONLYputs 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
Framework integrations
Official adapters for the Vercel AI SDK and Mastra: ratel().adaptTo(...) speaks each framework's native tool and message shapes, with per-turn recall built in.
Packages
The five TypeScript packages: SDK, Vercel AI SDK adapter, Mastra adapter, telemetry vocabulary, and the package behind Ratel Local.