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+causationIdare 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
- Studio internals
- Observability recipe — wiring it up for a real backend