Tracing

Instrument the request lifecycle with diagnostics channels and the built-in tracing logger.
nitro.config.ts
import { defineConfig } from "nitro";

export default defineConfig({
  serverDir: true,

  // Instrument the h3, srvx and unstorage tracing channels.
  tracingChannel: true,

  experimental: {
    // Log every completed span to the console (built-in, dependency-free sink).
    tracingLogger: true,
  },
});

Nitro can instrument its request lifecycle through Node diagnostics channels — no OpenTelemetry SDK required. This example turns instrumentation on and enables the built-in console logger, which groups every h3, srvx and unstorage span into a per-request timeline (waterfall).

Enabling tracing

nitro.config.ts
import { defineConfig } from "nitro";

export default defineConfig({
  // Instrument the h3, srvx and unstorage tracing channels.
  tracingChannel: true,

  experimental: {
    // Log every completed span to the console (built-in, dependency-free sink).
    tracingLogger: true,
  },
});

tracingChannel: true wires up the producers (it accepts { h3, srvx, unstorage } to trace a subset). experimental.tracingLogger adds a built-in sink that console.logs each completed span — a dependency-free alternative to a vendor exporter, handy for local development.

Try it

npm run dev
# then, in another terminal:
curl http://localhost:3000/
curl http://localhost:3000/users/42

Each request prints a timeline of its spans to the console — the middleware, the matched route, and every storage operation, positioned and sized by when they ran and how long they took:

▶ GET /  4.10ms  (4 spans)
  middleware GET /             █·······················   0.12ms h3.handler_type=middleware http.route=/
  GET /                        ·███████████████████·····   2.49ms h3.handler_type=route http.route=/
  getItem                      ···██····················   0.18ms db.operation=getItem db.system=memory unstorage.keys_count=1
  setItem                      ·····██··················   0.16ms db.operation=setItem db.system=memory unstorage.keys_count=1

The header line () is the request itself — its method, path and total time; the rows below are the spans that ran within it. Note the dynamic route is named by its matched template — GET /users/:id, not /users/42 — keeping span names low-cardinality per the OpenTelemetry HTTP conventions.

The request boundary comes from Nitro's request/response runtime hooks, so grouping works identically in vite dev and in a production build. Spans are grouped per request via async context, so timelines for concurrent requests stay separate, and a failed span is marked with its error message.

What gets traced

ChannelSpanEmitted by
h3.requesteach matched route and middlewareserver/routes/*, server/middleware/*
unstorage.*each storage operation (getItem, setItem, …)useStorage() in server/routes/index.ts
srvx.requestthe whole request, with response status (production server)the srvx server layer