Enrichers

Custom Enrichers

Write custom enrichers to add derived context to your wide events. Add deployment metadata, tenant IDs, feature flags, or any computed data.

Write custom enrichers to add any derived context to your wide events. The recommended way is defineEnricher from evlog/toolkit — provide a single compute() function returning the value you want to merge into the event, and the toolkit handles error isolation, undefined skipping, and the merge step.

Write a custom evlog enricher

Basic Example

Add deployment metadata to every event. The enricher is the same function everywhere — only the wiring step differs per framework.

// 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

The evlog:enrich hook receives an EnrichContext:

enrich-context.ts
interface EnrichContext {
  /** The emitted wide event (mutable) */
  event: WideEvent
  /** Request metadata */
  request?: {
    method?: string
    path?: string
    requestId?: string
  }
  /** Safe HTTP request headers (sensitive headers filtered out) */
  headers?: Record<string, string>
  /** Response metadata */
  response?: {
    status?: number
    headers?: Record<string, string>
  }
}

Every built-in enricher uses this same factory. Provide compute() and you're done:

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 automatically:

  • skips when compute() returns undefined
  • merges the result into ctx.event[field] via mergeEventField (respecting options.overwrite)
  • catches errors and logs them as [evlog/<name>] instead of breaking the pipeline
// 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)
})

Combining with Built-in Enrichers

Custom and built-in enrichers compose freely — they're all just (ctx: EnrichContext) => void functions. Use composeEnrichers from evlog/toolkit to combine them into a single callable:

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(), // userAgent + geo + requestSize + traceContext
  region,
])
// Wire `enrich` to your framework — see the Basic Example above for tabs per framework.

More Examples

Each example below is a plain (ctx: EnrichContext) => void function — wire it the same way as the Basic Example, regardless of framework.

Feature Flags

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'),
  }),
})

Response Time Classification

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'
  },
})

Next Steps