日志记录

宽事件(Wide Events)

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

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

为什么要使用宽事件?

传统日志会产生噪音:

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 条以上日志会使排查问题更困难
  • 不完整:如果发生错误,部分日志可能缺失

宽事件解决了这些问题:

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

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

创建宽事件

createLogger(通用用途)

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

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 的轻量包装器,预填充了 methodpathrequestId

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()
createLoggercreateRequestLogger 都需要手动调用 log.emit()。事件直到你调用它才会被发出。

useLogger(获取请求日志记录器)

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

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 }
  // 在响应结束时自动发出
})
useLogger 不会创建日志记录器,它获取的是框架中间件已经附加到事件上的日志。中间件会自动处理创建和发射。在 Nuxt 中,useLogger 是自动导入的。

宽事件的结构

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

操作上下文

关于操作的基本信息:

import { useLogger } from 'evlog'

const log = useLogger(event)
log.set({
  method: 'POST',
  path: '/api/checkout',
  requestId: 'abc-123-def',
})
在框架集成中,请求上下文(methodpathrequestId)由中间件自动填充。你无需手动设置这些字段。

用户/执行者上下文

触发操作的用户信息:

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

业务上下文

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

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

结果

操作的结果:

log.set({
  status: 200,
  duration: Date.now() - startTime,
  success: true,
})

最佳实践

使用有意义的键名

server/api/orders.post.ts
// 避免使用通用键名
log.set({ data: { id: 123 } })

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

对相关数据进行分组

server/api/checkout.post.ts
// 平坦结构难以阅读
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()

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

优雅地处理错误

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

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

输出格式

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

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

后续步骤