---
title: "Wide Events"
description: "Accumulate context over any unit of work and emit a single comprehensive event. Works for HTTP requests, scripts, background jobs, queue workers, and workflows."
canonical_url: "https://www.evlog.dev/learn/wide-events"
---
# Wide Events

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

A wide event is one structured log event per unit of work, such as an HTTP request, a script run or a queue job. It carries every field that work touched: who called, what they sent, what happened and how it ended. The same pattern is known as [canonical log lines](https://stripe.com/blog/canonical-log-lines) at Stripe.

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.

<callout color="neutral" icon="i-lucide-globe">

Not running an HTTP framework? See [Standalone TypeScript](https://www.evlog.dev/integrate/frameworks/standalone) and [Cloudflare Workers](https://www.evlog.dev/integrate/frameworks/cloudflare-workers), because wide events apply just as cleanly outside of request lifecycles.

</callout>

<prompt :actions="["copy","cursor","claude"]" description="Convert my request handlers to wide events" icon="i-lucide-layers">

Convert my existing request handlers from scattered logs to evlog wide events.

- Find handlers that call console.log/logger.info multiple times per request
- Replace those with a single useLogger(event) (or framework equivalent) at the top
- Use log.set({ user, cart, payment, ... }) to accumulate context as the request progresses
- Group related fields into nested objects (user, cart, payment) instead of flat keys
- Remove redundant info-level logs once the wide event captures the same information
- Keep error logs that capture distinct failure cases via log.error()
- Trust the framework integration to auto-emit one wide event per request

Docs: [https://www.evlog.dev/learn/wide-events](https://www.evlog.dev/learn/wide-events)
Best practices: [https://www.evlog.dev/reference/best-practices](https://www.evlog.dev/reference/best-practices)

</prompt>

## Why Wide Events?

<wide-event-collapse>



</wide-event-collapse>

Traditional logging creates noise:

```typescript [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:

<code-group>

```typescript [server/api/checkout.post.ts]
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' } })
```

```typescript [scripts/sync-data.ts]
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()
```

```bash [Output]
[INFO] POST /api/checkout (234ms)
  user: { id: 1, plan: 'pro' }
  cart: { id: 42, items: 3, total: 9999 }
  payment: { method: 'card', status: 'success' }
  status: 200
```

</code-group>

One log, all context. Everything you need to understand what happened.

## Accumulate context, emit once

### `createLogger` (General Purpose)

Use `createLogger()` for scripts, background jobs, queue workers, cron jobs, or any operation where you manage the lifecycle:

```typescript [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`:

```typescript [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()
```

<callout color="info" icon="i-lucide-info">

Both `createLogger` and `createRequestLogger` require a manual `log.emit()` call. The event won't be emitted until you call it.

</callout>

### `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:

<framework-tabs>

```typescript [Nuxt]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
})
```

```typescript [Nitro]routes/api/checkout.post.ts
import { defineHandler } from 'nitro/h3'
import { useLogger } from 'evlog/nitro/v3'

export default defineHandler(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
})
```

```typescript [Next.js]app/api/checkout/route.ts
import { withEvlog, useLogger } from '@/lib/evlog'

export const POST = withEvlog(async () => {
  const log = useLogger()

  log.set({ user: { id: 1, plan: 'pro' } })
  log.set({ cart: { items: 3, total: 9999 } })

  return Response.json({ success: true })
  // auto-emitted on response end
})
```

```typescript [Hono]src/index.ts
app.post('/api/checkout', async (c) => {
  const log = c.get('log')

  log.set({ user: { id: 1, plan: 'pro' } })
  log.set({ cart: { items: 3, total: 9999 } })

  return c.json({ success: true })
  // auto-emitted on response end
})
```

```typescript [Express]src/index.ts
app.post('/api/checkout', async (req, res) => {
  const log = req.log

  log.set({ user: { id: 1, plan: 'pro' } })
  log.set({ cart: { items: 3, total: 9999 } })

  return res.json({ success: true })
  // auto-emitted on response end
})
```

</framework-tabs>

<callout color="info" icon="i-lucide-info">

`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.

</callout>

## After emit: sealing and background work

When the wide event is **emitted** (automatically at the end of the request, or when you call `log.emit()` yourself), that logger instance is **sealed**. Further `set`, `error`, `info`, and `warn` calls do **not** update the event that was already sent to your drains. They are ignored and evlog prints a **[evlog] warning** to the console with the keys that were dropped. This also applies when **head sampling** discards the event (`emit()` returned `null`): the logger is still sealed for that unit of work.

This matters for **async work that outlives the handler** (fire-and-forget promises, `setTimeout`, tasks started but not awaited). On many runtimes, `AsyncLocalStorage` keeps returning the same request logger, so `useLogger()` still succeeds even though the HTTP response, and the wide event, are already finished. Without warnings, that looks like silent data loss.

### `log.fork(label, fn)`

For intentional background work that should produce **its own** wide event, use **log.fork(label, fn)** when your integration provides it (Express, Fastify, NestJS, SvelteKit, React Router, Next.js `withEvlog`, Elysia). Inside `fn`, `useLogger()` resolves to the forked logger. When `fn` completes (or throws), that logger emits an event with:

- **operation**: the `label` you passed
- **_parentRequestId**: the parent request’s `requestId` (for correlation in queries and dashboards)

The parent wide event may be emitted **before** the child event; they are two separate events ordered by time.

`log.fork()` returns a promise that resolves once the child event has drained, and it never rejects. Leave it unawaited for background work. Await it when the child carries a write that must land before the response, such as an audit event routed to `auditOnly(sink, { await: true })`:

```typescript
await log.fork('orders.cancel', async () => {
  useLogger().audit({
    action: 'orders.cancelled',
    actor: { type: 'user', id: user.id },
    target: { type: 'order', id: order.id },
  })
})
```

With `waitUntil` set (Next.js, Cloudflare Workers, Vercel Edge), an unawaited fork is registered with it when you call `log.fork()`, so the runtime stays alive until the child drains.

**Not available yet:** Hono (no `useLogger` without `c.get('log')` + ALS) and Nitro/Nuxt `useLogger(event)`, so use the post-emit warnings to catch mistakes; a different API may arrive later for event-scoped forks.

For AI SDK streaming responses, supported framework integrations (Next.js, Nitro/Nuxt, SvelteKit, Hono, React Router, oRPC) defer wide-event emit until the response body finishes, so `createAILogger(log)` metadata lands on the same request event automatically.

```typescript [server/routes/checkout.post.ts]
import { evlog, useLogger } from 'evlog/express'

// Inside a route after evlog middleware:
const log = req.log
log.set({ order_dispatched: true })

log.fork?.('process_order', async () => {
  const child = useLogger()
  child.set({ inventory_checked: true })
})
```

## Read a wide event field by field

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:

<code-group>

```typescript [server/api/checkout.post.ts]
import { useLogger } from 'evlog'

const log = useLogger(event)
log.set({
  method: 'POST',
  path: '/api/checkout',
  requestId: 'abc-123-def',
})
```

```typescript [scripts/sync-data.ts]
import { createLogger } from 'evlog'

const log = createLogger({
  jobId: 'sync-001',
  queue: 'emails',
  source: 'postgres',
})
```

</code-group>

<callout color="info" icon="i-lucide-info">

In framework integrations, request context (`method`, `path`, `requestId`) is auto-populated by the middleware. You don't need to set these fields manually.

</callout>

### User / Actor Context

Who triggered the operation:

```typescript [services/checkout.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:

```typescript [services/checkout.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:

<code-group>

```typescript [Success]
log.set({
  status: 200,
  duration: Date.now() - startTime,
  success: true,
})
```

```typescript [Error]
log.set({
  status: 500,
  error: {
    message: err.message,
    code: err.code,
    type: err.constructor.name,
  },
})
```

</code-group>

## Best Practices

### Use Meaningful Keys

```typescript [services/orders.ts]
// Avoid generic keys
log.set({ data: { id: 123 } })

// Use specific, descriptive keys
log.set({ order: { id: 123, status: 'pending' } })
```

### Group Related Data

```typescript [services/checkout.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:

<code-group>

```typescript [server/api/checkout.post.ts]
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 }
})
```

```bash [Output]
[INFO] POST /api/checkout (456ms)
  user: { id: 1, plan: 'pro' }
  cart: { items: 3, total: 9999 }
  payment: { method: 'card', status: 'success' }
  status: 200
```

</code-group>

### Handle Errors Gracefully

When errors occur, the wide event still emits with error context:

<framework-tabs>

```typescript [Nuxt]server/api/checkout.post.ts
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
  }
})
```

```typescript [Nitro]routes/api/checkout.post.ts
import { defineHandler } from 'nitro/h3'
import { useLogger } from 'evlog/nitro/v3'

export default defineHandler(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
  }
})
```

```typescript [Next.js]app/api/checkout/route.ts
import { withEvlog, useLogger } from '@/lib/evlog'

export const POST = withEvlog(async () => {
  const log = useLogger()

  try {
    const result = await processPayment(cart)
    return Response.json(result)
  } catch (err) {
    log.set({
      error: {
        message: err.message,
        code: err.code,
        type: err.constructor.name,
      },
    })
    throw err
  }
})
```

```typescript [Hono]src/index.ts
app.post('/api/checkout', async (c) => {
  const log = c.get('log')

  try {
    const result = await processPayment(cart)
    return c.json(result)
  } catch (err) {
    log.set({
      error: {
        message: err.message,
        code: err.code,
        type: err.constructor.name,
      },
    })
    throw err
  }
})
```

```typescript [Express]src/index.ts
app.post('/api/checkout', async (req, res) => {
  const log = req.log

  try {
    const result = await processPayment(cart)
    return res.json(result)
  } catch (err) {
    log.set({
      error: {
        message: err.message,
        code: err.code,
        type: err.constructor.name,
      },
    })
    throw err
  }
})
```

</framework-tabs>

```bash [Output]
ERROR [checkout] POST /api/checkout 402 in 123ms
  ├─ error: Card declined
  │     at server/api/checkout.post.ts:42
  │   ❯ 42 ┃ throw createError({ code: 'CARD_DECLINED', ... })
  │     Why: Issuer declined the charge
  │     Fix: Ask the customer to use another card
  │     stack (3 frames hidden in node_modules)
  ├─ user: id=1 plan=pro
  └─ cart: items=3 total=9999
```

### Setting the Level Manually

`log.error(err)` populates the `error` field with `{ name, message, stack }` and promotes the wide event to `level: 'error'`. When you want to control the `error` field yourself (typed error codes, no stack, or richer custom shapes), use `log.setLevel()` to promote the level without touching the context:

```typescript
log.setLevel('error')
log.set({
  error: {
    code: 'PAYMENT_DECLINED',
    reason: 'insufficient_funds',
  },
})
```

`setLevel()` accepts `'error' | 'warn' | 'info' | 'debug'` and wins over the level computed from `.error()` / `.warn()`. Combine it with `log.set()` to keep the wide event tidy while still routing through error-level sampling and drains.

## Choose how the event is rendered

evlog automatically switches between formats based on environment: pretty in development, JSON in production. This is the default behavior, no configuration needed.

<code-group>

```bash [Development (Pretty)]
[INFO] POST /api/checkout (234ms)
  user: { id: 1, plan: 'pro' }
  cart: { items: 3, total: 9999 }
  payment: { method: 'card', status: 'success' }
```

```json [Production (JSON)]
{
  "level": "info",
  "method": "POST",
  "path": "/api/checkout",
  "duration": 234,
  "user": { "id": 1, "plan": "pro" },
  "cart": { "items": 3, "total": 9999 },
  "payment": { "method": "card", "status": "success" }
}
```

</code-group>

## FAQ

<accordion type="single">
<accordion-item label="How is a wide event different from a structured log?">

Structured logging is the format: key-value fields, usually JSON, instead of free text. A wide event is a structured log with a scope. One unit of work produces one event, so the fields that would be spread over several lines sit on the same record and can be queried together.

</accordion-item>

<accordion-item label="Are wide events the same as traces?">

No. A wide event is a log record, while a span is a timed operation inside a trace. evlog does not create spans or metrics. It can attach the incoming `traceparent` to the event and export it over OTLP, so the event lands next to the spans your stack already emits ([evlog and OpenTelemetry](https://www.evlog.dev/compare/opentelemetry)).

</accordion-item>

<accordion-item label="Do wide events cost more to store?">

Usually less. Line-per-step logging repeats the level, timestamp and request context on every line. On the checkout request measured in [Log cost](https://www.evlog.dev/reference/cost), one event is 56% fewer bytes and 75% fewer events than four lines. To cut volume further, [sampling](https://www.evlog.dev/learn/sampling) drops routine events and can keep every error and slow request.

</accordion-item>

<accordion-item label="How does evlog emit a wide event?">

With a framework integration, the middleware creates a logger per request and emits it when the response ends, including when the handler throws. Next.js navigation signals such as `redirect()` and `notFound()` are rethrown without an event. `useLogger(event)` retrieves that logger. In scripts and jobs, `createLogger()` gives you the same logger and you call `log.emit()` yourself. After emit the logger is sealed, and `log.fork()` covers background work that needs its own event.

</accordion-item>

<accordion-item label="What should I put in a wide event?">

IDs and business context grouped by entity (`user`, `cart`, `payment`), plus the outcome. Log IDs rather than whole objects, and leave secrets out. Redaction is on by default in production once `initLogger()` has run, so a script that only calls `createLogger()` has none ([Redaction](https://www.evlog.dev/learn/redaction)). [Best Practices](https://www.evlog.dev/reference/best-practices) covers naming and what to skip.

</accordion-item>
</accordion>

## Next Steps

- [Simple Logging](https://www.evlog.dev/learn/simple-logging) - Fire-and-forget logs when you don't need context accumulation
- [Typed Fields](https://www.evlog.dev/learn/typed-fields) - Add compile-time type safety to your wide events
- [Structured Errors](https://www.evlog.dev/learn/structured-errors) - Errors with actionable context
- [Frameworks](https://www.evlog.dev/integrate/frameworks/overview) - Auto-managed request logging per framework

---

- [Structured Errors](https://www.evlog.dev/learn/structured-errors)
- [Best Practices](https://www.evlog.dev/reference/best-practices)


## Sitemap

See the full [sitemap](https://www.evlog.dev/sitemap.md) for all pages.
