---
title: "宽事件（Wide Events）"
description: "在任意工作单元上累积上下文并发出一个全面的事件。适用于 HTTP 请求、脚本、后端任务、队列工作者和工作流。"
canonical_url: "https://evlog.zhcndoc.com/learn/wide-events"
---
# 宽事件（Wide Events）

> 在任意工作单元上累积上下文并发出一个全面的事件。适用于 HTTP 请求、脚本、后端任务、队列工作者和工作流。

宽事件是 evlog 的核心概念。不是在代码库中分散记录日志，而是累积任意工作单元的上下文（无论是请求、脚本、任务还是工作流），并发出一个全面且集中的日志事件。

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

不使用 HTTP 框架？请参阅 [Standalone TypeScript](https://evlog.zhcndoc.com/integrate/frameworks/standalone) 和 [Cloudflare Workers](https://evlog.zhcndoc.com/integrate/frameworks/cloudflare-workers)，因为宽事件在请求生命周期之外同样适用。

</callout>

<prompt :actions="["copy","cursor","claude"]" description="将我的请求处理程序转换为宽事件" icon="i-lucide-layers">

将我现有的请求处理程序从零散的日志改为 evlog 宽事件。

- 找出每个请求中多次调用 console.log/logger.info 的处理程序
- 将这些替换为在顶部只调用一次 useLogger(event)（或框架等效方法）
- 使用 log.set({ user, cart, payment, ... }) 在请求推进时累积上下文
- 将相关字段分组到嵌套对象中（user、cart、payment），不要使用扁平键
- 当宽事件已捕获相同信息时，移除冗余的 info 级日志
- 保留通过 log.error() 捕获不同失败情况的错误日志
- 依赖框架集成在每个请求上自动发出一个宽事件

文档：[https://www.evlog.dev/learn/wide-events](https://www.evlog.dev/learn/wide-events)
最佳实践: [https://www.evlog.dev/reference/best-practices](https://www.evlog.dev/reference/best-practices)

</prompt>

## 为什么使用宽事件？

<wide-event-collapse>



</wide-event-collapse>

传统日志会制造噪音：

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

这种方式存在以下问题：

- **上下文分散**：信息分散在多条日志中
- **难以关联**：需要将日志匹配到操作时必须到处使用 ID
- **噪音过多**：每个操作产生 10 条以上日志会使排查问题更困难
- **不完整**：如果发生错误，部分日志可能缺失

宽事件解决了这些问题：

<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 [输出]
[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>

一条日志，包含所有上下文。足以理解发生了什么。

## 累积上下文，一次发出

### `createLogger`（通用用途）

在脚本、后端任务、队列工作者、定时任务或任何需要手动管理生命周期的操作中使用 `createLogger()`：

```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 上下文）

在框架集成之外处理 HTTP 请求时使用 `createRequestLogger()`。它是 `createLogger` 的轻量包装器，预填充了 `method`、`path` 和 `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">

`createLogger` 和 `createRequestLogger` 都需要手动调用 `log.emit()`。事件直到你调用它才会被发出。

</callout>

### `useLogger`（获取请求日志记录器）

在使用框架集成（Nuxt、Hono、Express 等）时，中间件会自动在每个请求上创建一个宽事件日志。`useLogger(event)` 从请求上下文中获取该日志：

<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 }
  // 在响应结束时自动发出
})
```

```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` 不会创建日志记录器，它获取的是框架中间件已经附加到事件上的日志。中间件会自动处理创建和发出。在 Nuxt 中，`useLogger` 是自动导入的。

</callout>

## 发出后：密封与后台工作

当宽事件被**发出**（在请求结束时自动发出，或当你手动调用 `log.emit()` 时），该日志记录器实例会被**密封**。后续的 `set`、`error`、`info` 和 `warn` 调用**不会**更新已经发送到接收端的事件。它们会被忽略，并且 evlog 会在控制台打印一条 **[evlog] 警告**，显示被丢弃的键。这也适用于**头采样**丢弃事件的情况（`emit()` 返回 `null`）：该工作单元的日志记录器仍然会被密封。

这对于**生命周期超过处理程序的异步工作**（即发即弃的 promise、`setTimeout`、已启动但未等待的任务）非常重要。在许多运行时中，`AsyncLocalStorage` 会持续返回同一个请求日志记录器，因此即使 HTTP 响应和宽事件都已结束，`useLogger()` 仍然会成功。如果没有警告，这看起来就像数据被静默丢失。

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

对于有意执行且应产生**其自身**宽事件的后台工作，当你的集成提供此功能时（Express、Fastify、NestJS、SvelteKit、React Router、Next.js `withEvlog`），请使用 **log.fork(label, fn)**。在 `fn` 内部，`useLogger()` 会解析为分叉后的日志记录器。当 `fn` 完成（或抛出异常）时，该日志记录器会发出一个包含以下内容的事件：

- **operation**：你传入的 `label`
- **_parentRequestId**：父请求的 `requestId`（用于在查询和仪表板中关联）

父宽事件可能在子事件**之前**发出；它们是按时间顺序排列的两个独立事件。

`log.fork()` 返回一个 promise，它会在子事件完成排空后解析，且永远不会 reject。对于后台工作，可以不等待它。当子事件包含必须在响应前落盘的写入时再等待它，例如发送到 `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 },
  })
})
```

设置了 `waitUntil` 时（Next.js、Cloudflare Workers、Vercel Edge），未等待的分叉会在调用 `log.fork()` 时注册到其中，因此运行时会一直存活到子事件完成排空。

**尚不可用：** Hono（没有 `c.get('log')` + ALS 就无法使用 `useLogger`）以及 Nitro/Nuxt `useLogger(event)`，因此请使用发出后的警告来发现错误；之后可能会为事件范围内的分叉提供不同的 API。

对于 AI SDK 流式响应，受支持的框架集成（Next.js、Nitro/Nuxt、SvelteKit、Hono、React Router、oRPC）会将宽事件发出延迟到响应正文完成，因此 `createAILogger(log)` 元数据会自动落到同一个请求事件上。

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

// 在 evlog 中间件后的路由内：
const log = req.log
log.set({ order_dispatched: true })

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

## 逐字段读取宽事件

一个设计良好的宽事件应包含来自多个层级的上下文。以下示例展示了在处理函数或脚本中应添加的内容。假设 `log` 已通过 `createLogger`、`createRequestLogger` 或 `useLogger` 创建。

### 操作上下文

关于操作的基本信息：

<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">

在框架集成中，请求上下文（`method`、`path`、`requestId`）由中间件自动填充。你无需手动设置这些字段。

</callout>

### 用户/执行者上下文

触发操作的用户信息：

```typescript [services/checkout.ts]
log.set({
  userId: user.id,
  email: user.email,
  subscription: user.plan,
  accountAge: daysSince(user.createdAt),
})
```

### 业务上下文

与操作相关的领域特定数据：

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

### 结果

操作的结果：

<code-group>

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

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

</code-group>

## 最佳实践

### 使用有意义的键名

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

// 使用具体、描述性的键名
log.set({ order: { id: 123, status: 'pending' } })
```

### 对相关数据进行分组

```typescript [services/checkout.ts]
// Flat structure is hard to read
log.set({
  userId: 1,
  userEmail: 'a@b.com',
  cartId: 2,
  cartTotal: 100,
})

// 分组结构更清晰
log.set({
  user: { id: 1, email: 'a@b.com' },
  cart: { id: 2, total: 100 },
})
```

### 增量添加上下文

随着信息收集逐步调用 `log.set()`：

<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 [输出]
[INFO] POST /api/checkout (456ms)
  user: { id: 1, plan: 'pro' }
  cart: { items: 3, total: 9999 }
  payment: { method: 'card', status: 'success' }
  status: 200
```

</code-group>

### 优雅地处理错误

当发生错误时，宽事件仍然会携带错误上下文进行发出：

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

### 手动设置级别

`log.error(err)` 会使用 `{ name, message, stack }` 填充 `error` 字段，并将宽事件提升为 `level: 'error'`。当你希望自行控制 `error` 字段时（例如使用类型化错误代码、不包含堆栈或使用更丰富的自定义结构），请使用 `log.setLevel()` 提升级别，而不修改上下文：

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

`setLevel()` 接受 `'error' | 'warn' | 'info' | 'debug'`，并且优先于从 `.error()` / `.warn()` 计算出的级别。将它与 `log.set()` 结合使用，可以在保持宽事件整洁的同时，仍然走错误级别的采样和分发。

## 选择事件的呈现方式

evlog 会根据环境自动在纯文本（美观）和 JSON 格式之间切换。这是默认行为，无需配置。

<code-group>

```bash [开发环境（美观）]
[INFO] POST /api/checkout (234ms)
  user: { id: 1, plan: 'pro' }
  cart: { items: 3, total: 9999 }
  payment: { method: 'card', status: 'success' }
```

```json [生产环境（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>

## 后续步骤

- [简单日志](https://evlog.zhcndoc.com/learn/simple-logging) - 在不需要上下文累积时使用即发即弃日志
- [类型化字段](https://evlog.zhcndoc.com/learn/typed-fields) - 为你的宽事件添加编译时类型安全
- [结构化错误](https://evlog.zhcndoc.com/learn/structured-errors) - 带有可操作上下文的错误
- [框架](https://evlog.zhcndoc.com/integrate/frameworks/overview) - 按框架自动管理请求日志。

---

- [结构化错误](https://evlog.zhcndoc.com/learn/structured-errors)
- [最佳实践](https://evlog.zhcndoc.com/reference/best-practices)


## Sitemap

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