Frameworks

Nitro

Using evlog with Nitro — automatic wide events, structured errors, drain adapters, enrichers, and tail sampling in Nitro v2 and v3 applications.

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.

Quick Start

1. Install

pnpm add evlog

2. Add the module

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

export default defineConfig({
  modules: [
    evlog({
      env: { service: 'my-app' },
    }),
  ],
})

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-...

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

Route Filtering

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

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

export default defineConfig({
  modules: [
    evlog({
      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

Use Nitro plugin hooks to send logs to external services and enrich them with additional context.

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.

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

export default defineConfig({
  modules: [
    evlog({
      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.