Logging
Accumulate context over any unit of work and emit a single comprehensive event. Works for HTTP requests, scripts, background jobs, queue workers, and workflows.

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:

src/service.ts
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' } })

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:

scripts/migrate-users.ts
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:

src/worker.ts
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()
Both 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:

server/api/checkout.post.ts
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',
})
In framework integrations, request context (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:

server/api/checkout.post.ts
log.set({
  userId: user.id,
  email: user.email,
  subscription: user.plan,
  accountAge: daysSince(user.createdAt),
})

Business Context

Domain-specific data relevant to the operation:

server/api/checkout.post.ts
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,
})

Best Practices

Use Meaningful Keys

server/api/orders.post.ts
// Avoid generic keys
log.set({ data: { id: 123 } })

// Use specific, descriptive keys
log.set({ order: { id: 123, status: 'pending' } })
server/api/checkout.post.ts
// 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 }
})

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
  }
})

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' }

Next Steps