Skip to content

Telemetry pipeline (OTel bridge)

runtime.onTelemetry(listener) is the single substrate for everything observability. One tagged-union stream covers 22 kinds — every dispatch, every state transition, every workflow firing, every bus publish.

The stream

ts
runtime.onTelemetry((record) => {
  switch (record.kind) {
    case "action.dispatched":   /* ... */ break
    case "action.completed":    /* ... */ break
    case "action.failed":       /* ... */ break
    case "event.published":     /* ... */ break
    case "actor.loaded":        /* ... */ break
    case "actor.transitioned":  /* ... */ break
    case "actor.saved":         /* ... */ break
    case "projection.applied":  /* ... */ break
    case "workflow.entered":    /* ... */ break
    case "workflow.exited":     /* ... */ break
    case "workflow.timer.set":  /* ... */ break
    case "workflow.timer.fired":/* ... */ break
    case "cron.fired":          /* ... */ break
    case "external.call":       /* ... */ break
    case "external.response":   /* ... */ break
    case "bus.publish":         /* ... */ break
    case "bus.receive":         /* ... */ break
    case "outbox.write":        /* ... */ break
    case "outbox.dispatch":     /* ... */ break
    case "inbox.dedupe":        /* ... */ break
    case "inbox.apply":         /* ... */ break
    case "saga.completed":      /* ... */ break
  }
})

Every record carries correlationId and causationId so you can rebuild the causal tree from any starting point.

OTel bridge

@nwire/telemetry-otel subscribes to the stream and emits OTLP spans + events.

ts
import { attachOtelExporter } from "@nwire/telemetry-otel"
import { trace } from "@opentelemetry/api"

attachOtelExporter(runtime, { tracer: trace.getTracer("my-app") })

The bridge is duck-typed against the OTel Tracer interface — no hard dependency on @opentelemetry/api, so you can supply any tracer (Honeycomb, Datadog, Sentry, GreptimeDB via Vector).

Why a tagged union and not raw OTel spans

  • Studio reads the same stream. No double instrumentation.
  • Lifecycle moments map to record kinds, not span boundaries. Workflow timers fire outside the action pipeline — they need their own kind, not a span.
  • Causation tree is first-class. correlationId + causationId are baked in; OTel parent-span linking is a downstream concern.

Studio's role

Studio reads /_nwire/telemetry/recent and /_nwire/telemetry/stream (SSE). The same records that go to OTel power the Live page and the EventStorm Play Trace.

See also

MIT licensed.