Docs menu
Observability integrations (OTLP presets)
Named OTLP presets (Langfuse/LangSmith/Braintrust/Honeycomb/Datadog/Collector) export the journal to an observability provider in one line; the journal is the single source of truth, OTLP is just a receipt.
Philosophy — journal is the single source of truth, OTLP is a receipt#
In GNL, the source of truth is the journal. @gnldev/otel converts a run into a real OpenTelemetry trace post-hoc from the journal: deterministic trace/span ids → idempotent (exporting the same run twice yields the same trace), complete even after a crash, and consistent across replays. This is exactly-once / crash-proof observability — frameworks that instrument live can't give you that. OTLP export is only a "receipt": it sends a copy to a provider, but the correctness of the system doesn't depend on it.
otlpPresets — one-line integration#
Each preset converts a provider's documented OTLP/HTTP endpoint + auth headers into a ready-made ExportRunToOtlpOptions. Available presets: langfuse (eu/us cloud or self-hosted, Basic auth), apiKeyOtlp (x-api-key + project, any endpoint), braintrust (Bearer + x-bt-parent), honeycomb (x-honeycomb-team), datadogAgent (local Agent OTLP receiver), and a generic collector (Jaeger/Tempo/OTel Collector). Every preset has an endpoint override.
import { otlpPresets, exportRunToOtlp } from '@gnldev/otel';
// one line: preset → a ready export configuration (pure, no network call)
const cfg = otlpPresets.langfuse({
publicKey: process.env.LF_PUBLIC!,
secretKey: process.env.LF_SECRET!,
region: 'eu',
});
// "plug" a run from the journal into the provider
const res = await exportRunToOtlp(reader, 'r1', cfg);
// res => { traceId, spans, ok, status }otlpPresets.apiKeyOtlp({ endpoint: 'https://otel.example.com/v1/traces', apiKey, project: 'gnl-prod' });
otlpPresets.braintrust({ apiKey, project: 'gnl' });
otlpPresets.honeycomb({ apiKey, dataset: 'gnl' });
otlpPresets.datadogAgent({ host: 'localhost', port: 4318 });
otlpPresets.collector({ baseUrl: 'http://tempo:4318' });Live mode — @gnldev/otel/live#
In addition to post-hoc export, @gnldev/otel/live offers real-time monitoring: it emits live OTEL spans + cost as model/tool calls actually run. The wrappers compose underneath durable → they never run during replay (journal cache hit) → the live layer never touches the journal and doesn't break determinism. Since it carries real wall-clock latency (non-deterministic), it only goes to the exporter, not to the journal.
import { liveObservability } from '@gnldev/otel/live';
const live = liveObservability({
endpoint: 'http://localhost:4318/v1/traces',
onCost: (c) => { if (c.costUsd > 1) alertBudget(c); }, // live budget alarm
});
await runDurable(live.instrument({ runId: 'r1', model, tools, prompt }));
await live.flush();
const total = live.cost(); // { inputTokens, ..., costUsd }API reference#
otlpPresets{ langfuse, apiKeyOtlp, braintrust, honeycomb, datadogAgent, collector } — each a pure function producing ExportRunToOtlpOptions.
exportRunToOtlp(reader, runId, opts) → { traceId, spans, ok, status }; POSTs the run from the journal to an OTLP/HTTP endpoint.
exportRun(reader, runId, opts?) → exports a run to an OTEL SpanExporter; deterministic ids → idempotent.
liveObservability(@gnldev/otel/live) live spans + cost; instrument()/cost()/flush(); composes underneath durable → never touches the journal.
ExportRunToOtlpOptions{ endpoint, headers?, serviceName?, resourceAttributes?, now? } — the type produced by the presets.
endpoint, serviceName, and resourceAttributes overrides.