---
title: "Lifecycle"
description: "An evlog event from creation to drain, across all three modes, through sampling, enrichment, and delivery."
canonical_url: "https://www.evlog.dev/learn/lifecycle"
---
# Lifecycle

> An evlog event from creation to drain, across all three modes, through sampling, enrichment, and delivery.

evlog events follow a pipeline from creation to delivery. The pipeline differs slightly depending on which logging mode you use, but the core stages (emit, sample, enrich, drain) are shared.

<lifecycle-flow>



</lifecycle-flow>

## Follow an event through each mode

<table>
<thead>
  <tr>
    <th>
      Stage
    </th>
    
    <th>
      <code>
        log
      </code>
      
       (simple)
    </th>
    
    <th>
      <code>
        createLogger
      </code>
      
       / <code>
        createRequestLogger
      </code>
    </th>
    
    <th>
      Framework middleware
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Create
      </strong>
    </td>
    
    <td>
      Implicit per call
    </td>
    
    <td>
      <code>
        createLogger({...})
      </code>
      
       or <code>
        createRequestLogger({...})
      </code>
    </td>
    
    <td>
      Auto on request start
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Accumulate
      </strong>
    </td>
    
    <td>
      N/A (single call)
    </td>
    
    <td>
      <code>
        log.set()
      </code>
      
       multiple times
    </td>
    
    <td>
      <code>
        log.set()
      </code>
      
       via <code>
        useLogger(event)
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Emit
      </strong>
    </td>
    
    <td>
      Immediate
    </td>
    
    <td>
      Manual <code>
        log.emit()
      </code>
    </td>
    
    <td>
      Auto on response end
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Sample
      </strong>
    </td>
    
    <td>
      Head sampling only
    </td>
    
    <td>
      Head + tail sampling
    </td>
    
    <td>
      Head + tail sampling
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Enrich
      </strong>
    </td>
    
    <td>
      Via global drain
    </td>
    
    <td>
      Via global drain
    </td>
    
    <td>
      Via hooks or callbacks
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Drain
      </strong>
    </td>
    
    <td>
      Via global drain
    </td>
    
    <td>
      Via global drain
    </td>
    
    <td>
      Via hooks or callbacks
    </td>
  </tr>
</tbody>
</table>

After **emit** (including when sampling returns no output), the request logger is **sealed**: later `set` / `error` / `info` / `warn` calls are ignored with a console warning. For background work that needs its own event, use **log.fork()** where your integration supports it. See [After emit](https://www.evlog.dev/learn/wide-events#after-emit-sealing-and-background-work) in Wide events.

## Trace a request, step by step

For framework-managed request logging, every request walks the pipeline above. Each stage is detailed below.

### 1. Route Filtering

When a request arrives, evlog checks whether the path matches the configured `include` / `exclude` patterns. If the route is excluded, no logger is created and the request proceeds without any logging overhead.

By default, all routes are logged. Use `include` to restrict logging to specific patterns:

<framework-tabs>

```typescript [Nuxt]nuxt.config.ts
export default defineNuxtConfig({
  modules: ['evlog/nuxt'],
  evlog: {
    include: ['/api/**'],
  },
})
```

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

export default defineConfig({
  modules: [evlog({ include: ['/api/**'] })],
})
```

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

export const { withEvlog, useLogger, log, createError } = createEvlog({
  service: 'my-app',
  include: ['/api/**'],
})
```

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

export const { handle, handleError } = createEvlogHooks({ include: ['/api/**'] })
```

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

export default defineConfig({
  modules: [evlog({ include: ['/api/**'] })],
})
```

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

export const middleware: Route.MiddlewareFunction[] = [
  evlog({ include: ['/api/**'] }),
]
```

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

app.use(evlog({ include: ['/api/**'] }))
```

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

app.use(evlog({ include: ['/api/**'] }))
```

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

await app.register(evlog, { include: ['/api/**'] })
```

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

app.use(evlog({ include: ['/api/**'] }))
```

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

EvlogModule.forRoot({ include: ['/api/**'] })
```

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

const handler = withEvlog(new RPCHandler(router), { include: ['/api/**'] })
```

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

export default withEvlog(handler, { include: ['/api/**'] })
```

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

initLogger({ include: ['/api/**'] })
```

</framework-tabs>

### 2. Logger Creation

For matched routes, evlog creates a `RequestLogger` and attaches it to the request context. The logger is pre-populated with:

<table>
<thead>
  <tr>
    <th>
      Field
    </th>
    
    <th>
      Source
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        method
      </code>
    </td>
    
    <td>
      HTTP method (<code>
        GET
      </code>
      
      , <code>
        POST
      </code>
      
      , ...)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        path
      </code>
    </td>
    
    <td>
      Request path
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        requestId
      </code>
    </td>
    
    <td>
      Auto-generated UUID (or <code>
        cf-ray
      </code>
      
       on Cloudflare)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        startTime
      </code>
    </td>
    
    <td>
      <code>
        Date.now()
      </code>
      
       for duration calculation
    </td>
  </tr>
</tbody>
</table>

The logger is stored on the event context. `useLogger(event)` is a shortcut to retrieve it, it doesn't create a new logger.

### 3. Context Accumulation

During the handler, you call `log.set()` to attach context. Each call deep-merges into the existing context, so you can call it as many times as needed:

```typescript [server/api/checkout.post.ts]
import { useLogger } from 'evlog'

const log = useLogger(event)

const user = await getUser(event)
log.set({ user: { id: user.id, plan: user.plan } })

const cart = await getCart(user.id)
log.set({ cart: { items: cart.items.length, total: cart.total } })
```

If an error is thrown, evlog's `error` hook captures it automatically and records it on the logger with the status code.

### 4. Request End

When the response is sent (or an error is thrown), evlog computes:

- **Status code** from the response (or from the error's `status` / `statusCode`)
- **Duration** from `Date.now() - startTime`
- **Level** - `error` if an error was recorded, `warn` if status >= 400, otherwise `info`

If an error triggered the emit, the request is marked as already emitted to prevent double-emission in the response hook.

### 5. Tail Sampling (`evlog:emit:keep`)

Before the event is sampled, evlog evaluates **tail sampling** rules. These run *after* the request completes, so they can inspect the outcome:

```typescript [nuxt.config.ts]
evlog: {
  sampling: {
    keep: [
      { duration: 1000 },          // slow requests
      { status: 400 },             // client/server errors
      { path: '/api/critical/**' }, // critical paths
    ],
  },
}
```

The `evlog:emit:keep` hook also fires, letting you force-keep based on custom business logic:

<framework-tabs>

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

```typescript [Nitro]server/plugins/evlog.ts
import { definePlugin } from 'nitro'
export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:emit:keep', (ctx) => {
    if (ctx.context.user?.premium) {
      ctx.shouldKeep = true
    }
  })
})
```

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

export const { withEvlog, useLogger, log, createError } = createEvlog({
  service: 'my-app',
  keep: (ctx) => {
      if (ctx.context.user?.premium) {
        ctx.shouldKeep = true
      }
    },
})
```

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

export const { handle, handleError } = createEvlogHooks({
  keep: (ctx) => {
      if (ctx.context.user?.premium) {
        ctx.shouldKeep = true
      }
    },
})
```

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

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook('evlog:emit:keep', (ctx) => {
    if (ctx.context.user?.premium) {
      ctx.shouldKeep = true
    }
  })
})
```

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

export const middleware: Route.MiddlewareFunction[] = [
  evlog({
      keep: (ctx) => {
          if (ctx.context.user?.premium) {
            ctx.shouldKeep = true
          }
        },
    }),
]
```

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

app.use(evlog({
  keep: (ctx) => {
      if (ctx.context.user?.premium) {
        ctx.shouldKeep = true
      }
    },
}))
```

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

app.use(evlog({
  keep: (ctx) => {
      if (ctx.context.user?.premium) {
        ctx.shouldKeep = true
      }
    },
}))
```

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

await app.register(evlog, {
  keep: (ctx) => {
      if (ctx.context.user?.premium) {
        ctx.shouldKeep = true
      }
    },
})
```

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

app.use(evlog({
  keep: (ctx) => {
      if (ctx.context.user?.premium) {
        ctx.shouldKeep = true
      }
    },
}))
```

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

EvlogModule.forRoot({
  keep: (ctx) => {
      if (ctx.context.user?.premium) {
        ctx.shouldKeep = true
      }
    },
})
```

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

const handler = withEvlog(new RPCHandler(router), {
  keep: (ctx) => {
      if (ctx.context.user?.premium) {
        ctx.shouldKeep = true
      }
    },
})
```

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

export default withEvlog(handler, {
  keep: (ctx) => {
      if (ctx.context.user?.premium) {
        ctx.shouldKeep = true
      }
    },
})
```

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

initLogger({
  keep: (ctx) => {
      if (ctx.context.user?.premium) {
        ctx.shouldKeep = true
      }
    },
})
```

</framework-tabs>

If any rule or hook sets `shouldKeep = true`, the event **bypasses head sampling entirely**.

### 6. Head Sampling

If the event wasn't force-kept by tail sampling, head sampling applies. This is a random coin flip per log level.

By default, all levels are kept at 100% (no sampling). Configure `sampling.rates` to reduce volume in production:

```typescript [nuxt.config.ts]
evlog: {
  sampling: {
    rates: { info: 10, warn: 50, debug: 0 },
  },
}
```

- `info: 10` - keep 10% of info-level events
- `warn: 50` - keep 50% of warnings
- `error` defaults to **100%** (never sampled out, even if you set a rate)

If the event is sampled out, processing stops entirely: no console output, no enrichment, no drain.

### 7. Emit

The `WideEvent` object is built from the accumulated context:

```json [WideEvent]
{
  "timestamp": "2026-01-15T10:30:00.000Z",
  "level": "info",
  "service": "my-app",
  "method": "POST",
  "path": "/api/checkout",
  "requestId": "abc-123",
  "duration": 234,
  "status": 200,
  "user": { "id": 1, "plan": "pro" },
  "cart": { "items": 3, "total": 9999 }
}
```

The logger is sealed at this point. Nothing is printed yet: the console line waits for enrichers, so the terminal shows the same event drains receive.

### 8. Enrich (`evlog:enrich`)

After emission, enrichers add derived context to the event. Built-in enrichers extract data from request headers:

<table>
<thead>
  <tr>
    <th>
      Enricher
    </th>
    
    <th>
      Adds
    </th>
    
    <th>
      Source
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      User Agent
    </td>
    
    <td>
      <code>
        userAgent
      </code>
      
       (browser, OS, device)
    </td>
    
    <td>
      <code>
        User-Agent
      </code>
      
       header
    </td>
  </tr>
  
  <tr>
    <td>
      Geo
    </td>
    
    <td>
      <code>
        geo
      </code>
      
       (country, region, city)
    </td>
    
    <td>
      Platform headers (Vercel, Cloudflare)
    </td>
  </tr>
  
  <tr>
    <td>
      Request Size
    </td>
    
    <td>
      <code>
        requestSize
      </code>
      
       (request/response bytes)
    </td>
    
    <td>
      <code>
        Content-Length
      </code>
      
       headers
    </td>
  </tr>
  
  <tr>
    <td>
      Trace Context
    </td>
    
    <td>
      <code>
        traceContext
      </code>
      
       (traceId, parentSpanId)
    </td>
    
    <td>
      <code>
        traceparent
      </code>
      
       header
    </td>
  </tr>
</tbody>
</table>

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

export default defineNitroPlugin((nitroApp) => {
  const enrichers = [createUserAgentEnricher(), createGeoEnricher()]

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

Enrichers receive the full `EnrichContext` with the mutable event, request metadata, safe headers, and response info.

Once they return, the event is printed to the console, pretty-formatted in development and as JSON in production. No configuration needed. The fields enrichers added (`userAgent`, `geo`, signal verdicts) are on that line, and on any platform that reads stdout.

### 9. Drain (`evlog:drain`)

The final step sends the enriched event to your observability platform. The `evlog:drain` hook receives a `DrainContext` with the complete event:

<framework-tabs>

```typescript [Nuxt]server/plugins/evlog.ts
import { createAxiomDrain } from 'evlog/axiom'

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

```typescript [Nitro]server/plugins/evlog.ts
import { definePlugin } from 'nitro'
import { createAxiomDrain } from 'evlog/axiom'

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

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

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

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

export const { handle, handleError } = createEvlogHooks({ drain: createAxiomDrain() })
```

```typescript [TanStack Start]server/plugins/evlog.ts
import { definePlugin } from 'nitro'
import { createAxiomDrain } from 'evlog/axiom'

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

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

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

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

app.use(evlog({ drain: createAxiomDrain() }))
```

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

app.use(evlog({ drain: createAxiomDrain() }))
```

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

await app.register(evlog, { drain: createAxiomDrain() })
```

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

app.use(evlog({ drain: createAxiomDrain() }))
```

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

EvlogModule.forRoot({ drain: createAxiomDrain() })
```

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

const handler = withEvlog(new RPCHandler(router), { drain: createAxiomDrain() })
```

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

export default withEvlog(handler, { drain: createAxiomDrain() })
```

```typescript [Standalone]src/index.ts
import { initLogger } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'

initLogger({ drain: createAxiomDrain() })
```

</framework-tabs>

On platforms with `waitUntil` (Cloudflare Workers, Vercel Edge), the drain runs after the response is sent to avoid adding latency. On traditional servers, the drain is awaited to prevent losing events in serverless cold shutdowns.

## Know when each hook runs

<table>
<thead>
  <tr>
    <th>
      Order
    </th>
    
    <th>
      Hook
    </th>
    
    <th>
      When
    </th>
    
    <th>
      Purpose
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      1
    </td>
    
    <td>
      <code>
        evlog:emit:keep
      </code>
    </td>
    
    <td>
      After request ends, before sampling
    </td>
    
    <td>
      Force-keep events based on outcome
    </td>
  </tr>
  
  <tr>
    <td>
      2
    </td>
    
    <td>
      <code>
        evlog:enrich
      </code>
    </td>
    
    <td>
      After emit, before console output and drain
    </td>
    
    <td>
      Add derived context to the event
    </td>
  </tr>
  
  <tr>
    <td>
      3
    </td>
    
    <td>
      <code>
        evlog:drain
      </code>
    </td>
    
    <td>
      After enrichment
    </td>
    
    <td>
      Send event to external services
    </td>
  </tr>
</tbody>
</table>

## See where the error path diverges

Both paths converge at the same emit/enrich/drain pipeline. The only difference is *when* the emit is triggered:

<table>
<thead>
  <tr>
    <th>
      
    </th>
    
    <th>
      Success
    </th>
    
    <th>
      Error
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Trigger
      </strong>
    </td>
    
    <td>
      <code>
        afterResponse
      </code>
      
       / <code>
        response
      </code>
      
       hook
    </td>
    
    <td>
      <code>
        error
      </code>
      
       hook
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Level
      </strong>
    </td>
    
    <td>
      <code>
        info
      </code>
      
       (or <code>
        warn
      </code>
      
       if status >= 400)
    </td>
    
    <td>
      <code>
        error
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Status
      </strong>
    </td>
    
    <td>
      From response
    </td>
    
    <td>
      From error's <code>
        status
      </code>
      
       field (default 500)
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Error context
      </strong>
    </td>
    
    <td>
      None
    </td>
    
    <td>
      <code>
        error
      </code>
      
       field with message, stack, <code>
        why
      </code>
      
      , <code>
        fix
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Double-emit guard
      </strong>
    </td>
    
    <td>
      Checks <code>
        _evlogEmitted
      </code>
      
       flag
    </td>
    
    <td>
      Sets <code>
        _evlogEmitted = true
      </code>
    </td>
  </tr>
</tbody>
</table>

## Follow a simple log call

When using the `log` singleton, the pipeline is shorter:

1. **Call**: `log.info({ action: 'deploy' })` or `log.info('tag', 'message')`
2. **Emit**: The event is built and printed immediately
3. **Drain**: If a global `drain` was configured via `initLogger()`, the event is sent to external services

Tagged logs (`log.info('tag', 'message')`) are console-only in pretty mode. Object-form logs (`log.info({ ... })`) always flow through the drain pipeline.

## Follow a standalone wide event

When using `createLogger()` outside a framework:

1. **Create**: `createLogger({ jobId: 'sync-001' })`
2. **Accumulate**: `log.set()`, `log.info()`, `log.warn()`, `log.error()` over the operation
3. **Emit**: Manual `log.emit()` call
4. **Sample**: Head sampling applies based on computed level. Tail sampling via `initLogger({ sampling: { keep: [...] } })`
5. **Drain**: If a global `drain` was configured, the event is sent

```typescript [scripts/migrate.ts]
import { initLogger, createLogger } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'

initLogger({
  env: { service: 'worker' },
  drain: createAxiomDrain(),
  sampling: { rates: { info: 10 } },
})

const log = createLogger({ task: 'migrate' })
log.set({ records: 500, status: 'complete' })
log.emit()
```

## Next Steps

- [Wide Events](https://www.evlog.dev/learn/wide-events) - Design effective wide events
- [Sampling](https://www.evlog.dev/learn/sampling) - Configure head and tail sampling
- [Adapters](https://www.evlog.dev/integrate/adapters/overview) - Send events to external platforms
- [Enrichers](https://www.evlog.dev/use-cases/enrichers) - Add derived context automatically

---

- [Wide Events](https://www.evlog.dev/learn/wide-events)
- [Sampling](https://www.evlog.dev/learn/sampling)


## Sitemap

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