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.
Quick Start
1. Install
pnpm add evlog
npm install evlog
yarn add evlog
bun 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' },
}),
],
})
import { defineNitroConfig } from 'nitropack/config'
import evlog from 'evlog/nitro'
export default defineNitroConfig({
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 }
})
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-...
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
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' },
},
})
],
})
import { defineNitroConfig } from 'nitropack/config'
import evlog from 'evlog/nitro'
export default defineNitroConfig({
modules: [
evlog({
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
Use Nitro plugin hooks to send logs to external services and enrich them with additional context.
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.
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 },
],
},
})
],
})
import { defineNitroConfig } from 'nitropack/config'
import evlog from 'evlog/nitro'
export default defineNitroConfig({
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:
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.