Nuxt
evlog provides a first-class Nuxt module with auto-imported useLogger, createError, and parseError. Add it to your config and start logging with zero boilerplate.
Set up evlog in my Nuxt app
Quick Start
1. Install
pnpm add evlog
bun add evlog
yarn add evlog
npm install evlog
2. Add the module
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
env: {
service: 'my-app',
},
},
})
That's it. useLogger, createError, and parseError are auto-imported.
Wide Events
Build up context progressively throughout a request with useLogger(event). evlog emits a single wide event when the request completes.
export default defineEventHandler(async (event) => {
const log = useLogger(event)
const body = await readBody(event)
log.set({ user: { id: body.userId, plan: 'enterprise' } })
const cart = await db.findCart(body.cartId)
log.set({ cart: { items: cart.items.length, total: cart.total } })
const payment = await processPayment(cart)
log.set({ payment: { method: payment.method, cardLast4: payment.last4 } })
return { success: true, orderId: payment.orderId }
})
One request, one log line with all context:
10:23:45 INFO [my-app] POST /api/checkout 200 in 145ms
├─ user: id=usr_123 plan=enterprise
├─ cart: items=3 total=14999
├─ payment: method=card cardLast4=4242
└─ 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.
export default defineEventHandler(async (event) => {
const log = useLogger(event)
const body = await readBody(event)
log.set({ payment: { amount: body.amount } })
if (body.amount <= 0) {
throw createError({
status: 400,
message: 'Invalid payment amount',
why: 'The amount must be a positive number',
fix: 'Pass a positive integer in cents (e.g. 4999 for $49.99)',
link: 'https://docs.example.com/api/payments#amount',
})
}
return { success: true }
})
EvlogError and returns a structured JSON response with why, fix, and link fields.Configuration
enabled, pretty, silent, sampling, middleware options, etc.).All options are set in nuxt.config.ts under the evlog key:
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Globally enable/disable all logging. When false, all operations become no-ops |
console | boolean | true | Enable/disable browser console output |
env.service | string | 'app' | Service name shown in logs |
env.environment | string | Auto-detected | Environment name |
include | string[] | undefined | Route patterns to log. Supports glob (/api/**) |
exclude | string[] | undefined | Route patterns to exclude. Exclusions take precedence |
routes | Record<string, RouteConfig> | undefined | Route-specific service configuration |
pretty | boolean | true in dev | Pretty print with tree formatting |
silent | boolean | false | Suppress console output. Events are still built, sampled, and drained. Use for stdout-based platforms |
sampling.rates | object | undefined | Head sampling rates per log level (0-100%) |
sampling.keep | array | undefined | Tail sampling conditions to force-keep logs |
transport.enabled | boolean | false | Enable client-to-server log transport |
transport.endpoint | string | '/api/_evlog/ingest' | Transport endpoint |
Route Filtering
Use include and exclude to control which routes are logged:
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
include: ['/api/**', '/auth/**'],
exclude: [
'/api/_nuxt_icon/**',
'/api/_content/**',
'/api/health',
],
},
})
include and exclude, it will be excluded.Route-Based Service Names
Assign different service names to different route groups:
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
env: { service: 'default-service' },
routes: {
'/api/auth/**': { service: 'auth-service' },
'/api/payment/**': { service: 'payment-service' },
'/api/booking/**': { service: 'booking-service' },
},
},
})
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)
})
Enricher Plugin
import {
createUserAgentEnricher,
createGeoEnricher,
createRequestSizeEnricher,
createTraceContextEnricher,
} from 'evlog/enrichers'
const enrichers = [
createUserAgentEnricher(),
createGeoEnricher(),
createRequestSizeEnricher(),
createTraceContextEnricher(),
]
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 defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
sampling: {
rates: {
info: 10,
warn: 50,
debug: 5,
error: 100,
},
},
},
})
Each level is a percentage from 0 to 100. Levels you don't configure default to 100% (keep everything). Error defaults to 100% even when other levels are configured.
Tail Sampling
Evaluate after the request completes and force-keep logs that match specific conditions, regardless of head sampling.
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
sampling: {
rates: { info: 10 },
keep: [
{ duration: 1000 },
{ status: 400 },
{ path: '/api/critical/**' },
],
},
},
})
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.Client Transport
Send browser logs to your server for processing and draining alongside server-side events.
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
transport: {
enabled: true,
endpoint: '/api/_evlog/ingest',
},
},
})
How It Works
- Client calls
log.info({ action: 'click', button: 'submit' }) - Log is sent to
/api/_evlog/ingestvia POST - Server enriches with environment context
evlog:drainhook is called withsource: 'client'- External services receive the log
Client Identity
Attach user context to every client log with setIdentity:
// After login
setIdentity({ userId: 'usr_123', orgId: 'org_456' })
log.info({ action: 'checkout' })
// -> { userId: 'usr_123', orgId: 'org_456', action: 'checkout', ... }
// After logout
clearIdentity()
Syncing Identity with Auth
Use a route middleware to keep identity in sync with your auth state:
export default defineNuxtRouteMiddleware(() => {
const { user } = useAuth()
if (user.value) {
setIdentity({ userId: user.value.id, email: user.value.email })
} else {
clearIdentity()
}
})
Production Tips
Use Nuxt's $production override to keep full logging in development while sampling and disabling console output in production:
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
env: { service: 'my-app' },
},
$production: {
evlog: {
console: false,
sampling: {
rates: { info: 10, warn: 50, debug: 0 },
keep: [{ duration: 1000 }, { status: 400 }],
},
},
},
})
Next Steps
Deepen your Nuxt 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