Custom Enrichers
An enricher runs on every emitted event 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 — 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.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
})
})
// lib/evlog.ts
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 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({ enrichers: [deployment] })) // Hono / Express / Elysia
// await app.register(evlog, { enrichers: [deployment] }) // Fastify
// EvlogModule.forRoot({ enrichers: [deployment] }) // NestJS
// index.ts
import type { EnrichContext } from 'evlog'
import { initLogger } from 'evlog'
const deployment = (ctx: EnrichContext) => {
ctx.event.deploymentId = process.env.DEPLOYMENT_ID
ctx.event.deployedBy = process.env.DEPLOYED_BY
}
initLogger({ enrichers: [deployment] })
EnrichContext
The evlog:enrich hook receives an EnrichContext:
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>
}
}
authorization, cookie, x-api-key, etc.) are automatically filtered and never passed to enrichers.Recommended pattern — defineEnricher
Every built-in enricher uses this same factory. Provide compute() and you're done:
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()returnsundefined - merges the result into
ctx.event[field]viamergeEventField(respectingoptions.overwrite) - catches errors and logs them as
[evlog/<name>]instead of breaking the pipeline
Wire it like any other 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)
})
// lib/evlog.ts
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 { createTenantEnricher } from './enrichers'
const enrichTenant = createTenantEnricher({ headerName: 'x-org-id' })
app.use(evlog({ enrichers: [enrichTenant] }))
// await app.register(evlog, { enrichers: [enrichTenant] }) // Fastify
// EvlogModule.forRoot({ enrichers: [enrichTenant] }) // NestJS
import { initLogger } from 'evlog'
import { createTenantEnricher } from './enrichers'
initLogger({
enrichers: [createTenantEnricher({ headerName: 'x-org-id' })],
})
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:
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 — wire it the same way as the basic example, regardless of framework.
Feature flags
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
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'
},
})
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
Plugins
definePlugin is the canonical extension point for evlog — opt into any subset of setup, onRequestStart, enrich, keep, drain, onRequestFinish, onClientLog, extendLogger from a single cohesive object.
Tail sampling
Decide post-hoc whether to keep an event with full knowledge of its outcome (status, duration, errors). The opposite of head sampling — keep all errors and slow requests while throwing away healthy noise.