Observability & Tracing
@moon-wave/observability provides a lightweight distributed tracing system. Wrap your agent and tool calls in spans to capture timing, attributes, and errors — then export traces to the console, Cloudflare D1, or your own backend.
Installation
pnpm add @moon-wave/observabilityQuick start
import { Tracer, ConsoleExporter } from '@moon-wave/observability';
const tracer = new Tracer();tracer.addExporter(new ConsoleExporter());
const result = await tracer.trace('agent.run', async (spanId) => { tracer.setAttribute(spanId, 'agent', 'support'); tracer.setAttribute(spanId, 'sessionId', ctx.sessionId); return myAgent.run(input, ctx);});// ConsoleExporter prints span details to stdoutCore concepts
Tracer
A Tracer owns a traceId (UUID) and manages all spans within that trace.
const tracer = new Tracer(); // auto-generated traceIdconst tracer = new Tracer('my-trace-id'); // custom traceIdSpans
Spans represent a unit of work. Each span has a name, start/end times, status, attributes, and events.
// Manual span lifecycleconst spanId = tracer.startSpan('fetch.users', { userId: '123' });try { const users = await fetchUsers(); tracer.endSpan(spanId, 'ok'); return users;} catch (err) { tracer.endSpan(spanId, 'error', err.message); throw err;}
// Convenience wrapper (recommended)const users = await tracer.trace('fetch.users', async (spanId) => { tracer.setAttribute(spanId, 'userId', '123'); return fetchUsers();});Attributes & events
// Attach key-value metadata to a spantracer.setAttribute(spanId, 'model', 'llama-3.3-70b');tracer.setAttribute(spanId, 'tokens', 350);
// Record timestamped events within a spantracer.addEvent(spanId, 'tool.called', { tool: 'search_web' });tracer.addEvent(spanId, 'tool.completed', { results: 5 });Nested spans
Pass parentSpanId to link spans into a tree:
const rootId = tracer.startSpan('agent.run');const llmId = tracer.startSpan('llm.call', { model: 'llama-3.3-70b' }, rootId);// ...tracer.endSpan(llmId);tracer.endSpan(rootId);Or with tracer.trace:
await tracer.trace('agent.run', async (rootId) => { await tracer.trace('llm.call', async (llmId) => { // parentSpanId is rootId return callLLM(messages); }, { model: 'llama-3.3-70b' }, rootId);});Exporters
ConsoleExporter
Logs each completed span to console.log. Useful during development.
import { ConsoleExporter } from '@moon-wave/observability';tracer.addExporter(new ConsoleExporter());D1Exporter
Persists spans to a Cloudflare D1 database. Run the migration once to create the table:
import { D1Exporter, TRACES_MIGRATION } from '@moon-wave/observability';
// In your worker setup / migration script:await env.DB.exec(TRACES_MIGRATION);
// Attach the exportertracer.addExporter(new D1Exporter(env.DB));The schema created by TRACES_MIGRATION:
CREATE TABLE IF NOT EXISTS traces ( span_id TEXT PRIMARY KEY, trace_id TEXT NOT NULL, parent_span_id TEXT, name TEXT NOT NULL, start_time INTEGER NOT NULL, end_time INTEGER, duration_ms INTEGER, status TEXT NOT NULL, attributes TEXT, events TEXT, error TEXT);CREATE INDEX IF NOT EXISTS idx_trace_id ON traces(trace_id);Custom exporter
Implement SpanExporter to ship spans anywhere:
import type { SpanExporter, Span } from '@moon-wave/observability';
class DatadogExporter implements SpanExporter { async export(spans: Span[]): Promise<void> { await fetch('https://http-intake.logs.datadoghq.com/api/v2/spans', { method: 'POST', headers: { 'DD-API-KEY': this.apiKey }, body: JSON.stringify(spans), }); }}
tracer.addExporter(new DatadogExporter(env.DD_API_KEY));Multiple exporters can be added — all receive every completed span:
tracer .addExporter(new ConsoleExporter()) .addExporter(new D1Exporter(env.DB));Retrieving a trace
const trace = tracer.getTrace();// { traceId, spans: Span[], startTime: number }Full example: tracing an agent request
import { Tracer, D1Exporter } from '@moon-wave/observability';
export default { async fetch(req: Request, env: Env) { const tracer = new Tracer(); tracer.addExporter(new D1Exporter(env.DB));
const { output } = await tracer.trace('request', async (rootId) => { tracer.setAttribute(rootId, 'url', req.url); tracer.setAttribute(rootId, 'method', req.method);
return tracer.trace('agent.run', async (agentId) => { tracer.setAttribute(agentId, 'agent', 'support'); const body = await req.json() as { input: string; sessionId: string }; return myAgent.run(body.input, { sessionId: body.sessionId, env }); }, {}, rootId); });
return Response.json({ output }); },};Type reference
interface Span { traceId: string; spanId: string; parentSpanId?: string; name: string; startTime: number; // epoch ms endTime?: number; durationMs?: number; status: 'ok' | 'error' | 'running'; attributes: Record<string, unknown>; events: Array<{ name: string; timestamp: number; attributes?: Record<string, unknown> }>; error?: string;}