---
title: "Plugins"
description: "definePlugin is evlog's canonical extension point: opt into any subset of the lifecycle hooks from one cohesive object."
canonical_url: "https://www.evlog.dev/extend/plugins"
---
# Plugins

> definePlugin is evlog's canonical extension point: opt into any subset of the lifecycle hooks from one cohesive object.

<lifecycle-flow>



</lifecycle-flow>

`definePlugin()` is the **canonical extension point** for evlog. Drains and enrichers are special cases of plugins, but a single plugin can opt into multiple hooks at once, the right shape for any non-trivial extension that mixes several concerns (e.g. enrich on every event + side-effect on drain + keep decision on tail sampling, all reading the same shared state).

When the extension only does one thing, prefer the single-purpose [`enricherPlugin()`](https://www.evlog.dev/extend/custom-enrichers) / [`drainPlugin()`](https://www.evlog.dev/extend/custom-drains) wrappers. Reach for `definePlugin` when several hooks share state.

<prompt :actions="["copy","cursor","claude"]" description="Build a multi-hook evlog plugin" icon="i-lucide-blocks">

Build an evlog plugin that hooks into more than one lifecycle stage.

- Import `definePlugin` from `evlog` and pick the hooks I need (`onRequestStart`, `enrich`, `drain`, `extendLogger`, `keep`, `onClientLog`, `onRequestFinish`)
- Keep `enrich` pure (no I/O, no throwing, use try/catch internally)
- For drains, batch and ship to the destination; respect backpressure if the drain is slow
- Register the plugin via the framework config: Next/standalone `initLogger({ plugins: [myPlugin] })`, Hono/Express/Fastify/Elysia middleware option `{ plugins: [myPlugin] }`. For Nitro, register the individual hooks directly (`nitroApp.hooks.hook('evlog:enrich' | 'evlog:drain' | …)`)
- Prefer single-purpose `enricherPlugin()` / `drainPlugin()` wrappers for simple extensions; use `definePlugin` only when several hooks are needed

Docs: [https://www.evlog.dev/extend/plugins](https://www.evlog.dev/extend/plugins)

</prompt>

## Minimal example

```ts
import { definePlugin } from 'evlog'

export const tenantPlugin = definePlugin({
  name: 'tenant',
  onRequestStart({ logger, headers }) {
    const tenantId = headers?.['x-tenant-id']
    if (tenantId) logger.set({ tenant: { id: tenantId } })
  },
  enrich({ event }) {
    event.region = process.env.REGION
  },
})
```

Register the plugin where you bootstrap evlog. The shape depends on your runtime:

<framework-tabs>

```ts [Nuxt]server/plugins/evlog.ts
import { tenantPlugin } from './plugins/tenant'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:enrich', tenantPlugin.enrich!)
})
```

```ts [Nitro]server/plugins/evlog.ts
import { definePlugin } from 'nitro'
import { tenantPlugin } from './plugins/tenant'

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:enrich', tenantPlugin.enrich!)
})
```

```ts [Next.js]lib/evlog.ts
import { createEvlog } from 'evlog/next'
import { tenantPlugin } from './plugins/tenant'

export const { withEvlog, useLogger, log, createError } = createEvlog({
  service: 'my-app',
  plugins: [tenantPlugin],
})
```

```ts [SvelteKit]src/hooks.server.ts
import { createEvlogHooks } from 'evlog/sveltekit'
import { tenantPlugin } from './plugins/tenant'

export const { handle, handleError } = createEvlogHooks({ plugins: [tenantPlugin] })
```

```ts [TanStack Start]server/plugins/evlog.ts
import { definePlugin } from 'nitro'
import { tenantPlugin } from './plugins/tenant'

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:enrich', tenantPlugin.enrich!)
})
```

```ts [React Router]app/root.tsx
import { evlog } from 'evlog/react-router'
import { tenantPlugin } from './plugins/tenant'

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

```ts [Hono]src/index.ts
import { evlog } from 'evlog/hono'
import { tenantPlugin } from './plugins/tenant'

app.use(evlog({ plugins: [tenantPlugin] }))
```

```ts [Express]src/index.ts
import { evlog } from 'evlog/express'
import { tenantPlugin } from './plugins/tenant'

app.use(evlog({ plugins: [tenantPlugin] }))
```

```ts [Fastify]src/index.ts
import { evlog } from 'evlog/fastify'
import { tenantPlugin } from './plugins/tenant'

await app.register(evlog, { plugins: [tenantPlugin] })
```

```ts [Elysia]src/index.ts
import { evlog } from 'evlog/elysia'
import { tenantPlugin } from './plugins/tenant'

app.use(evlog({ plugins: [tenantPlugin] }))
```

```ts [NestJS]src/app.module.ts
import { EvlogModule } from 'evlog/nestjs'
import { tenantPlugin } from './plugins/tenant'

EvlogModule.forRoot({ plugins: [tenantPlugin] })
```

```ts [oRPC]server/orpc.ts
import { withEvlog } from 'evlog/orpc'
import { tenantPlugin } from './plugins/tenant'

const handler = withEvlog(new RPCHandler(router), { plugins: [tenantPlugin] })
```

```ts [Cloudflare Workers]src/worker.ts
import { withEvlog } from 'evlog/workers'
import { tenantPlugin } from './plugins/tenant'

export default withEvlog(handler, { plugins: [tenantPlugin] })
```

```ts [Standalone]src/index.ts
import { initLogger } from 'evlog'
import { tenantPlugin } from './plugins/tenant'

initLogger({ plugins: [tenantPlugin] })
```

</framework-tabs>

## Hooks

<table>
<thead>
  <tr>
    <th>
      Hook
    </th>
    
    <th>
      When
    </th>
    
    <th>
      Use it for
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        setup(ctx)
      </code>
    </td>
    
    <td>
      Once when registered
    </td>
    
    <td>
      Read <code>
        env
      </code>
      
      , set up shared state
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        onRequestStart(ctx)
      </code>
    </td>
    
    <td>
      Each request, before any handler runs
    </td>
    
    <td>
      Pull values from headers into <code>
        logger
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        enrich(ctx)
      </code>
    </td>
    
    <td>
      Every event, before drain
    </td>
    
    <td>
      Add derived fields (geo, deploy id…)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        keep(ctx)
      </code>
    </td>
    
    <td>
      Tail sampling decision
    </td>
    
    <td>
      Force-keep based on outcome (<code>
        status >= 400
      </code>
      
      , <code>
        duration > 500
      </code>
      
      , …)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        drain(ctx)
      </code>
    </td>
    
    <td>
      Every emitted event
    </td>
    
    <td>
      Side-effect: alert, mirror to a queue, etc.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        onRequestFinish(ctx)
      </code>
    </td>
    
    <td>
      After response
    </td>
    
    <td>
      Per-request post-processing
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        onClientLog(ctx)
      </code>
    </td>
    
    <td>
      Browser-submitted event hits the ingest endpoint
    </td>
    
    <td>
      Observe / reject client traffic
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        extendLogger(logger)
      </code>
    </td>
    
    <td>
      Each request
    </td>
    
    <td>
      Add custom methods (e.g. <code>
        logger.audit.refund()
      </code>
      
      )
    </td>
  </tr>
</tbody>
</table>

Every hook is **optional**. A plugin can implement any subset. The full type lives in [`packages/evlog/src/shared/plugin.ts`](https://github.com/evloghq/evlog/blob/main/packages/evlog/src/shared/plugin.ts).

## A multi-hook example

Plugins shine when several concerns share state. Here, a single `request-metrics` plugin tracks per-request timing through `setup`, `onRequestStart`, and `drain`:

```ts
import { definePlugin } from 'evlog/toolkit'

export const requestMetricsPlugin = definePlugin({
  name: 'request-metrics',

  setup({ env }) {
    statsd.init({ service: env.service })
  },

  enrich({ event }) {
    if (event.durationMs !== undefined) event.tier = event.durationMs > 1000 ? 'slow' : 'fast'
  },

  drain({ event }) {
    if (event.durationMs !== undefined) {
      statsd.timing('http.request', event.durationMs, { path: event.path as string })
    }
  },

  onRequestStart({ logger, request }) {
    logger.set({ trace: { startedAt: Date.now() } })
  },

  onRequestFinish({ event, durationMs }) {
    if (event && (event.level === 'error' || durationMs > 5000)) {
      // alert / forward / etc.
    }
  },
})
```

## Sugar plugins

For single-hook extensions, the toolkit offers `drainPlugin()` and `enricherPlugin()` wrappers:

```typescript
import { drainPlugin, enricherPlugin } from 'evlog/toolkit'

const drainOnly = drainPlugin('axiom', createAxiomDrain())
const enricherOnly = enricherPlugin('user-agent', createUserAgentEnricher())
```

These are equivalent to a `definePlugin({ name, drain | enrich })` shape but read more clearly when intent is obvious.

## Common pitfalls

- **Don't throw from a hook.** The plugin runner catches and logs errors with the plugin name, but a thrown error from `enrich` won't propagate the event downstream. Keep hooks defensive.
- **drain runs for every event**: not just per-request. If you only care about per-request lifecycle, use `onRequestFinish` instead.
- **extendLogger mutates the logger object**: augment `RequestLogger` in a `.d.ts` so `useLogger(event)` exposes the new methods to TypeScript. See [typed fields](https://www.evlog.dev/learn/typed-fields).
- **Plugins are de-duplicated by name**. Re-registering with the same `name` replaces the previous version (last registration wins).

## Next steps

- [Custom Enrichers](https://www.evlog.dev/extend/custom-enrichers): single-hook enrichment
- [Custom Drains](https://www.evlog.dev/extend/custom-drains): single-destination output
- [Tail Sampling](https://www.evlog.dev/extend/tail-sampling): outcome-aware keep decisions
- [Identity Headers](https://www.evlog.dev/extend/identity-headers): tag every drain request

---

- [Custom Enrichers](https://www.evlog.dev/extend/custom-enrichers)
- [Custom Drains](https://www.evlog.dev/extend/custom-drains)
- [Tail Sampling](https://www.evlog.dev/extend/tail-sampling)


## Sitemap

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