扩展
definePlugin 是 evlog 的规范扩展点:通过一个统一的对象,可选择接入任意生命周期钩子子集。
request lifecycle
IN PIPELINE·req_8a2c
- 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
context
$POST/api/checkout
log.set(user:{ id: 1, plan: "pro" })
log.set(cart:{ items: 3, total: 9999 })
log.set(payment:{ method: "card", status: "ok" })
wide event
{
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 的规范扩展点。Drain 和 Enricher 是插件的特殊情况,但单个插件可以同时接入多个钩子,这正适合任何混合多种职责的复杂扩展(例如,在每个事件上进行 enrich+在 drain 时执行副作用+在尾部采样时做保留决策,并且所有操作都读取同一份共享状态)。
当扩展只做一件事时,优先使用单一用途的 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!)
钩子
| 钩子 | 何时 | 用途 |
|---|---|---|
setup(ctx) | 注册时只执行一次 | 读取 env,建立共享状态 |
onRequestStart(ctx) | 每个请求,在任何处理程序运行之前 | 将请求头中的值提取到 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 }) {
if (event.durationMs !== undefined) event.tier = event.durationMs > 1000 ? 'slow' : 'fast'
},
drain({ event }) {
if (event.durationMs !== undefined) {
statsd.timing('http.request', event.durationMs, { 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 可用。请参阅类型化字段。- 插件会根据
name去重。使用相同的name重新注册时,会替换之前的版本(后注册的版本优先)。
下一步
- 自定义 Enricher:单钩子 enrich
- 自定义 Drain:单目标输出
- 尾部采样:基于结果的保留决策
- 身份标识请求头:为每个 drain 请求添加标签。