---
title: "Custom Enrichers"
description: "Write an enricher that adds deployment metadata, tenant ids, feature flags, or anything computed. Error isolation and merging are handled."
canonical_url: "https://www.evlog.dev/extend/custom-enrichers"
---
# Custom Enrichers

> Write an enricher that adds deployment metadata, tenant ids, feature flags, or anything computed. Error isolation and merging are handled.

<enricher-chain>



</enricher-chain>

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.

<prompt :actions="["copy","cursor","claude"]" description="Write a custom evlog enricher" icon="i-lucide-code">

Write a custom evlog enricher that adds derived context to every wide event.

- Use `defineEnricher` from `evlog/toolkit`, never write the merge / error / undefined logic by hand
- Pass `{ name, field, compute }` to `defineEnricher`
- `compute(ctx)` reads from `ctx.headers` / `ctx.request` / `ctx.response` / `ctx.event` and returns the value to merge (or `undefined` to skip)
- Keep `compute` pure and fast: no awaitable I/O on the hot path; cache anything expensive at module scope
- `defineEnricher` already handles: error isolation (errors logged, never thrown), single-field merge, overwrite option
- Wire the enricher into my framework via the `enrich` option (middleware) or `initLogger.enrichers` (standalone)
- For multiple enrichers, use `composeEnrichers([...])` from `evlog/toolkit`
- For multi-hook features (enrich + drain side-effect, etc.), use `definePlugin` instead

Docs: [https://www.evlog.dev/extend/custom-enrichers](https://www.evlog.dev/extend/custom-enrichers)
Built-in: [https://www.evlog.dev/use-cases/enrichers](https://www.evlog.dev/use-cases/enrichers)

</prompt>

## Basic example

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

<framework-tabs>

```typescript [Nuxt]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
  })
})
```

```typescript [Nitro]server/plugins/evlog-enrich.ts
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
  })
})
```

```typescript [Next.js]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
  },
})
```

```typescript [SvelteKit]src/hooks.server.ts
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 })
```

```typescript [TanStack Start]server/plugins/evlog-enrich.ts
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
  })
})
```

```typescript [React Router]app/root.tsx
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 }),
]
```

```typescript [Hono]src/index.ts
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 }))
```

```typescript [Express]src/index.ts
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 }))
```

```typescript [Fastify]src/index.ts
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 })
```

```typescript [Elysia]src/index.ts
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 }))
```

```typescript [NestJS]src/app.module.ts
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 })
```

```typescript [oRPC]server/orpc.ts
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 })
```

```typescript [Cloudflare Workers]src/worker.ts
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 })
```

```typescript [Standalone]index.ts
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)] })
```

</framework-tabs>

## EnrichContext

The `evlog:enrich` hook receives an `EnrichContext`:

```typescript [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>
  }
}
```

<callout color="success" icon="i-lucide-shield-check">

**Security:** Sensitive headers (`authorization`, `cookie`, `x-api-key`, etc.) are automatically filtered and never passed to enrichers.

</callout>

## Recommended pattern: `defineEnricher`

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

```typescript [server/utils/enrichers.ts]
import { defineEnricher, type EnricherOptions } from 'evlog/toolkit'

interface TenantInfo {
  id: string
}

// `event.user` is set by the auth layer with log.set({ user }), never by the
// client. A request header would let the caller choose its own tenant.
export function createTenantEnricher(options: EnricherOptions = {}) {
  return defineEnricher<TenantInfo>({
    name: 'tenant',
    field: 'tenant',
    compute: ({ event }) => {
      const user = event.user as { orgId?: string } | undefined
      if (!user?.orgId) return undefined
      return { id: user.orgId }
    },
  }, 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:

<framework-tabs>

```typescript [Nuxt]server/plugins/evlog-enrich.ts
import { createTenantEnricher } from '~/server/utils/enrichers'

export default defineNitroPlugin((nitroApp) => {
  const enrichTenant = createTenantEnricher()
  nitroApp.hooks.hook('evlog:enrich', enrichTenant)
})
```

```typescript [Nitro]server/plugins/evlog-enrich.ts
import { definePlugin } from 'nitro'
import { createTenantEnricher } from '~/server/utils/enrichers'

export default definePlugin((nitroApp) => {
  const enrichTenant = createTenantEnricher()
  nitroApp.hooks.hook('evlog:enrich', enrichTenant)
})
```

```typescript [Next.js]lib/evlog.ts
import { createEvlog } from 'evlog/next'
import { createTenantEnricher } from './enrichers'

const enrichTenant = createTenantEnricher()

export const { withEvlog, useLogger, log, createError } = createEvlog({
  service: 'my-app',
  enrich: enrichTenant,
})
```

```typescript [SvelteKit]src/hooks.server.ts
import { createEvlogHooks } from 'evlog/sveltekit'
import { createTenantEnricher } from './enrichers'

const enrichTenant = createTenantEnricher()

export const { handle, handleError } = createEvlogHooks({ enrich: enrichTenant })
```

```typescript [TanStack Start]server/plugins/evlog-enrich.ts
import { definePlugin } from 'nitro'
import { createTenantEnricher } from '~/server/utils/enrichers'

export default definePlugin((nitroApp) => {
  const enrichTenant = createTenantEnricher()
  nitroApp.hooks.hook('evlog:enrich', enrichTenant)
})
```

```typescript [React Router]app/root.tsx
import { evlog } from 'evlog/react-router'
import { createTenantEnricher } from './enrichers'

const enrichTenant = createTenantEnricher()

export const middleware: Route.MiddlewareFunction[] = [
  evlog({ enrich: enrichTenant }),
]
```

```typescript [Hono]src/index.ts
import { evlog } from 'evlog/hono'
import { createTenantEnricher } from './enrichers'

const enrichTenant = createTenantEnricher()

app.use(evlog({ enrich: enrichTenant }))
```

```typescript [Express]src/index.ts
import { evlog } from 'evlog/express'
import { createTenantEnricher } from './enrichers'

const enrichTenant = createTenantEnricher()

app.use(evlog({ enrich: enrichTenant }))
```

```typescript [Fastify]src/index.ts
import { evlog } from 'evlog/fastify'
import { createTenantEnricher } from './enrichers'

const enrichTenant = createTenantEnricher()

await app.register(evlog, { enrich: enrichTenant })
```

```typescript [Elysia]src/index.ts
import { evlog } from 'evlog/elysia'
import { createTenantEnricher } from './enrichers'

const enrichTenant = createTenantEnricher()

app.use(evlog({ enrich: enrichTenant }))
```

```typescript [NestJS]src/app.module.ts
import { EvlogModule } from 'evlog/nestjs'
import { createTenantEnricher } from './enrichers'

const enrichTenant = createTenantEnricher()

EvlogModule.forRoot({ enrich: enrichTenant })
```

```typescript [oRPC]server/orpc.ts
import { withEvlog } from 'evlog/orpc'
import { createTenantEnricher } from './enrichers'

const enrichTenant = createTenantEnricher()

const handler = withEvlog(new RPCHandler(router), { enrich: enrichTenant })
```

```typescript [Cloudflare Workers]src/worker.ts
import { withEvlog } from 'evlog/workers'
import { createTenantEnricher } from './enrichers'

const enrichTenant = createTenantEnricher()

export default withEvlog(handler, { enrich: enrichTenant })
```

```typescript [Standalone]
import { initLogger } from 'evlog'
import { enricherPlugin } from 'evlog/toolkit'
import { createTenantEnricher } from './enrichers'

initLogger({
  plugins: [enricherPlugin('tenant', createTenantEnricher())],
})
```

</framework-tabs>

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

```typescript [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

```typescript [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

```typescript [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](https://www.evlog.dev/extend/plugins) instead: one cohesive object covering several lifecycle points.

## Next steps

- [Built-in Enrichers](https://www.evlog.dev/use-cases/enrichers): User Agent, Geo, Request Size, Trace Context
- [Plugins](https://www.evlog.dev/extend/plugins): multi-hook extensions (drain + enrich + keep in one object)
- [Adapters](https://www.evlog.dev/integrate/adapters/overview): send enriched events to external services

---

- [Built-in Enrichers](https://www.evlog.dev/use-cases/enrichers)
- [Plugins](https://www.evlog.dev/extend/plugins)


## Sitemap

See the full [sitemap](https://www.evlog.dev/sitemap.md) for all pages.
