Extend

Custom Enrichers

Write an enricher that adds deployment metadata, tenant ids, feature flags, or anything computed. Error isolation and merging are handled.
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

An enricher runs on every emitted event before it is printed and before it reaches drains. It's the right tool when you want a field on every event without touching every call site: geo, user agent, trace context, deploy id, tenant id, feature flags, performance tier.

Use defineEnricher from evlog/toolkit and 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. Every built-in enricher is built on this same factory.

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.tsserver/plugins/evlog-enrich.tslib/evlog.tssrc/hooks.server.tsserver/plugins/evlog-enrich.tsapp/root.tsxsrc/index.tssrc/index.tssrc/index.tssrc/index.tssrc/app.module.tsserver/orpc.tssrc/worker.tsindex.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
  })
})
import { definePlugin } from 'nitro'
export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:enrich', (ctx) => {
    ctx.event.deploymentId = process.env.DEPLOYMENT_ID
    ctx.event.deployedBy = process.env.DEPLOYED_BY
  })
})
import { createEvlog } from 'evlog/next'

export const { withEvlog, useLogger, log, createError } = createEvlog({
  service: 'my-app',
  enrich: (ctx) => {
    ctx.event.deploymentId = process.env.DEPLOYMENT_ID
    ctx.event.deployedBy = process.env.DEPLOYED_BY
  },
})
import { createEvlogHooks } from 'evlog/sveltekit'
import type { EnrichContext } from 'evlog'

const deployment = (ctx: EnrichContext) => {
  ctx.event.deploymentId = process.env.DEPLOYMENT_ID
  ctx.event.deployedBy = process.env.DEPLOYED_BY
}

export const { handle, handleError } = createEvlogHooks({ enrich: deployment })
import { definePlugin } from 'nitro'

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:enrich', (ctx) => {
    ctx.event.deploymentId = process.env.DEPLOYMENT_ID
    ctx.event.deployedBy = process.env.DEPLOYED_BY
  })
})
import { evlog } from 'evlog/react-router'
import type { EnrichContext } from 'evlog'

const deployment = (ctx: EnrichContext) => {
  ctx.event.deploymentId = process.env.DEPLOYMENT_ID
  ctx.event.deployedBy = process.env.DEPLOYED_BY
}

export const middleware: Route.MiddlewareFunction[] = [
  evlog({ enrich: deployment }),
]
import { evlog } from 'evlog/hono'
import type { EnrichContext } from 'evlog'

const deployment = (ctx: EnrichContext) => {
  ctx.event.deploymentId = process.env.DEPLOYMENT_ID
  ctx.event.deployedBy = process.env.DEPLOYED_BY
}

app.use(evlog({ enrich: deployment }))
import { evlog } from 'evlog/express'
import type { EnrichContext } from 'evlog'

const deployment = (ctx: EnrichContext) => {
  ctx.event.deploymentId = process.env.DEPLOYMENT_ID
  ctx.event.deployedBy = process.env.DEPLOYED_BY
}

app.use(evlog({ enrich: deployment }))
import { evlog } from 'evlog/fastify'
import type { EnrichContext } from 'evlog'

const deployment = (ctx: EnrichContext) => {
  ctx.event.deploymentId = process.env.DEPLOYMENT_ID
  ctx.event.deployedBy = process.env.DEPLOYED_BY
}

await app.register(evlog, { enrich: deployment })
import { evlog } from 'evlog/elysia'
import type { EnrichContext } from 'evlog'

const deployment = (ctx: EnrichContext) => {
  ctx.event.deploymentId = process.env.DEPLOYMENT_ID
  ctx.event.deployedBy = process.env.DEPLOYED_BY
}

app.use(evlog({ enrich: deployment }))
import { EvlogModule } from 'evlog/nestjs'
import type { EnrichContext } from 'evlog'

const deployment = (ctx: EnrichContext) => {
  ctx.event.deploymentId = process.env.DEPLOYMENT_ID
  ctx.event.deployedBy = process.env.DEPLOYED_BY
}

EvlogModule.forRoot({ enrich: deployment })
import { withEvlog } from 'evlog/orpc'
import type { EnrichContext } from 'evlog'

const deployment = (ctx: EnrichContext) => {
  ctx.event.deploymentId = process.env.DEPLOYMENT_ID
  ctx.event.deployedBy = process.env.DEPLOYED_BY
}

const handler = withEvlog(new RPCHandler(router), { enrich: deployment })
import { withEvlog } from 'evlog/workers'
import type { EnrichContext } from 'evlog'

const deployment = (ctx: EnrichContext) => {
  ctx.event.deploymentId = process.env.DEPLOYMENT_ID
  ctx.event.deployedBy = process.env.DEPLOYED_BY
}

export default withEvlog(handler, { enrich: deployment })
import type { EnrichContext } from 'evlog'
import { initLogger } from 'evlog'
import { enricherPlugin } from 'evlog/toolkit'

const deployment = (ctx: EnrichContext) => {
  ctx.event.deploymentId = process.env.DEPLOYMENT_ID
  ctx.event.deployedBy = process.env.DEPLOYED_BY
}

initLogger({ plugins: [enricherPlugin('deployment', deployment)] })

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>
  }
}
Security: Sensitive headers (authorization, cookie, x-api-key, etc.) are automatically filtered and never passed to enrichers.

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

Wire it like any other enricher:

server/plugins/evlog-enrich.tsserver/plugins/evlog-enrich.tslib/evlog.tssrc/hooks.server.tsserver/plugins/evlog-enrich.tsapp/root.tsxsrc/index.tssrc/index.tssrc/index.tssrc/index.tssrc/app.module.tsserver/orpc.tssrc/worker.ts
import { createTenantEnricher } from '~/server/utils/enrichers'

export default defineNitroPlugin((nitroApp) => {
  const enrichTenant = createTenantEnricher({ headerName: 'x-org-id' })
  nitroApp.hooks.hook('evlog:enrich', enrichTenant)
})
import { definePlugin } from 'nitro'
import { createTenantEnricher } from '~/server/utils/enrichers'

export default definePlugin((nitroApp) => {
  const enrichTenant = createTenantEnricher({ headerName: 'x-org-id' })
  nitroApp.hooks.hook('evlog:enrich', enrichTenant)
})
import { createEvlog } from 'evlog/next'
import { createTenantEnricher } from './enrichers'

const enrichTenant = createTenantEnricher({ headerName: 'x-org-id' })

export const { withEvlog, useLogger, log, createError } = createEvlog({
  service: 'my-app',
  enrich: enrichTenant,
})
import { createEvlogHooks } from 'evlog/sveltekit'
import { createTenantEnricher } from './enrichers'

const enrichTenant = createTenantEnricher({ headerName: 'x-org-id' })

export const { handle, handleError } = createEvlogHooks({ enrich: enrichTenant })
import { definePlugin } from 'nitro'
import { createTenantEnricher } from '~/server/utils/enrichers'

export default definePlugin((nitroApp) => {
  const enrichTenant = createTenantEnricher({ headerName: 'x-org-id' })
  nitroApp.hooks.hook('evlog:enrich', enrichTenant)
})
import { evlog } from 'evlog/react-router'
import { createTenantEnricher } from './enrichers'

const enrichTenant = createTenantEnricher({ headerName: 'x-org-id' })

export const middleware: Route.MiddlewareFunction[] = [
  evlog({ enrich: enrichTenant }),
]
import { evlog } from 'evlog/hono'
import { createTenantEnricher } from './enrichers'

const enrichTenant = createTenantEnricher({ headerName: 'x-org-id' })

app.use(evlog({ enrich: enrichTenant }))
import { evlog } from 'evlog/express'
import { createTenantEnricher } from './enrichers'

const enrichTenant = createTenantEnricher({ headerName: 'x-org-id' })

app.use(evlog({ enrich: enrichTenant }))
import { evlog } from 'evlog/fastify'
import { createTenantEnricher } from './enrichers'

const enrichTenant = createTenantEnricher({ headerName: 'x-org-id' })

await app.register(evlog, { enrich: enrichTenant })
import { evlog } from 'evlog/elysia'
import { createTenantEnricher } from './enrichers'

const enrichTenant = createTenantEnricher({ headerName: 'x-org-id' })

app.use(evlog({ enrich: enrichTenant }))
import { EvlogModule } from 'evlog/nestjs'
import { createTenantEnricher } from './enrichers'

const enrichTenant = createTenantEnricher({ headerName: 'x-org-id' })

EvlogModule.forRoot({ enrich: enrichTenant })
import { withEvlog } from 'evlog/orpc'
import { createTenantEnricher } from './enrichers'

const enrichTenant = createTenantEnricher({ headerName: 'x-org-id' })

const handler = withEvlog(new RPCHandler(router), { enrich: enrichTenant })
import { withEvlog } from 'evlog/workers'
import { createTenantEnricher } from './enrichers'

const enrichTenant = createTenantEnricher({ headerName: 'x-org-id' })

export default withEvlog(handler, { enrich: enrichTenant })
import { initLogger } from 'evlog'
import { enricherPlugin } from 'evlog/toolkit'
import { createTenantEnricher } from './enrichers'

initLogger({
  plugins: [enricherPlugin('tenant', createTenantEnricher({ headerName: 'x-org-id' }))],
})

Combining with built-in enrichers

Custom and built-in enrichers compose freely, since 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,
])

More examples

Each example below is a plain defineEnricher call, wired 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 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'
  },
})

When to reach for a plugin instead

If your feature mixes enrichment with other hooks (e.g. enrich + tail-sample + side-effect on drain), use a plugin instead: one cohesive object covering several lifecycle points.

Next steps

  • Built-in Enrichers: User Agent, Geo, Request Size, Trace Context
  • Plugins: multi-hook extensions (drain + enrich + keep in one object)
  • Adapters: send enriched events to external services