Wide Events
Wide events are the core concept behind evlog. Instead of scattering logs throughout your codebase, you accumulate context over any unit of work, whether a request, script, job, or workflow, and emit a single, comprehensive log event.
Why Wide Events?
Traditional logging creates noise:
logger.info('Job started')
logger.info('User authenticated', { userId: user.id })
logger.info('Fetching data', { source: 'postgres' })
logger.info('Processing records')
logger.info('Processing complete')
logger.info('Job finished', { duration: 234 })
This approach has problems:
- Scattered context: Information is spread across multiple log lines
- Hard to correlate: Matching logs to operations requires IDs everywhere
- Noise: 10+ log lines per operation makes finding issues harder
- Incomplete: Some logs might be missing if errors occur
Wide events solve this:
import { useLogger } from 'evlog'
const log = useLogger(event)
log.set({ user: { id: 1, plan: 'pro' } })
log.set({ cart: { id: 42, items: 3, total: 9999 } })
log.set({ payment: { method: 'card', status: 'success' } })
import { createLogger } from 'evlog'
const log = createLogger({ jobId: 'sync-001', queue: 'emails' })
log.set({ source: 'postgres', target: 's3' })
log.set({ records: { found: 1250, synced: 1250 } })
log.emit()
[INFO] POST /api/checkout (234ms)
user: { id: 1, plan: 'pro' }
cart: { id: 42, items: 3, total: 9999 }
payment: { method: 'card', status: 'success' }
status: 200
One log, all context. Everything you need to understand what happened.
Creating Wide Events
createLogger (General Purpose)
Use createLogger() for scripts, background jobs, queue workers, cron jobs, or any operation where you manage the lifecycle:
import { initLogger, createLogger } from 'evlog'
initLogger({ env: { service: 'migrate' } })
const log = createLogger({ task: 'user-migration' })
const users = await db.query('SELECT * FROM legacy_users')
log.set({ found: users.length })
let migrated = 0
for (const user of users) {
await newDb.upsert({ id: user.id, email: user.email, plan: user.plan })
migrated++
}
log.set({ migrated, status: 'complete' })
log.emit()
createRequestLogger (HTTP Contexts)
Use createRequestLogger() when working with HTTP requests outside of a framework integration. It's a thin wrapper around createLogger that pre-populates method, path, and requestId:
import { initLogger, createRequestLogger } from 'evlog'
initLogger({ env: { service: 'my-worker' } })
const log = createRequestLogger({ method: 'POST', path: '/api/checkout' })
log.set({ user: { id: 1, plan: 'pro' } })
log.set({ cart: { items: 3, total: 9999 } })
log.emit()
createLogger and createRequestLogger require a manual log.emit() call. The event won't be emitted until you call it.useLogger (Retrieving the Request Logger)
When using a framework integration (Nuxt, Hono, Express, etc.), the middleware creates a wide event logger automatically on each request. useLogger(event) retrieves that logger from the request context:
import { useLogger } from 'evlog'
export default defineEventHandler(async (event) => {
const log = useLogger(event)
log.set({ user: { id: 1, plan: 'pro' } })
log.set({ cart: { items: 3, total: 9999 } })
return { success: true }
// auto-emitted on response end
})
useLogger doesn't create a logger, it retrieves the one the framework middleware already attached to the event. The middleware handles creation and emission automatically. In Nuxt, useLogger is auto-imported.Anatomy of a Wide Event
A well-designed wide event contains context from multiple layers. The examples below show what to add inside your handler or script. They assume log is already created via createLogger, createRequestLogger, or useLogger.
Operation Context
Basic information about the operation:
import { useLogger } from 'evlog'
const log = useLogger(event)
log.set({
method: 'POST',
path: '/api/checkout',
requestId: 'abc-123-def',
})
import { createLogger } from 'evlog'
const log = createLogger({
jobId: 'sync-001',
queue: 'emails',
source: 'postgres',
})
method, path, requestId) is auto-populated by the middleware. You don't need to set these fields manually.User / Actor Context
Who triggered the operation:
log.set({
userId: user.id,
email: user.email,
subscription: user.plan,
accountAge: daysSince(user.createdAt),
})
Business Context
Domain-specific data relevant to the operation:
log.set({
cart: {
id: cart.id,
items: cart.items.length,
total: cart.total,
currency: 'USD',
},
shipping: {
method: 'express',
country: address.country,
},
coupon: appliedCoupon?.code,
})
Outcome
The result of the operation:
log.set({
status: 200,
duration: Date.now() - startTime,
success: true,
})
log.set({
status: 500,
error: {
message: err.message,
code: err.code,
type: err.constructor.name,
},
})
Best Practices
Use Meaningful Keys
// Avoid generic keys
log.set({ data: { id: 123 } })
// Use specific, descriptive keys
log.set({ order: { id: 123, status: 'pending' } })
Group Related Data
// Flat structure is hard to read
log.set({
userId: 1,
userEmail: 'a@b.com',
cartId: 2,
cartTotal: 100,
})
// Grouped structure is clearer
log.set({
user: { id: 1, email: 'a@b.com' },
cart: { id: 2, total: 100 },
})
Add Context Incrementally
Call log.set() as you gather information:
import { useLogger } from 'evlog'
export default defineEventHandler(async (event) => {
const log = useLogger(event)
const user = await getUser(event)
log.set({ user: { id: user.id, plan: user.plan } })
const cart = await getCart(user.id)
log.set({ cart: { items: cart.items.length, total: cart.total } })
const payment = await processPayment(cart)
log.set({ payment: { method: payment.method, status: payment.status } })
return { success: true }
})
[INFO] POST /api/checkout (456ms)
user: { id: 1, plan: 'pro' }
cart: { items: 3, total: 9999 }
payment: { method: 'card', status: 'success' }
status: 200
Handle Errors Gracefully
When errors occur, the wide event still emits with error context:
import { useLogger } from 'evlog'
export default defineEventHandler(async (event) => {
const log = useLogger(event)
try {
const result = await processPayment(cart)
return result
} catch (err) {
log.set({
error: {
message: err.message,
code: err.code,
type: err.constructor.name,
},
})
throw err
}
})
[ERROR] POST /api/checkout (123ms)
user: { id: 1, plan: 'pro' }
cart: { items: 3, total: 9999 }
error: {
message: 'Card declined',
code: 'CARD_DECLINED',
type: 'PaymentError'
}
status: 500
Output Formats
evlog automatically switches between formats based on environment: pretty in development, JSON in production. This is the default behavior, no configuration needed.
[INFO] POST /api/checkout (234ms)
user: { id: 1, plan: 'pro' }
cart: { items: 3, total: 9999 }
payment: { method: 'card', status: 'success' }
{
"level": "info",
"method": "POST",
"path": "/api/checkout",
"duration": 234,
"user": { "id": 1, "plan": "pro" },
"cart": { "items": 3, "total": 9999 },
"payment": { "method": "card", "status": "success" }
}
Next Steps
- Simple Logging - Fire-and-forget logs when you don't need context accumulation
- Typed Fields - Add compile-time type safety to your wide events
- Structured Errors - Errors with actionable context
- Frameworks - Auto-managed request logging per framework
Simple Logging
Structured logging for everyday use. Replace console.log with log.info, log.error, log.warn, and log.debug. Fire-and-forget events with pretty output in dev and JSON in production.
Structured Errors
Create errors that explain why they occurred and how to fix them. Add actionable context with why, fix, and link fields for humans and AI agents.