Skip to content
nerva docs v0.2.1

OpenTelemetry

Nerva includes built-in OpenTelemetry integration for exporting traces, spans, and cost metrics to any OTel-compatible backend (Jaeger, Datadog, Grafana Tempo, etc.).

Tracer interface

All observability in Nerva flows through the Tracer interface. Tracers receive four callbacks:

CallbackWhen it fires
onSpanStart(span, ctx)A new span begins (agent invocation, tool call, router decision)
onSpanEnd(span, ctx)A span completes with timing data
onEvent(event, ctx)A discrete structured event is recorded
onComplete(ctx)The entire request finishes — use for flushing buffers

Multiple tracers run simultaneously. A typical setup combines JSON logging for local debugging with OTel export for production monitoring.

Built-in tracers

OTelTracer

Maps Nerva spans to OpenTelemetry spans. Requires @opentelemetry/api as an optional peer dependency — falls back to no-ops if not installed.

import { OTelTracer } from 'nerva';
const tracer = new OTelTracer('my-service', {
'deployment.environment': 'production',
'service.version': '1.2.0',
});

Each Nerva span becomes an OTel span with these attributes:

AttributeSource
nerva.request_idctx.requestId
nerva.trace_idctx.traceId
nerva.span_idspan.spanId
nerva.parent_idspan.parentId (if not root)

Nerva events are added as OTel span events on the most recently active span. On request completion, any remaining open spans are flushed.

JsonLogTracer

Writes one JSON line per trace callback to a file or stream. Useful for local debugging, log aggregation, and audit trails.

import { JsonLogTracer } from 'nerva';
// Write to file (append mode)
const tracer = new JsonLogTracer('./traces/app.jsonl');
// Write to stderr (default)
const tracer = new JsonLogTracer();

Each line contains:

{
"type": "span_end",
"timestamp": 1711234567.123456,
"request_id": "req_abc123",
"trace_id": "trace_xyz",
"span_id": "span_001",
"span_name": "invoke_tool",
"duration_s": 0.042
}

The completion record includes a summary with span count, event count, token usage breakdown, and elapsed time.

CostTracker

Wraps any Tracer and enriches spans with USD cost computed from token usage and per-model pricing.

import { CostTracker, OTelTracer } from 'nerva';
const pricing = {
'gpt-4o': 0.005,
'claude-sonnet-4-20250514': 0.003,
'llama-3.1-8b': 0.0002,
};
const tracer = new CostTracker(new OTelTracer(), pricing);

The cost tracker:

  1. Snapshots the token count when each span starts
  2. Computes the delta when the span ends
  3. Looks up the model from the span’s model attribute
  4. Emits a cost.calculated event with delta_tokens and cost_usd
  5. Emits a cost.total event on request completion summing all span costs

Pricing is specified as USD per 1,000 tokens. Unknown models default to 0.0 cost.

Exporting to backends

Jaeger

Terminal window
npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/exporter-jaeger

Configure the OTel SDK before creating your Nerva runtime:

import { NodeSDK } from '@opentelemetry/sdk-node';
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
const sdk = new NodeSDK({
traceExporter: new JaegerExporter({ endpoint: 'http://localhost:14268/api/traces' }),
serviceName: 'my-nerva-agent',
});
sdk.start();

Datadog

Terminal window
npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-http
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: 'http://localhost:4318/v1/traces',
}),
serviceName: 'my-nerva-agent',
});
sdk.start();

Grafana Tempo

Use the same OTLP exporter — Tempo accepts OTLP traces on port 4317 (gRPC) or 4318 (HTTP).

Graceful fallback

The OTelTracer does not hard-fail when @opentelemetry/api is missing. It attempts a dynamic import at construction time and silently falls back to no-op callbacks. Use isOTelAvailable() to check:

import { isOTelAvailable } from 'nerva';
if (isOTelAvailable()) {
console.log('OTel export active');
} else {
console.log('OTel not installed — traces are local only');
}