宽事件是 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' } })
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():
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:
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()
createLogger 和 createRequestLogger 都需要手动调用 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 已通过 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)由中间件自动填充。你无需手动设置这些字段。用户/执行者上下文
触发操作的用户信息:
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,
})
log.set({
status: 500,
error: {
message: err.message,
code: err.code,
type: err.constructor.name,
},
})
最佳实践
使用有意义的键名
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 }
})
[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
输出格式
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" }
}