- filterroute allowed
/api/checkout matches include
- create loggerrequestId · startTime
POST · /api/checkout · req_8a2c
- handlerlog.set() x3
context accumulates
- tail sampleevlog:emit:keep
no rule matched
- head sampleinfo: 100% kept
random < rate
- emitWideEvent built
logger sealed · ready to ship
- enrichevlog:enrich
+ userAgent · + geo
- drainevlog:drain
→ axiom · → fs
{
level: "info",
method: "POST",
path: "/api/checkout",
duration: 234,
status: 200,
user: { id: 1, plan: "pro" },
cart: { items: 3, total: 9999 },
payment: { method: "card", status: "ok" },
userAgent: { browser: "chrome" },
geo: { country: "FR" }
}definePlugin() 是 evlog 的规范扩展点。Drains 和 enrichers 是插件的特殊情况,但单个插件可以同时选择接入多个钩子——这对于任何需要混合多个职责的非平凡扩展来说,都是合适的形态(例如:对每个事件进行 enrich + 在 drain 时产生副作用 + 在尾部采样时做 keep 决策,并且都读取同一份共享状态)。
当扩展只做一件事时,优先使用单一用途的 enricherPlugin() / drainPlugin() 包装器。只有当多个钩子共享状态时,才使用 definePlugin。
构建一个多钩子的 evlog 插件
最小示例
import { definePlugin } from 'evlog'
export const tenantPlugin = definePlugin({
name: 'tenant',
onRequestStart({ logger, headers }) {
const tenantId = headers?.['x-tenant-id']
if (tenantId) logger.set({ tenant: { id: tenantId } })
},
enrich({ event }) {
event.region = process.env.REGION
},
})
在你引导 evlog 的地方注册该插件。具体形态取决于你的运行时:
import { initLogger } from 'evlog'
import { tenantPlugin } from './plugins/tenant'
initLogger({ plugins: [tenantPlugin] })
import { evlogMiddleware } from 'evlog/<framework>'
import { tenantPlugin } from './plugins/tenant'
app.use(evlogMiddleware({ plugins: [tenantPlugin] }))
// 直接注册你实际使用的钩子:
nitroApp.hooks.hook('evlog:enrich', tenantPlugin.enrich!)
nitroApp.hooks.hook('evlog:request:start', tenantPlugin.onRequestStart!)
钩子
| Hook | 何时 | 用途 |
|---|---|---|
setup(ctx) | 注册时只执行一次 | 读取 env,建立共享状态 |
onRequestStart(ctx) | 每个请求,在任何处理程序运行之前 | 将 header 中的值提取到 logger 中 |
enrich(ctx) | 每个事件,在 drain 之前 | 添加派生字段(地理位置、部署 ID……) |
keep(ctx) | 尾部采样决策 | 基于结果强制保留(status >= 400、duration > 500,……) |
drain(ctx) | 每个已发出的事件 | 副作用:告警、镜像到队列等 |
onRequestFinish(ctx) | 响应后 | 每请求后的后处理 |
onClientLog(ctx) | 浏览器提交的事件命中摄取端点时 | 观察 / 拒绝客户端流量 |
extendLogger(logger) | 每个请求 | 添加自定义方法(例如 logger.audit.refund()) |
每个钩子都是可选的。一个插件可以实现任意子集。完整类型位于 packages/evlog/src/shared/plugin.ts。
一个多钩子示例
当多个职责共享状态时,插件就会大放异彩。这里,单个 request-metrics 插件通过 setup、onRequestStart 和 drain 跟踪每个请求的时序:
import { definePlugin } from 'evlog/toolkit'
export const requestMetricsPlugin = definePlugin({
name: 'request-metrics',
setup({ env }) {
statsd.init({ service: env.service })
},
enrich({ event }) {
event.tier = event.duration && event.duration > 1000 ? 'slow' : 'fast'
},
drain({ event }) {
statsd.timing('http.request', event.duration as number, { path: event.path as string })
},
onRequestStart({ logger, request }) {
logger.set({ trace: { startedAt: Date.now() } })
},
onRequestFinish({ event, durationMs }) {
if (event && (event.level === 'error' || durationMs > 5000)) {
// 告警 / 转发 / 等等。
}
},
})
便捷插件
对于单钩子扩展,工具包提供了 drainPlugin() 和 enricherPlugin() 包装器:
import { drainPlugin, enricherPlugin } from 'evlog/toolkit'
const drainOnly = drainPlugin('axiom', createAxiomDrain())
const enricherOnly = enricherPlugin('user-agent', createUserAgentEnricher())
当意图很明显时,这些写法等价于 definePlugin({ name, drain | enrich }) 的形式,但可读性更强。
常见陷阱
- 不要在钩子里抛出错误。 插件运行器会捕获错误并记录插件名称,但从
enrich抛出的错误不会将事件向下游传递。请让钩子具备防御性。 drain会对每个事件运行——而不只是每个请求。如果你只关心每请求生命周期,请改用onRequestFinish。extendLogger会修改 logger 对象——请在.d.ts中为RequestLogger做增强,这样useLogger(event)就能在 TypeScript 中暴露新方法。参见 typed fields。- 插件会按
name去重。使用相同的name重新注册会替换之前的版本(最后一次注册生效)。
下一步
- 自定义 Enricher — 单钩子 enrich
- 自定义 Drain — 单目标输出
- 尾部采样 — 基于结果的 keep 决策
- 身份头 — 为每个 drain 请求添加标签