Frameworks

Nitro

One module covers Nitro v3 and v2. Reach the request logger with `useLogger(event)`, and set drain and enrichers from Nitro plugins.

evlog provides modules for both Nitro v3 and Nitro v2 (nitropack). The module hooks into the request lifecycle, creating a request-scoped logger accessible via useLogger(event), and emits a wide event when the response completes.

Set up evlog in my Nitro app

Quick Start

1. Install

pnpm add evlog

2. Create evlog.config.ts

evlog.config.ts
import { defineEvlog } from 'evlog'

export default defineEvlog({
  service: 'my-app',
})

Every option lives in this file: sampling, drain, enrich, route filters. npx evlog init writes it for you. See Configuration.

3. Add the module

import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'

export default defineConfig({
  modules: [
    evlog(),
  ],
})

The module finds evlog.config.ts and loads it, with nothing to import.

Wide Events

Build up context progressively throughout a request with useLogger(event). evlog emits a single wide event when the request completes.

import { defineHandler } from 'nitro/h3'
import { useLogger } from 'evlog/nitro/v3'

export default defineHandler(async (event) => {
  const log = useLogger(event)
  const body = await readBody(event)

  log.set({ user: { id: body.userId } })
  log.set({ cart: { items: body.items.length, total: body.total } })

  return { success: true }
})

One request, one log line with all context:

Terminal output
10:23:45 INFO [my-app] POST /api/checkout 200 in 145ms
  ├─ user: id=usr_123
  ├─ cart: items=3 total=14999
  └─ requestId: a1b2c3d4-...

Nitro uses useLogger(event) (event-bound scope), not AsyncLocalStorage, so log.fork() is not available here yet. For AI SDK streaming responses, evlog defers wide-event emit until the response body finishes so createAILogger(log) metadata stays on the same request event. Post-emit warnings only apply when code calls set() after the wide event has actually emitted, for example in non-streaming handlers or background work. See After emit in Wide events.

Error Handling

createError produces structured errors with why, fix, and link fields that help both humans and AI agents understand what went wrong.

import { defineHandler } from 'nitro/h3'
import { useLogger, createError } from 'evlog/nitro/v3'

export default defineHandler(async (event) => {
  const log = useLogger(event)

  throw createError({
    status: 402,
    message: 'Payment failed',
    why: 'Card declined by issuer',
    fix: 'Try a different payment method',
  })
})
In Nitro v3, import createError from evlog/nitro/v3 - it wraps the Nitro error handler. In Nitro v2, import createError from evlog directly.

Configuration

Every option goes in evlog.config.ts. See the Configuration reference for the full list.

Options passed to evlog() in nitro.config.ts override the file, merged key by key. The file's drain, enrich and keep are never overridden: they run alongside the evlog:* hooks, so an event reaches both.

Route Filtering

Use include and exclude to control which routes are logged, and routes to assign different service names to different route groups:

evlog.config.ts
export default defineEvlog({
  include: ['/api/**'],
  exclude: ['/api/health'],
  routes: {
    '/api/auth/**': { service: 'auth-service' },
    '/api/payment/**': { service: 'payment-service' },
  },
})
Exclusions take precedence. If a path matches both include and exclude, it will be excluded.

Drain & Enrichers

Set drain and enrich in evlog.config.ts:

evlog.config.ts
import { defineEvlog } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'

export default defineEvlog({
  service: 'my-app',
  drain: createAxiomDrain(),
  enrich: (ctx) => {
    ctx.event.region = process.env.FLY_REGION
  },
})

A buffered drain from the file, such as one built with createDrainPipeline, is flushed when the server closes. Nitro plugins hooking evlog:drain and evlog:enrich, as below, still work and run alongside the file: both receive every event.

Drain Plugin

server/plugins/evlog-drain.ts
import type { DrainContext } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 50, intervalMs: 5000 },
  retry: { maxAttempts: 3 },
})
const drain = pipeline(createAxiomDrain())

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:drain', drain)
})
For Nitro v3 standalone, use definePlugin from nitro instead of defineNitroPlugin.

Enricher Plugin

server/plugins/evlog-enrich.ts
import { createUserAgentEnricher, createGeoEnricher } from 'evlog/enrichers'

const enrichers = [createUserAgentEnricher(), createGeoEnricher()]

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:enrich', (ctx) => {
    for (const enricher of enrichers) enricher(ctx)
  })
})
See the Adapters and Enrichers docs for the full list of available drains and enrichers.

Sampling

Head Sampling

Randomly keep a percentage of logs per level. Runs before the request completes.

evlog.config.ts
export default defineEvlog({
  sampling: {
    rates: { info: 10, warn: 50, debug: 5 },
    keep: [
      { duration: 1000 },
      { status: 400 },
    ],
  },
})

Each level is a percentage from 0 to 100. Levels you don't configure default to 100% (keep everything).

Custom Tail Sampling

For conditions beyond status, duration, and path, use the evlog:emit:keep hook:

server/plugins/evlog-sampling.ts
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:emit:keep', (ctx) => {
    const user = ctx.context.user as { premium?: boolean } | undefined
    if (user?.premium) ctx.shouldKeep = true
  })
})
Errors are always kept by default. You have to explicitly set error: 0 to drop them.

Next Steps

Deepen your Nitro integration:

  • Wide Events: Design comprehensive events with context layering
  • Adapters: Send logs to Axiom, Sentry, PostHog, and more
  • Sampling: Control log volume with head and tail sampling
  • Structured Errors: Throw errors with why, fix, and link fields