扩展

自定义 Enricher

编写自定义 enricher,为你的宽事件添加派生上下文。添加部署元数据、租户 ID、功能开关、地理位置或任何计算得出的数据——工具包会处理错误隔离、跳过 undefined,以及合并步骤。
enrich pipeline·idle
UserAgent
+3 fields
Geo
+3 fields
RequestSize
+2 fields
TraceContext
+2 fields
wide event·4 fields
{
method:"POST",
path:"/api/checkout",
status:200,
duration:234,
userAgent.browser:"chrome 142",+UserAgent
userAgent.os:"macOS 26",+UserAgent
userAgent.device:"desktop",+UserAgent
geo.country:"FR",+Geo
geo.city:"Paris",+Geo
geo.region:"Île-de-France",+Geo
request.size:1248,+RequestSize
response.size:8412,+RequestSize
trace.traceId:"4bf92f3577b34da6a3ce…",+TraceContext
trace.spanId:"00f067aa0ba902b7",+TraceContext
}
base fields4
enriched fields+0
app code touched0 lines

enricher 会在每个发出的事件到达 drain 之前运行。当你希望每个事件都拥有某个字段,而不必修改每个调用点时,这就是合适的工具——例如地理位置、用户代理、trace 上下文、部署 ID、租户 ID、功能开关、性能等级。

使用 evlog/toolkit 中的 defineEnricher——只需提供一个返回你想合并到事件中的值的 compute() 函数,工具包会处理错误隔离、跳过 undefined,以及合并步骤。所有内置 enricher 都是基于这个相同的工厂构建的。

编写一个自定义 evlog enricher

基本示例

为每个事件添加部署元数据。这个 enricher 在任何地方都是同一个函数——只是不同框架的接入步骤不同。

// server/plugins/evlog-enrich.ts
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:enrich', (ctx) => {
    ctx.event.deploymentId = process.env.DEPLOYMENT_ID
    ctx.event.deployedBy = process.env.DEPLOYED_BY
  })
})

EnrichContext

evlog:enrich 钩子接收一个 EnrichContext

enrich-context.ts
interface EnrichContext {
  /** 发出的宽事件(可变) */
  event: WideEvent
  /** 请求元数据 */
  request?: {
    method?: string
    path?: string
    requestId?: string
  }
  /** 安全的 HTTP 请求头(已过滤敏感请求头) */
  headers?: Record<string, string>
  /** 响应元数据 */
  response?: {
    status?: number
    headers?: Record<string, string>
  }
}
安全性: 敏感请求头(authorizationcookiex-api-key 等)会自动过滤,绝不会传递给 enrichers。

推荐模式 — defineEnricher

每个内置 enricher 都使用同一个工厂。提供 compute() 就完成了:

server/utils/enrichers.ts
import { defineEnricher, getHeader, type EnricherOptions } from 'evlog/toolkit'

interface TenantInfo {
  id: string
  org?: string
}

export function createTenantEnricher(options: EnricherOptions & { headerName?: string } = {}) {
  const headerName = options.headerName ?? 'x-tenant-id'

  return defineEnricher<TenantInfo>({
    name: 'tenant',
    field: 'tenant',
    compute: ({ headers }) => {
      const id = getHeader(headers, headerName)
      if (!id) return undefined
      return { id }
    },
  }, options)
}

defineEnricher 会自动:

  • compute() 返回 undefined 时跳过
  • 通过 mergeEventField 将结果合并到 ctx.event[field] 中(遵循 options.overwrite
  • 捕获错误并将其记录为 [evlog/<name>],而不是中断管道

像任何其他 enricher 一样接入它:

// server/plugins/evlog-enrich.ts
import { createTenantEnricher } from '~/server/utils/enrichers'

export default defineNitroPlugin((nitroApp) => {
  const enrichTenant = createTenantEnricher({ headerName: 'x-org-id' })
  nitroApp.hooks.hook('evlog:enrich', enrichTenant)
})

与内置 enrichers 组合

自定义 enricher 和内置 enricher 可以自由组合——它们本质上都只是 (ctx: EnrichContext) => void 函数。使用 evlog/toolkit 中的 composeEnrichers 将它们组合成一个可调用对象:

enrichers.ts
import { composeEnrichers, defineEnricher } from 'evlog/toolkit'
import { createDefaultEnrichers } from 'evlog/enrichers'

const region = defineEnricher({
  name: 'region',
  field: 'region',
  compute: () => process.env.FLY_REGION ?? process.env.AWS_REGION,
})

export const enrich = composeEnrichers([
  createDefaultEnrichers(), // 用户代理 + 地理位置 + 请求大小 + trace 上下文
  region,
])

更多示例

下面的每个示例都是一次普通的 defineEnricher 调用——无论使用什么框架,接入方式都与基本示例相同。

功能开关

enricher-feature-flags.ts
import { defineEnricher } from 'evlog/toolkit'

export const featureFlags = defineEnricher({
  name: 'feature-flags',
  field: 'featureFlags',
  compute: () => ({
    newCheckout: isEnabled('new-checkout'),
    betaApi: isEnabled('beta-api'),
  }),
})

响应时间分级

enricher-perf-tier.ts
import { defineEnricher } from 'evlog/toolkit'

export const performanceTier = defineEnricher<string>({
  name: 'performance-tier',
  field: 'performanceTier',
  compute: ({ event }) => {
    const duration = event.duration as number | undefined
    if (duration === undefined) return undefined
    if (duration < 100) return 'fast'
    if (duration < 500) return 'normal'
    if (duration < 2000) return 'slow'
    return 'critical'
  },
})

什么时候应该改用 plugin

如果你的功能把 enrichment 与其他钩子混合在一起(例如 enrich + tail-sample + 在 drain 上做副作用),请改用 plugin —— 一个统一对象即可覆盖多个生命周期点。

下一步

  • 内置 Enrichers — 用户代理、地理位置、请求大小、trace 上下文
  • Plugins — 多钩子扩展(把 drain + enrich + keep 放在一个对象中)
  • Adapters — 将已丰富的事件发送到外部服务