宽事件是 evlog 的核心概念。不是在代码库中分散记录日志,而是累积任意工作单元的上下文(无论是请求、脚本、任务还是工作流),并发出一个全面且集中的日志事件。
将我的请求处理程序转换为宽事件
为什么使用宽事件?
{
requestId: "req_8a2c",
status: 200,
method: "POST",
user: { id: 42, plan: "pro" },
cart: { items: 3, total: 9999 },
charge: { id: "ch_…", amount: 9999 },
order: { id: "ord_889" },
duration: 234
}传统日志会制造噪音:
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' } })
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
一条日志,包含所有上下文。足以理解发生了什么。
创建宽事件
createLogger(通用用途)
在脚本、后端任务、队列工作者、定时任务或任何需要手动管理生命周期的操作中使用 createLogger():
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:
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 和 createRequestLogger 都需要手动调用 log.emit()。事件直到你调用它才会被发出。useLogger(获取请求日志记录器)
在使用框架集成(Nuxt、Hono、Express 等)时,中间件会自动在每个请求上创建一个宽事件日志。useLogger(event) 从请求上下文中获取该日志:
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.emit() 时),该日志记录器实例会被密封。后续的 set、error、info 和 warn 调用不会更新已经发送到接收端的事件。它们会被忽略,并且 evlog 会在控制台打印一条 [evlog] 警告,显示被丢弃的键。这也适用于头采样丢弃事件的情况(emit() 返回 null):该工作单元的日志记录器仍然会被密封。
这对于存活时间超过处理程序的异步工作(即发即弃的 promise、setTimeout、已启动但未等待的任务)很重要。在许多运行时中,AsyncLocalStorage 会继续返回相同的请求日志记录器,因此即使 HTTP 响应——以及宽事件——已经结束,useLogger() 仍然会成功。如果没有警告,这看起来像是静默的数据丢失。
log.fork(label, fn)
对于应该产生其自己宽事件的有意后台工作,当你的集成提供 log.fork(label, fn) 时使用它(Express、Fastify、NestJS、SvelteKit、React Router、Next.js withEvlog、Elysia)。在 fn 内部,useLogger() 会解析为一个子日志记录器。当 fn 完成(或抛出错误)时,子记录器会发出一个包含以下内容的事件:
operation:你传入的label_parentRequestId:父请求的requestId(用于在查询和仪表板中关联)
父宽事件可能在子事件之前发出;它们是按时间顺序排列的两个独立事件。
暂不可用: Hono(没有 c.get('log') + ALS 就没有 useLogger)和 Nitro/Nuxt useLogger(event) —— 使用发出后的警告来捕捉错误;针对事件范围的 fork,后续可能会提供不同的 API。
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 创建。
操作上下文
关于操作的基本信息:
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)由中间件自动填充。你无需手动设置这些字段。用户/执行者上下文
触发操作的用户信息:
log.set({
userId: user.id,
email: user.email,
subscription: user.plan,
accountAge: daysSince(user.createdAt),
})
业务上下文
与操作相关的领域特定数据:
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,
})
log.set({
status: 500,
error: {
message: err.message,
code: err.code,
type: err.constructor.name,
},
})
最佳实践
使用有意义的键名
// 避免使用通用键名
log.set({ data: { id: 123 } })
// 使用具体、描述性的键名
log.set({ order: { id: 123, status: 'pending' } })
对相关数据进行分组
// 平坦结构难以阅读
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 }
})
[INFO] POST /api/checkout (456ms)
user: { id: 1, plan: 'pro' }
cart: { items: 3, total: 9999 }
payment: { method: 'card', status: 'success' }
status: 200
优雅地处理错误
当发生错误时,宽事件仍然会携带错误上下文进行发出:
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
手动设置级别
log.error(err) 会用 { name, message, stack } 填充 error 字段,并将宽事件提升为 level: 'error'。当你想自己控制 error 字段——例如类型化错误码、不要堆栈,或更丰富的自定义结构——请使用 log.setLevel() 来提升级别,而不触碰上下文:
log.setLevel('error')
log.set({
error: {
code: 'PAYMENT_DECLINED',
reason: 'insufficient_funds',
},
})
setLevel() 接受 'error' | 'warn' | 'info' | 'debug',并且优先于从 .error() / .warn() 计算出的级别。将它与 log.set() 结合使用,可以在保持宽事件整洁的同时,仍然走错误级别的采样和分发。
输出格式
evlog 会根据环境自动在纯文本(美观)和 JSON 格式之间切换。这是默认行为,无需配置。
[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" }
}
后续步骤
- Simple Logging - 在不需要上下文累积时使用即发即弃日志
- Typed Fields - 为你的宽事件添加编译时类型安全
- Structured Errors - 带有可操作上下文的错误
- Frameworks - 按框架自动管理请求日志