扩展
definePlugin 是 evlog 的规范扩展点:通过一个统一的对象,可选择接入任意生命周期钩子子集。
request lifecycle
IN PIPELINE
  1. filterroute allowed

    /api/checkout matches include

  2. create loggerrequestId · startTime

    POST · /api/checkout · req_8a2c

  3. handlerlog.set() x3

    context accumulates

  4. tail sampleevlog:emit:keep

    no rule matched

  5. head sampleinfo: 100% kept

    random < rate

  6. emitWideEvent built

    logger sealed · ready to ship

  7. enrichevlog:enrich

    + userAgent · + geo

  8. 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] })

钩子

钩子何时用途
setup(ctx)注册时只执行一次读取 env,建立共享状态
onRequestStart(ctx)每个请求,在任何处理程序运行之前将请求头中的值提取到 logger
enrich(ctx)每个事件,在 drain 之前添加派生字段(地理位置、部署 ID……)
keep(ctx)尾部采样决策基于结果强制保留(status >= 400duration > 500,……)
drain(ctx)每个已发出的事件副作用:告警、镜像到队列等
onRequestFinish(ctx)响应后每请求后的后处理
onClientLog(ctx)浏览器提交的事件命中摄取端点时观察 / 拒绝客户端流量
extendLogger(logger)每个请求添加自定义方法(例如 logger.audit.refund()

每个钩子都是可选的。插件可以实现任意子集。完整类型位于 packages/evlog/src/shared/plugin.ts

一个多钩子示例

当多个职责共享状态时,插件就会大放异彩。这里,单个 request-metrics 插件通过 setuponRequestStartdrain 跟踪每个请求的时序:

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 重新注册时,会替换之前的版本(后注册的版本优先)。

下一步