Nitro
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
bun add evlog
yarn add evlog
npm install evlog
2. Create 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(),
],
})
import { defineNitroConfig } from 'nitropack/config'
import evlog from 'evlog/nitro'
export default defineNitroConfig({
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 }
})
import { defineEventHandler, readBody } from 'h3'
import { useLogger } from 'evlog/nitro'
export default defineEventHandler(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:
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',
})
})
import { defineEventHandler } from 'h3'
import { useLogger } from 'evlog/nitro'
import { createError } from 'evlog'
export default defineEventHandler(async (event) => {
const log = useLogger(event)
throw createError({
status: 402,
message: 'Payment failed',
why: 'Card declined by issuer',
fix: 'Try a different payment method',
})
})
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:
export default defineEvlog({
include: ['/api/**'],
exclude: ['/api/health'],
routes: {
'/api/auth/**': { service: 'auth-service' },
'/api/payment/**': { service: 'payment-service' },
},
})
include and exclude, it will be excluded.Drain & Enrichers
Set drain and enrich in 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
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)
})
definePlugin from nitro instead of defineNitroPlugin.Enricher Plugin
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)
})
})
Sampling
Head Sampling
Randomly keep a percentage of logs per level. Runs before the request completes.
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:
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
})
})
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, andlinkfields