Skip to content
Night Owls.dev
Jump to a page

@nightowlsdev/telemetry-otel

Telemetry

OTLP/HTTP telemetry exporter for @nightowlsdev/core that ships swarm traces to any OpenTelemetry-compatible backend.

What it does

A thin adapter over @nightowlsdev/telemetry-core that maps a swarm run's run/generation/tool spans to OpenTelemetry and exports them over OTLP/HTTP via a BatchSpanProcessor to any compatible backend (Datadog, New Relic, Honeycomb, Grafana Tempo, Jaeger, or a plain OpenTelemetry Collector) just by changing url + headers. Generation spans carry gen_ai.* semantic-convention attributes (model, input/output tokens, cost). Call otelTelemetry({ url, headers, serviceName?, resourceAttributes? }) and hand it to defineSwarm({ telemetry }); the replayer awaits forceFlush() after each run's batch so spans aren't lost in short-lived invocations. Also exports nightOwlsPlugin, the declarative CLI plugin manifest for @nightowlsdev/cli. Telemetry is best-effort.

Install

pnpm add @nightowlsdev/telemetry-otel

Key exports

  • otelTelemetry
  • nightOwlsPlugin
  • OtelTelemetryOpts (type)

Usage

telemetry-otel.ts
import { defineSwarm } from "@nightowlsdev/core";
import { otelTelemetry } from "@nightowlsdev/telemetry-otel";

const telemetry = otelTelemetry({
  url: "https://otlp.example.com/v1/traces",
  headers: { authorization: `Bearer ${process.env.OTLP_TOKEN}` },
});

const swarm = defineSwarm({ agents, telemetry });

What it provides

An OTLP/HTTP telemetry exporter for swarm runs. Call otelTelemetry({ url, headers }) and hand it to defineSwarm({ telemetry }); a run's run/generation/tool spans ship over OTLP to any OpenTelemetry-compatible backend — Datadog, New Relic, Honeycomb, Grafana Tempo, Jaeger, or a plain OpenTelemetry Collector — by changing url + headers. Generation spans carry gen_ai.* semantic-convention attributes (model, input/output token usage, cost), so traces land with the LLM context your APM already understands. It is a thin shell over @nightowlsdev/telemetry-core, differing only in the BatchSpanProcessor + OTLPTraceExporter it hands in.

When to use it

  • You already run an APM / distributed-tracing stack and want swarm traces alongside the rest of your system's spans in one backend.
  • You want vendor-neutral, standards-based export (OTLP) rather than committing to a single LLM-observability SaaS.
  • You want swarm generations correlated with your HTTP / db / queue spans, using the gen_ai.* semantic conventions your backend recognizes.
  • You want env-driven setup — owl install telemetry-otel wires it from OTEL_EXPORTER_OTLP_ENDPOINT / OTEL_EXPORTER_OTLP_HEADERS and composes with any other telemetry adapter.

When not to

  • You want a purpose-built LLM view — prompt/response inspection, a generation-centric cost/eval product — with no APM to plug into. That's @nightowlsdev/telemetry-langfuse.
  • Your vendor only offers a gRPC OTLP endpoint — this factory builds the OTLP/HTTP exporter; you'd pass your own SpanExporter via the exporter option to use another transport.
  • You don't run any OTLP backend and don't want to stand a Collector up — Langfuse Cloud is a lower-lift start, or use core's customTelemetry to route spans elsewhere.

Alternatives

  • @nightowlsdev/telemetry-langfuseYou want LLM-native observability (generation view, prompt inspection, evals) rather than infra-style OTLP tracing.
  • otelTelemetry({ exporter })You need a non-HTTP OTLP transport (gRPC) or a test sink — pass your own SpanExporter to skip the OTLP/HTTP construction while keeping the SwarmSpan replay.
  • core's customTelemetry(fn)You don't want OpenTelemetry at all — receive the raw SwarmSpan[] and forward it however you like.

Strengths

  • Backend-agnostic: one factory, any OTLP/HTTP backend, selected purely by url + headers.
  • gen_ai.* semantic conventions so APMs recognize model, input/output tokens, and cost on generation spans.
  • BatchSpanProcessor for production throughput, with the replayer awaiting forceFlush() after each run's batch — safe in serverless / short-lived invocations.
  • env-driven via the CLI plugin (owl install telemetry-otel) and composes with any other telemetry adapter.
  • Best-effort: a throwing or hung exporter never breaks the run (the engine exports in a finally and swallows errors).
  • @mastra-free (the engine wall) — the built .d.ts has zero @mastra references.

Limits & trade-offs

  • OTLP/HTTP only out of the box — the factory constructs @opentelemetry/exporter-trace-otlp-http; a gRPC endpoint means supplying your own SpanExporter.
  • No LLM-native UI — you get spans, not a prompt/eval product; the generation view is only as rich as your backend's gen_ai.* support.
  • Backend-dependent auth is on you — you parse OTEL_EXPORTER_OTLP_HEADERS and pass the vendor's auth headers yourself.
  • Node-only, inherited from telemetry-core's node:crypto trace-id derivation.
  • Spans are replayed at the end of the run's batch, not streamed live — you see the trace when the run exports.

How it works

otelTelemetry builds an OTLPTraceExporter({ url, headers }) wrapped in a BatchSpanProcessor and hands it to telemetry-core's replayerExporter, threading serviceName and resourceAttributes through. When core's engine finishes a run it calls the exporter with the batch; the replayer converts the SwarmSpans to real OTel spans (run + generations + tools) with gen_ai.* attributes, then awaits forceFlush() so the BatchSpanProcessor ships them to your endpoint. For an alternate transport or a test, pass your own SpanExporter as opts.exporter to bypass the OTLP/HTTP construction while keeping the same replay.

Examples

Wire an OTLP backend into a swarm

Point url at your collector or vendor's OTLP/HTTP traces endpoint and pass auth via headers.

telemetry-otel-example-1.ts
import { defineSwarm } from "@nightowlsdev/core";
import { otelTelemetry } from "@nightowlsdev/telemetry-otel";

const telemetry = otelTelemetry({
  url: "https://otlp.example.com/v1/traces",
  headers: { "x-api-key": process.env.OTLP_KEY! },
});

const swarm = defineSwarm({ agents, telemetry });

Honeycomb, with a service name + resource attributes

resourceAttributes tag every span (e.g. deployment.environment); the vendor's team key rides in headers.

telemetry-otel-example-2.ts
import { otelTelemetry } from "@nightowlsdev/telemetry-otel";

const telemetry = otelTelemetry({
  url: "https://api.honeycomb.io/v1/traces",
  headers: { "x-honeycomb-team": process.env.HONEYCOMB_KEY! },
  serviceName: "nightowls",
  resourceAttributes: { "deployment.environment": "prod" },
});

Advanced: supply your own SpanExporter

The exporter override skips OTLP/HTTP construction — useful for a gRPC transport or an in-memory test sink.

telemetry-otel-example-3.ts
import { otelTelemetry } from "@nightowlsdev/telemetry-otel";
import { InMemorySpanExporter } from "@opentelemetry/sdk-trace-base";

const sink = new InMemorySpanExporter();
const telemetry = otelTelemetry({ exporter: sink, serviceName: "nightowls" });
// after a run, sink.getFinishedSpans() holds the replayed run/generation/tool spans.

Related

  • telemetry-langfuseThe other exporter — pick it when you want an LLM-native generation view instead of (or alongside) OTLP.
  • telemetry-coreThe shared replay engine this wraps — and where deriveTraceId, the gen_ai.* mapping, and the raw-call helpers live.
  • coreProvides defineSwarm({ telemetry }), compositeTelemetry, and the SwarmSpan batch this exports.