---
title: "Nitro"
description: "One module covers Nitro v3 and v2. Reach the request logger with `useLogger(event)`, and set drain and enrichers from Nitro plugins."
canonical_url: "https://www.evlog.dev/integrate/frameworks/nitro"
---
# Nitro

> One module covers Nitro v3 and v2. Reach the request logger with `useLogger(event)`, and set drain and enrichers from Nitro plugins.

evlog provides modules for both Nitro v3 and Nitro v2 (nitropack). The module hooks into the request lifecycle, creating a request-scoped logger accessible via `useLogger(event)`, and emits a wide event when the response completes.

<prompt :actions="["copy","cursor","claude"]" description="Set up evlog in my Nitro app" icon="i-custom-nitro">

Set up evlog in my Nitro app.

- Install evlog: pnpm add evlog
- Import the evlog module in nitro.config.ts (evlog/nitro for v2, evlog/nitro/v3 for v3)
- Create evlog.config.ts at the app root with export default defineEvlog({ service: 'my-app' }); the module loads it, so call evlog() with no options
- Use useLogger(event) in route handlers to build wide events
- Use log.set() to accumulate context throughout the request
- Throw errors with createError({ message, status, why, fix })
- Wide events are auto-emitted when each request completes

Docs: [https://www.evlog.dev/integrate/frameworks/nitro](https://www.evlog.dev/integrate/frameworks/nitro)
Adapters: [https://www.evlog.dev/integrate/adapters/overview](https://www.evlog.dev/integrate/adapters/overview)

</prompt>

## Quick Start

### 1. Install

<code-group>

```bash [pnpm]
pnpm add evlog
```

```bash [bun]
bun add evlog
```

```bash [yarn]
yarn add evlog
```

```bash [npm]
npm install evlog
```

</code-group>

### 2. Create evlog.config.ts

```typescript [evlog.config.ts]
import { defineEvlog } from 'evlog'

export default defineEvlog({
  service: 'my-app',
})
```

Every option lives in this file: sampling, drain, `enrich`, route filters. `npx evlog init` writes it for you. See [Configuration](https://www.evlog.dev/reference/configuration).

### 3. Add the module

<code-group>

```typescript [nitro.config.ts (v3)]
import { defineConfig } from 'nitro'
import evlog from 'evlog/nitro/v3'

export default defineConfig({
  modules: [
    evlog(),
  ],
})
```

```typescript [nitro.config.ts (v2)]
import { defineNitroConfig } from 'nitropack/config'
import evlog from 'evlog/nitro'

export default defineNitroConfig({
  modules: [
    evlog(),
  ],
})
```

</code-group>

The module finds `evlog.config.ts` and loads it, with nothing to import.

## Wide Events

Build up context progressively throughout a request with `useLogger(event)`. evlog emits a single wide event when the request completes.

<code-group>

```typescript [routes/api/checkout.post.ts (v3)]
import { defineHandler } from 'nitro/h3'
import { useLogger } from 'evlog/nitro/v3'

export default defineHandler(async (event) => {
  const log = useLogger(event)
  const body = await readBody(event)

  log.set({ user: { id: body.userId } })
  log.set({ cart: { items: body.items.length, total: body.total } })

  return { success: true }
})
```

```typescript [routes/api/checkout.post.ts (v2)]
import { defineEventHandler, readBody } from 'h3'
import { useLogger } from 'evlog/nitro'

export default defineEventHandler(async (event) => {
  const log = useLogger(event)
  const body = await readBody(event)

  log.set({ user: { id: body.userId } })
  log.set({ cart: { items: body.items.length, total: body.total } })

  return { success: true }
})
```

</code-group>

One request, one log line with all context:

```bash [Terminal output]
10:23:45 INFO [my-app] POST /api/checkout 200 in 145ms
  ├─ user: id=usr_123
  ├─ cart: items=3 total=14999
  └─ requestId: a1b2c3d4-...
```

Nitro uses **useLogger(event)** (event-bound scope), not `AsyncLocalStorage`, so **log.fork() is not available** here yet. For AI SDK streaming responses, evlog defers wide-event emit until the response body finishes so `createAILogger(log)` metadata stays on the same request event. Post-emit warnings only apply when code calls `set()` after the wide event has actually emitted, for example in non-streaming handlers or background work. See [After emit](https://www.evlog.dev/learn/wide-events#after-emit-sealing-and-background-work) in Wide events.

## Error Handling

`createError` produces structured errors with `why`, `fix`, and `link` fields that help both humans and AI agents understand what went wrong.

<code-group>

```typescript [routes/api/payment.post.ts (v3)]
import { defineHandler } from 'nitro/h3'
import { useLogger, createError } from 'evlog/nitro/v3'

export default defineHandler(async (event) => {
  const log = useLogger(event)

  throw createError({
    status: 402,
    message: 'Payment failed',
    why: 'Card declined by issuer',
    fix: 'Try a different payment method',
  })
})
```

```typescript [routes/api/payment.post.ts (v2)]
import { defineEventHandler } from 'h3'
import { useLogger } from 'evlog/nitro'
import { createError } from 'evlog'

export default defineEventHandler(async (event) => {
  const log = useLogger(event)

  throw createError({
    status: 402,
    message: 'Payment failed',
    why: 'Card declined by issuer',
    fix: 'Try a different payment method',
  })
})
```

</code-group>

<callout color="info" icon="i-lucide-info">

In Nitro v3, import `createError` from `evlog/nitro/v3` - it wraps the Nitro error handler. In Nitro v2, import `createError` from `evlog` directly.

</callout>

## Configuration

Every option goes in `evlog.config.ts`. See the [Configuration reference](https://www.evlog.dev/reference/configuration) for the full list.

Options passed to `evlog()` in `nitro.config.ts` override the file, merged key by key. The file's `drain`, `enrich` and `keep` are never overridden: they run alongside the `evlog:*` hooks, so an event reaches both.

### Route Filtering

Use `include` and `exclude` to control which routes are logged, and `routes` to assign different service names to different route groups:

```typescript [evlog.config.ts]
export default defineEvlog({
  include: ['/api/**'],
  exclude: ['/api/health'],
  routes: {
    '/api/auth/**': { service: 'auth-service' },
    '/api/payment/**': { service: 'payment-service' },
  },
})
```

<callout color="warning" icon="i-lucide-alert-triangle">

**Exclusions take precedence.** If a path matches both `include` and `exclude`, it will be excluded.

</callout>

## Drain & Enrichers

Set `drain` and `enrich` in `evlog.config.ts`:

```typescript [evlog.config.ts]
import { defineEvlog } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'

export default defineEvlog({
  service: 'my-app',
  drain: createAxiomDrain(),
  enrich: (ctx) => {
    ctx.event.region = process.env.FLY_REGION
  },
})
```

A buffered drain from the file, such as one built with `createDrainPipeline`, is flushed when the server closes. Nitro plugins hooking `evlog:drain` and `evlog:enrich`, as below, still work and run alongside the file: both receive every event.

### Drain Plugin

```typescript [server/plugins/evlog-drain.ts]
import type { DrainContext } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'

const pipeline = createDrainPipeline<DrainContext>({
  batch: { size: 50, intervalMs: 5000 },
  retry: { maxAttempts: 3 },
})
const drain = pipeline(createAxiomDrain())

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

<callout color="info" icon="i-lucide-info">

For Nitro v3 standalone, use `definePlugin` from `nitro` instead of `defineNitroPlugin`.

</callout>

### Enricher Plugin

```typescript [server/plugins/evlog-enrich.ts]
import { createUserAgentEnricher, createGeoEnricher } from 'evlog/enrichers'

const enrichers = [createUserAgentEnricher(), createGeoEnricher()]

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:enrich', (ctx) => {
    for (const enricher of enrichers) enricher(ctx)
  })
})
```

<callout color="neutral" icon="i-lucide-arrow-right">

See the [Adapters](https://www.evlog.dev/integrate/adapters/overview) and [Enrichers](https://www.evlog.dev/use-cases/enrichers) docs for the full list of available drains and enrichers.

</callout>

## Sampling

### Head Sampling

Randomly keep a percentage of logs per level. Runs before the request completes.

```typescript [evlog.config.ts]
export default defineEvlog({
  sampling: {
    rates: { info: 10, warn: 50, debug: 5 },
    keep: [
      { duration: 1000 },
      { status: 400 },
    ],
  },
})
```

Each level is a percentage from 0 to 100. Levels you don't configure default to 100% (keep everything).

### Custom Tail Sampling

For conditions beyond status, duration, and path, use the `evlog:emit:keep` hook:

```typescript [server/plugins/evlog-sampling.ts]
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:emit:keep', (ctx) => {
    const user = ctx.context.user as { premium?: boolean } | undefined
    if (user?.premium) ctx.shouldKeep = true
  })
})
```

<callout color="info" icon="i-lucide-info">

Errors are always kept by default. You have to explicitly set `error: 0` to drop them.

</callout>

## Next Steps

Deepen your **Nitro** integration:

- [Wide Events](https://www.evlog.dev/learn/wide-events): Design comprehensive events with context layering
- [Adapters](https://www.evlog.dev/integrate/adapters/overview): Send logs to Axiom, Sentry, PostHog, and more
- [Sampling](https://www.evlog.dev/learn/sampling): Control log volume with head and tail sampling
- [Structured Errors](https://www.evlog.dev/learn/structured-errors): Throw errors with `why`, `fix`, and `link` fields


## Sitemap

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