Observability
Most stacks make you assemble observability: pick a logger, thread a request id, wire spans, hope the pieces line up. Nwire's runtime emits one typed stream of everything domain-significant it does, and every record in a causal chain shares a correlation id. You don't build the trace — you read it.
One stream, every significant act
Each dispatch, event, reaction, query, timer, and external call becomes a typed telemetry record on a single tap:
app.runtime.onTelemetry((rec) => {
switch (rec.kind) {
case "action.dispatched": metrics.inc("actions", { name: rec.action }); break;
case "action.failed": alerts.fire({ action: rec.action, err: rec.error }); break;
}
});That's the whole subscription surface. The telemetry kinds reference lists every record type and its fields; you switch on rec.kind and react.
Correlation is automatic
Every record carries an envelope with correlationId, causationId, and messageId. A handler that emits an event, whose listener dispatches a follow-up, produces a chain that all shares one correlationId:
registerUser dispatched correlationId = C
└─ UserRegistered published correlationId = C
└─ welcome listener fired correlationId = C
└─ sendWelcomeEmail correlationId = CYou didn't pass an id through five function calls. The envelope threads it for you — ctx.send and ctx.emit carry it forward — so the trace reconstructs the actual causal shape, not a flat pile of log lines.
Logs ride the same stream
Application logs aren't a separate channel. Every ctx.logger call — and the framework's own request, lifecycle, and error lines — writes to the configured driver and emits a log telemetry record, so a log shows up in Studio next to the dispatch that produced it.
The driver is pino, shaped by NODE_ENV: pretty and colorized in development, JSON lines in production. The templates wire it for you:
import { resolveLogger } from "@nwire/logger-pino";
const app = createApp({ logger: resolveLogger({ service: "orders" }) });Every HTTP request is logged once — method, path, status, duration, correlation id — at a level that tracks the status class. Set DEBUG=dispatch,hooks (or DEBUG=true) to surface the matching telemetry kinds to the console as well, for when you want the dispatch flow without opening Studio.
Seeing it live: Studio
Turn on the inspect plugin and the stream is served over HTTP for the bundled trace viewer:
await endpoint("orders", { port: 3000 })
.use(httpKoa({ inspect: true }))
.mount(app)
.run();Studio reads that stream and shows a live, correlation-grouped view — a domain trace (this request caused that event caused that email), not a wall of spans. Its Logs page tails the log records — level, message, fields — filtered and searchable. The same /_nwire/* endpoints also serve recent telemetry and the OpenAPI spec, so the viewer needs no separate datastore.
Exporting to your own stack
The stream is yours to forward. The OpenTelemetry bridge translates it to OTLP spans and events for Datadog, Honeycomb, Tempo, or GreptimeDB:
import { attachOtelExporter } from "@nwire/telemetry-otel";
import { trace } from "@opentelemetry/api";
attachOtelExporter(app.runtime, { tracer: trace.getTracer("orders") });The correlation ids become real span parent/child links, so the same causal tree you see in Studio shows up in your tracing backend.
See also
- Telemetry — the underlying stream: every record kind and its exact shape.
- Studio — the trace viewer in depth.
- Observability (OpenTelemetry) recipe — exporting end to end.