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:
| Callback | When 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:
| Attribute | Source |
|---|---|
nerva.request_id | ctx.requestId |
nerva.trace_id | ctx.traceId |
nerva.span_id | span.spanId |
nerva.parent_id | span.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:
- Snapshots the token count when each span starts
- Computes the delta when the span ends
- Looks up the model from the span’s
modelattribute - Emits a
cost.calculatedevent withdelta_tokensandcost_usd - Emits a
cost.totalevent 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
npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/exporter-jaegerConfigure 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
npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/exporter-trace-otlp-httpimport { 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');}