evlog 事件遵循从创建到传输的流水线。流水线会根据你使用的日志模式略有不同,但核心阶段(发送、采样、丰富化、传输)是共享的。
按模式概述
| 阶段 | log(简单) | createLogger / createRequestLogger | 框架中间件 |
|---|---|---|---|
| 创建 | 每次调用隐式创建 | createLogger({...}) 或 createRequestLogger({...}) | 请求开始时自动创建 |
| 积累 | 不适用(单次调用) | 多次调用 log.set() | 通过 useLogger(event) 调用 log.set() |
| 发送 | 立即发送 | 手动调用 log.emit() | 响应结束时自动发送 |
| 采样 | 仅头部采样 | 头部 + 尾部采样 | 头部 + 尾部采样 |
| 丰富化 | 通过全局传输钩子 | 通过全局传输钩子 | 通过钩子或回调 |
| 传输 | 通过全局传输钩子 | 通过全局传输钩子 | 通过钩子或回调 |
请求日志流水线
对于框架管理的请求日志,每个请求都会遵循以下流水线。中间件创建日志记录器,useLogger(event) 用于获取它:
请求进入
│
▼
┌──────────┐ 路由被排除?
│ 过滤器 │────── 是 ──▶ 跳过(不记录)
└──────────┘
│ 否
▼
┌──────────────────┐
│ 创建日志记录器 │ requestId、方法、路径、开始时间
└──────────────────┘
│
▼
┌──────────────────┐
│ 处理器运行 │ log.set() 积累上下文
│ │ log.error() 记录错误
└──────────────────┘
│
▼
┌──────────────────┐
│ 请求结束 │ 计算状态码和持续时间
└──────────────────┘
│
▼
┌──────────────────┐
│ 尾部采样 │ evlog:emit:keep 钩子
│ (保留?) │ 基于结果强制保留
└──────────────────┘
│
▼
┌──────────────────┐
│ 头部采样 │ 每个级别的随机抽样
│ (抽样?) │ 如果尾部已说“保留”则跳过
└──────────────────┘
│ 被抽样排除? ──▶ 丢弃(无输出)
│
▼
┌──────────────────┐
│ 发送 │ 构建宽事件 + 控制台输出
└──────────────────┘
│
▼
┌──────────────────┐
│ 丰富化 │ evlog:enrich 钩子
│ │ 用户代理、地理位置、追踪、自定义
└──────────────────┘
│
▼
┌──────────────────┐
│ 传输 │ evlog:drain 钩子
│ │ Axiom、OTLP、Sentry、自定义
└──────────────────┘
│
▼
完成
逐步说明
1. 路由过滤
当请求到达时,evlog 会检查路径是否匹配配置的 include / exclude 模式。如果路由被排除,不会创建日志记录器,请求将继续执行且不产生任何日志开销。
默认情况下记录所有路由。使用 include 限制仅记录特定模式:
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
include: ['/api/**'],
},
})
2. 日志记录器创建
对于匹配的路由,evlog 会创建一个 RequestLogger 并将其附加到请求上下文。日志记录器预先填充了以下字段:
| 字段 | 来源 |
|---|---|
method | HTTP 方法(GET、POST、……) |
path | 请求路径 |
requestId | 自动生成的 UUID(在 Cloudflare 上使用 cf-ray) |
startTime | 用于计算持续时间的 Date.now() |
日志记录器存储在事件上下文上。useLogger(event) 是获取它的快捷方式,不会创建新的日志记录器。
3. 上下文积累
在处理器中,你调用 log.set() 来附加上下文。每次调用都会深度合并到现有上下文中,因此可以根据需要多次调用:
import { useLogger } from 'evlog'
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 } })
如果抛出错误,evlog 的 error 钩子会自动捕获并记录错误及其状态码。
4. 请求结束
当响应发送(或抛出错误)时,evlog 会计算:
- 状态码:来自响应(或错误的
status/statusCode) - 持续时间:从
Date.now() - startTime - 级别:如果记录了错误则为
error,如果状态码 >= 400 则为warn,否则为info
如果错误触发了发送请求,会标记该请求已发送,以防止在响应钩子中重复发送。
5. 尾部采样(evlog:emit:keep)
在事件被采样之前,evlog 会评估尾部采样规则。这些规则在请求完成后运行,因此可以检查结果:
evlog: {
sampling: {
keep: [
{ duration: 1000 }, // 慢请求
{ status: 400 }, // 客户端/服务器错误
{ path: '/api/critical/**' }, // 关键路径
],
},
}
evlog:emit:keep 钩子也会触发,允许你基于自定义业务逻辑强制保留:
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:emit:keep', (ctx) => {
if (ctx.context.user?.premium) {
ctx.shouldKeep = true
}
})
})
如果任何规则或钩子设置了 shouldKeep = true,该事件将完全跳过头部采样。
6. 头部采样
如果事件未被尾部采样强制保留,则应用头部采样。这是基于日志级别的随机投掷硬币。
默认情况下所有级别都以 100% 保留(无采样)。配置 sampling.rates 可在生产环境中减少流量:
evlog: {
sampling: {
rates: { info: 10, warn: 50, debug: 0 },
},
}
info: 10- 保留 10% 的 info 级别事件warn: 50- 保留 50% 的警告error默认始终保留(即使设置了比率也不会被采样排除)
如果事件被采样排除,处理将完全停止:不会输出控制台内容,不会丰富化,也不会传输。
7. 发送
构建 WideEvent 对象,包含积累的上下文:
{
"timestamp": "2026-01-15T10:30:00.000Z",
"level": "info",
"service": "my-app",
"method": "POST",
"path": "/api/checkout",
"requestId": "abc-123",
"duration": 234,
"status": 200,
"user": { "id": 1, "plan": "pro" },
"cart": { "items": 3, "total": 9999 }
}
事件会被打印到控制台,开发环境下格式美观,生产环境下输出为 JSON。这是默认行为,无需配置。
8. 丰富化(evlog:enrich)
发送后,丰富化器会向事件添加派生上下文。内置丰富化器从请求头中提取数据:
| 丰富化器 | 添加内容 | 来源 |
|---|---|---|
| 用户代理 | userAgent(浏览器、操作系统、设备) | User-Agent 头 |
| 地理位置 | geo(国家、地区、城市) | 平台头(Vercel、Cloudflare) |
| 请求大小 | requestSize(请求/响应字节数) | Content-Length 头 |
| 追踪上下文 | traceContext(traceId、spanId) | traceparent 头 |
import { createUserAgentEnricher, createGeoEnricher } from 'evlog/enrichers'
export default defineNitroPlugin((nitroApp) => {
const enrichers = [createUserAgentEnricher(), createGeoEnricher()]
nitroApp.hooks.hook('evlog:enrich', (ctx) => {
for (const enricher of enrichers) enricher(ctx)
})
})
丰富化器接收完整的 EnrichContext,其中包含可变事件、请求元数据、安全头和响应信息。
9. 传输(evlog:drain)
最后一步将丰富化后的事件发送到你的可观测性平台。evlog:drain 钩子接收包含完整事件的 DrainContext:
import { createAxiomDrain } from 'evlog/axiom'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
})
在支持 waitUntil 的平台(Cloudflare Workers、Vercel Edge)上,传输在响应发送后执行以避免增加延迟。在传统服务器上,传输会被等待以防止在服务器无服务器冷启动时丢失事件。
钩子执行顺序
| 顺序 | 钩子 | 时机 | 目的 |
|---|---|---|---|
| 1 | evlog:emit:keep | 请求结束后,采样前 | 基于结果强制保留事件 |
| 2 | evlog:enrich | 发送后,传输前 | 向事件添加派生上下文 |
| 3 | evlog:drain | 丰富化后 | 将事件发送到外部服务 |
错误路径与成功路径
两条路径在相同的发送/丰富化/传输流水线中汇聚。唯一区别在于何时触发发送:
| 成功 | 错误 | |
|---|---|---|
| 触发时机 | afterResponse / response 钩子 | error 钩子 |
| 级别 | info(或状态码 >= 400 时为 warn) | error |
| 状态码 | 来自响应 | 来自错误的 status 字段(默认为 500) |
| 错误上下文 | 无 | error 字段,包含消息、堆栈、why、fix |
| 重复发送保护 | 检查 _evlogEmitted 标志 | 设置 _evlogEmitted = true |
简单日志流水线
使用 log 单例时,流水线更短:
- 调用:
log.info({ action: 'deploy' })或log.info('tag', 'message') - 发送:事件立即构建并输出
- 传输:如果通过
initLogger()配置了全局drain,事件会被发送到外部服务
标记日志(log.info('tag', 'message'))在纯文本模式下仅输出到控制台。对象形式日志(log.info({ ... }))始终通过传输流水线。
独立宽事件流水线
使用 createLogger() 在框架外使用时:
- 创建:
createLogger({ jobId: 'sync-001' }) - 积累:
log.set()、log.info()、log.warn()、log.error()在整个操作过程中 - 发送:手动调用
log.emit() - 采样:头部采样基于计算出的级别应用。尾部采样通过
initLogger({ sampling: { keep: [...] } })配置 - 传输:如果配置了全局
drain,事件会被发送
import { initLogger, createLogger } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
initLogger({
env: { service: 'worker' },
drain: createAxiomDrain(),
sampling: { rates: { info: 10 } },
})
const log = createLogger({ task: 'migrate' })
log.set({ records: 500, status: 'complete' })
log.emit()