Docs/telemetryv1.0.0-rc.1

Telemetry — Tracing

Overview

The tracing module instruments code with distributed spans compatible with OpenTelemetry. Every span captures its name, timing, status, attributes, and parent relationship. Completed spans are forwarded to all registered TraceExporter instances asynchronously.

Context propagation is automatic: any startSpan call inside the executor of an outer startSpan will inherit the outer span's traceId and record itself as a child span via parentSpanId.

Key Concepts

  • Span — a named unit of work with a start time, end time, status, and optional attributes and error.
  • Trace — a tree of spans sharing a common traceId, representing an end-to-end operation.
  • Context inheritance — startSpan reads the active TelemetryContext to find the current traceId and spanId, sets those on the new span, then updates the context for all code running inside the executor.
  • Automatic finalization — the span's status and endTime are set when the executor returns (or rejects), whether it is synchronous or returns a promise.

API Reference

startSpan<T>(name, options, executor): MaybePromise<T>

The primary tracing API. Wraps a block of synchronous or asynchronous code in a span.

Parameter Type Description
name string Human-readable span name
options SpanOptions | undefined Span configuration (see below)
executor (span: TracingSpan) => T | Promise<T> The instrumented code block

Returns the same type (sync or async) as the executor.

import { startSpan } from '@tagna/udiot/telemetry';

// Synchronous
const value = startSpan('compute', undefined, (span) => {
  return heavyComputation();
});

// Asynchronous
const result = await startSpan('fetchUser', { kind: 'client' }, async (span) => {
  return db.users.findById(span.traceId);
});

SpanOptions

Field Type Default Description
kind SpanKind 'internal' Semantic role of this span
attributes TelemetryAttributes undefined Key/value metadata
traceId string inherited or new Override the trace ID
spanId string auto-generated Override the span ID
parentSpanId string inherited Explicit parent span ID
context Partial<TelemetryContext> undefined Additional context fields to merge

SpanKind

type SpanKind = 'internal' | 'server' | 'client' | 'producer' | 'consumer';
Value When to use
'internal' Default. Internal computation with no network boundary.
'server' Receiving an inbound request (HTTP server, RPC handler).
'client' Making an outbound request (HTTP client, database query).
'producer' Publishing a message to a queue or event stream.
'consumer' Processing a message from a queue or event stream.

SpanStatus

type SpanStatus = 'in_progress' | 'ok' | 'error';

'in_progress' is the initial state. The framework sets 'ok' when the executor returns normally and 'error' when it throws or the returned promise rejects.

TracingSpan

The object passed to the executor and forwarded to exporters after finalization:

Field Type Description
id string 8-byte hex span ID
traceId string 16-byte hex trace ID
parentSpanId string | undefined Parent span ID (if any)
name string Span name
kind SpanKind Semantic kind
attributes TelemetryAttributes | undefined Attached metadata
status SpanStatus Finalized status
startTime number Unix timestamp in ms
endTime number | undefined Unix timestamp in ms (set on finalization)
error Error | undefined Error object if status is 'error'

Exporter management

registerTraceExporter(exporter: TraceExporter): void

Adds a trace exporter. Multiple exporters are supported.

unregisterTraceExporter(exporter: TraceExporter): void

Removes an exporter.

resetTraceExporters(): void

Clears all registered exporters. Useful in tests.

TraceExporter interface

interface TraceExporter {
  export(span: TracingSpan): Promise<void> | void;
}

Implement this interface to send spans to any backend. Errors thrown from export() are caught and logged as warnings — they do not propagate to the application.


Basic Usage

Nested spans for an API handler → database call

import { startSpan, getTelemetryContext } from '@tagna/udiot/telemetry';

async function handleCreateOrder(userId: string, items: string[]) {
  return startSpan('createOrder', { kind: 'server', attributes: { userId } }, async (parentSpan) => {
    // Inside here, traceId and spanId are set in the ambient context.
    // Any nested startSpan will automatically become a child of this one.

    const order = await startSpan('db.insertOrder', { kind: 'client' }, async () => {
      // parentSpanId is automatically set to parentSpan.id
      return db.orders.insert({ userId, items });
    });

    await startSpan('notify.orderCreated', { kind: 'producer' }, async () => {
      await eventBus.publish('order.created', { orderId: order.id });
    });

    return order;
  });
}

The resulting trace tree:

createOrder            (traceId: abc123, spanId: 0001)
  └── db.insertOrder   (traceId: abc123, spanId: 0002, parentSpanId: 0001)
  └── notify.orderCreated (traceId: abc123, spanId: 0003, parentSpanId: 0001)

Context propagation via HTTP headers

To continue a trace from an upstream service, pass the incoming traceId and spanId via SpanOptions:

import { startSpan } from '@tagna/udiot/telemetry';

// In your HTTP middleware
function traceMiddleware(req: Request, res: Response, next: NextFunction) {
  const incomingTraceId = req.headers['x-trace-id'] as string | undefined;
  const incomingSpanId = req.headers['x-span-id'] as string | undefined;

  startSpan(
    `${req.method} ${req.path}`,
    {
      kind: 'server',
      traceId: incomingTraceId,         // continue the upstream trace
      parentSpanId: incomingSpanId,     // upstream span becomes this span's parent
    },
    async () => {
      next();
    }
  );
}

// When making outbound HTTP calls, propagate the current context:
import { getTelemetryContext } from '@tagna/udiot/telemetry';

async function callDownstream(url: string) {
  const ctx = getTelemetryContext();
  return fetch(url, {
    headers: {
      'x-trace-id': ctx?.traceId ?? '',
      'x-span-id': ctx?.spanId ?? '',
    },
  });
}

Custom trace exporter

import { TraceExporter, TracingSpan, registerTraceExporter } from '@tagna/udiot/telemetry';

class MyExporter implements TraceExporter {
  async export(span: TracingSpan): Promise<void> {
    await sendToMyBackend({
      traceId: span.traceId,
      spanId: span.id,
      parentId: span.parentSpanId,
      name: span.name,
      durationMs: (span.endTime ?? 0) - span.startTime,
      status: span.status,
      error: span.error?.message,
    });
  }
}

registerTraceExporter(new MyExporter());

Common Patterns

Reading the active span inside an executor — the executor receives the TracingSpan object as its first argument. You can attach additional attributes by mutating span.attributes directly.

Fire-and-forget tasks — if you launch background work inside a span's executor without await, the parent span may close before the child. Either await all child work or start a separate root span for the background task using an explicit traceId.

Testing — collect exported spans with a custom TraceExporter implementation that pushes to an array, then assert against the array after the operation.

Pitfalls

  • Context propagation is module-level and not AsyncLocalStorage-based. In concurrent Node.js request handlers running in the same event loop tick without await separation, contexts can collide. Use withTelemetryContext() explicitly to isolate per-request context.
  • Exporter failures are silently swallowed (logged as warnings). If export reliability is critical, implement retry logic inside your TraceExporter.export() method.
  • The endTime field on TracingSpan is only set after the executor finishes. Do not read it inside the executor — it will be undefined.
From the canonical UDIOT public documentation.Try it in the playground ↗