evlog
Digging through logs is not observability. It's hope.
A single request generates 10+ log lines. When production breaks at 3am, you're sifting scattered lines for a needle of signal. Your errors say "Something went wrong", which helps nobody.
evlog is different. One wide event per operation. All the context. Errors that explain why and what to do next.
Why evlog?
The Problem
// server/api/checkout.post.ts
// Scattered logs - impossible to debug
console.log('Request received')
console.log('User:', user.id)
console.log('Cart loaded')
console.log('Payment failed') // Good luck finding this at 3am
throw new Error('Something went wrong')
The Solution
// server/api/checkout.post.ts
import { useLogger } from 'evlog'
// One comprehensive event per request
export default defineEventHandler(async (event) => {
const log = useLogger(event) // Auto-injected by evlog
log.set({ user: { id: user.id, plan: 'premium' } })
log.set({ cart: { items: 3, total: 9999 } })
log.error(error, { step: 'payment' })
// Emits ONE event with ALL context + duration (automatic)
})
Output:
{
"timestamp": "2025-01-24T10:23:45.612Z",
"level": "error",
"service": "my-app",
"method": "POST",
"path": "/api/checkout",
"duration": "1.2s",
"durationMs": 1204,
"user": { "id": "123", "plan": "premium" },
"cart": { "items": 3, "total": 9999 },
"error": { "message": "Card declined", "step": "payment" }
}
Built for AI-Assisted Development
We're in the age of AI agents writing and debugging code. When an agent encounters an error, it needs clear, structured context to understand what happened and how to fix it.
Traditional logs force agents to grep through noise. evlog gives them:
- One event per request with all context in one place
- Self-documenting errors with
whyandfixfields - Structured JSON that's easy to parse and reason about
Your AI copilot will thank you.
Installation
npm install evlog
Nuxt Integration
The recommended way to use evlog. Zero config, everything just works.
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
env: {
service: 'my-app',
},
// Optional: only log specific routes (supports glob patterns)
include: ['/api/**'],
},
})
Tip: Use
$productionto enable sampling only in production:export default defineNuxtConfig({ modules: ['evlog/nuxt'], evlog: { env: { service: 'my-app' } }, $production: { evlog: { sampling: { rates: { info: 10, warn: 50, debug: 0 } } }, }, })
That's it. Now use useLogger(event) in any API route:
// server/api/checkout.post.ts
import { useLogger, createError } from 'evlog'
export default defineEventHandler(async (event) => {
const log = useLogger(event)
// Authenticate user and add to wide event
const user = await requireAuth(event)
log.set({ user: { id: user.id, plan: user.plan } })
// Load cart and add to wide event
const cart = await getCart(user.id)
log.set({ cart: { items: cart.items.length, total: cart.total } })
// Process payment
try {
const payment = await processPayment(cart, user)
log.set({ payment: { id: payment.id, method: payment.method } })
} catch (error) {
log.error(error, { step: 'payment' })
throw createError({
message: 'Payment failed',
status: 402,
why: error.message,
fix: 'Try a different payment method or contact your bank',
})
}
// Create order
const order = await createOrder(cart, user)
log.set({ order: { id: order.id, status: order.status } })
return order
// log.emit() called automatically at request end
})
The wide event emitted at the end contains everything:
{
"timestamp": "2026-01-24T10:23:45.612Z",
"level": "info",
"service": "my-app",
"method": "POST",
"path": "/api/checkout",
"duration": "1.2s",
"durationMs": 1204,
"user": { "id": "user_123", "plan": "premium" },
"cart": { "items": 3, "total": 9999 },
"payment": { "id": "pay_xyz", "method": "card" },
"order": { "id": "order_abc", "status": "created" },
"status": 200
}
Nitro Integration
Works with any framework powered by Nitro: Nuxt, Analog, Vinxi, SolidStart, TanStack Start, and more.
Nitro v3
// nitro.config.ts
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'
export default defineConfig({
modules: [
evlog({ env: { service: 'my-api' } })
],
})
Nitro v2
// nitro.config.ts
import { defineNitroConfig } from 'nitropack/config'
import evlog from 'evlog/nitro'
export default defineNitroConfig({
modules: [
evlog({ env: { service: 'my-api' } })
],
})
Then use useLogger in any route. Import from evlog/nitro/v3 (v3) or evlog/nitro (v2). Usage is identical to the Nuxt section above; see the Nitro docs for the full route example.
Standalone TypeScript
For scripts, workers, or any TypeScript project:
// scripts/migrate.ts
import { initLogger, log, createRequestLogger } from 'evlog'
// Initialize once at script start
initLogger({
env: {
service: 'migration-script',
environment: 'production',
},
})
// Simple logging
log.info('migration', 'Starting database migration')
log.info({ action: 'migration', tables: ['users', 'orders'] })
// Or use request logger for a logical operation
const migrationLog = createRequestLogger({ action: 'full-migration' })
migrationLog.set({ tables: ['users', 'orders', 'products'] })
migrationLog.set({ rowsProcessed: 15000 })
migrationLog.emit()
// workers/sync-job.ts
import { initLogger, createRequestLogger, createError } from 'evlog'
initLogger({
env: {
service: 'sync-worker',
environment: process.env.NODE_ENV,
},
})
async function processSyncJob(job: Job) {
const log = createRequestLogger({ jobId: job.id, type: 'sync' })
try {
log.set({ source: job.source, target: job.target })
const result = await performSync(job)
log.set({ recordsSynced: result.count })
return result
} catch (error) {
log.error(error, { step: 'sync' })
throw error
} finally {
log.emit()
}
}
Cloudflare Workers
Use the Workers adapter for structured logs and correct platform severity. With initWorkersLogger({ drain }), use defineWorkerFetch so async drains are registered with waitUntil automatically (Cloudflare only passes ExecutionContext as the third fetch argument, and there is no global).
// src/index.ts
import { defineWorkerFetch, initWorkersLogger } from 'evlog/workers'
initWorkersLogger({
env: { service: 'edge-api' },
})
export default defineWorkerFetch(async (request, _env, _ctx, log) => {
try {
log.set({ route: 'health' })
const response = new Response('ok', { status: 200 })
log.emit({ status: response.status })
return response
} catch (error) {
log.error(error as Error)
log.emit({ status: 500 })
throw error
}
})
If you keep a raw export default { fetch }, pass { executionCtx: ctx } to createWorkersLogger or waitUntil on createRequestLogger.
// Lower-level (equivalent)
import { createWorkersLogger } from 'evlog/workers'
export default {
async fetch(request: Request, _env: unknown, ctx: ExecutionContext) {
const log = createWorkersLogger(request, { executionCtx: ctx })
// ...
},
}
Disable invocation logs to avoid duplicate request logs:
# wrangler.toml
[observability.logs]
invocation_logs = false
Notes:
- Prefer
defineWorkerFetchso you do not have to passexecutionCtxyourself when using a drain requestIddefaults tocf-raywhen availablerequest.cfis included (colo, country, asn) unless disabled- Use
headerAllowlistto avoid logging sensitive headers