扩展

自定义 Enricher

编写一个添加部署元数据、租户 ID、功能开关或任何计算值的 enricher。错误隔离和合并由系统处理。
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

每个发出的 event 在到达 drain 之前都会运行 enricher。当你希望每个 event 都包含某个字段,而不想修改每个调用位置时,它就是合适的工具:地理位置、用户代理、trace 上下文、部署 ID、租户 ID、功能开关、性能等级。

evlog/toolkit 使用 defineEnricher,并提供一个返回要合并到 event 中的值的 compute() 函数,toolkit 会处理错误隔离、跳过 undefined 以及合并步骤。每个内置 enricher 都基于同一个工厂构建。

编写一个自定义 evlog enricher

EnrichContext

evlog:enrich hook 接收一个 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 等)会自动过滤,绝不会传递给富化器。

推荐模式: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 组合

自定义和内置 enrichers 可以自由组合,因为它们都只是 (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 调用,无论使用哪种 framework,都以与基本示例相同的方式接入。

功能开关

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 durationMs = event.durationMs
    if (durationMs === undefined) return undefined
    if (durationMs < 100) return 'fast'
    if (durationMs < 500) return 'normal'
    if (durationMs < 2000) return 'slow'
    return 'critical'
  },
})

什么时候应该改用 plugin

如果你的功能将 enrich 与其他 hook 混合在一起(例如 enrich + 尾部采样 + drain 上的副作用),请改用 plugin:使用一个内聚对象覆盖多个生命周期节点。

下一步

  • 内置 Enrichers:User Agent、Geo、Request Size、Trace Context
  • 插件:多 hook 扩展(将 drain + enrich + keep 放在同一个对象中)
  • Adapters:将 enriched events 发送到外部服务