---
title: "Learn evlog"
description: "The mental model: three logging modes, the wide event lifecycle, sampling, typed fields, and redaction. Read it in order if you are new."
canonical_url: "https://www.evlog.dev/learn/overview"
---
# Learn evlog

> The mental model: three logging modes, the wide event lifecycle, sampling, typed fields, and redaction. Read it in order if you are new.

This section is the **mental model** of evlog. By the end, you'll know exactly what evlog does, when each API fits, and how an event flows from your code to your drain.

If you're new, read it in order. If you've already shipped with evlog, jump to the page that matches your question.

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

All three modes coexist in the same logger. Pick per call. There is no upgrade path, no advanced mode, no toggle to flip. Same drains, same redaction, same types underneath.

</callout>

<callout color="neutral" icon="i-lucide-globe">

Not running an HTTP framework? See [Standalone TypeScript](https://www.evlog.dev/integrate/frameworks/standalone) and [Cloudflare Workers](https://www.evlog.dev/integrate/frameworks/cloudflare-workers).

</callout>

## The three logging modes

<card-group>
<card color="neutral" icon="i-lucide-terminal" title="Simple Logging" to="https://www.evlog.dev/learn/simple-logging">

A fully-featured general-purpose logger. Replaces `console.log`, consola, pino, or winston with `log.info`, `log.error`, `log.warn`, `log.debug`, with the same level filtering, drain pipeline, redaction, and pretty/JSON output.

</card>

<card color="neutral" icon="i-lucide-layers" title="Wide Events" to="https://www.evlog.dev/learn/wide-events">

Accumulate context over a unit of work (a script, job, queue task, or request) then emit a single comprehensive event.

</card>

<card color="neutral" icon="i-lucide-git-branch" title="Request Logging" to="https://www.evlog.dev/integrate/frameworks/overview">

Auto-managed wide events scoped to HTTP requests. Framework middleware creates the logger and emits it for you.

</card>
</card-group>

## Compare the three at a glance

### Simple Logging (`log`)

One event per call. No accumulation, no lifecycle management.

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

log.info('auth', 'User logged in')
log.error({ action: 'payment', error: 'card_declined', userId: 42 })
```

### Wide Events (`createLogger` / `createRequestLogger`)

One event per unit of work. Accumulate context progressively, emit when done.

<code-group>

```typescript [scripts/sync-job.ts]
import { createLogger } from 'evlog'

const log = createLogger({ jobId: 'sync-001', queue: 'emails' })
log.set({ batch: { size: 50, processed: 50 } })
log.emit()
```

```typescript [src/worker.ts]
import { createRequestLogger } from 'evlog'

const log = createRequestLogger({ method: 'POST', path: '/api/checkout' })
log.set({ user: { id: 1, plan: 'pro' } })
log.emit()
```

</code-group>

`createRequestLogger` is a thin wrapper around `createLogger` that pre-populates `method`, `path`, and `requestId`.

### Request Logging (framework middleware)

Framework integrations create a wide event logger automatically on each request. `useLogger(event)` retrieves the logger that's already attached to the request context:

<framework-tabs>

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

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

  log.set({ user: { id: 1, plan: 'pro' } })
  return { success: true }
  // auto-emitted on response end
})
```

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

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

  log.set({ user: { id: 1, plan: 'pro' } })
  return { success: true }
  // auto-emitted on response end
})
```

```typescript [Next.js]app/api/checkout/route.ts
import { withEvlog, useLogger } from '@/lib/evlog'

export const POST = withEvlog(async () => {
  const log = useLogger()

  log.set({ user: { id: 1, plan: 'pro' } })
  return Response.json({ success: true })
  // auto-emitted on response end
})
```

```typescript [Hono]src/index.ts
app.post('/api/checkout', async (c) => {
  const log = c.get('log')

  log.set({ user: { id: 1, plan: 'pro' } })
  return c.json({ success: true })
  // auto-emitted on response end
})
```

```typescript [Express]src/index.ts
app.post('/api/checkout', async (req, res) => {
  const log = req.log

  log.set({ user: { id: 1, plan: 'pro' } })
  return res.json({ success: true })
  // auto-emitted on response end
})
```

</framework-tabs>

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

`useLogger(event)` doesn't create a logger, it retrieves the one the framework middleware already attached to the event. Each framework has its own way to access it (`useLogger`, `req.log`, `c.get('log')`, etc.). In Nuxt, `useLogger` is auto-imported.

</callout>

## Choose the mode for the job

<table>
<thead>
  <tr>
    <th>
      
    </th>
    
    <th>
      <code>
        log
      </code>
    </th>
    
    <th>
      <code>
        createLogger
      </code>
      
       / <code>
        createRequestLogger
      </code>
    </th>
    
    <th>
      Framework middleware
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Use case
      </strong>
    </td>
    
    <td>
      Quick one-off events
    </td>
    
    <td>
      Scripts, jobs, workers, queues, HTTP without a framework
    </td>
    
    <td>
      API routes with a framework integration
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Context
      </strong>
    </td>
    
    <td>
      Single call
    </td>
    
    <td>
      Accumulate with <code>
        set()
      </code>
    </td>
    
    <td>
      Accumulate with <code>
        set()
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Emit
      </strong>
    </td>
    
    <td>
      Immediate
    </td>
    
    <td>
      Manual <code>
        emit()
      </code>
    </td>
    
    <td>
      Automatic on response end
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Lifecycle
      </strong>
    </td>
    
    <td>
      None
    </td>
    
    <td>
      You manage it
    </td>
    
    <td>
      Framework manages it
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Output
      </strong>
    </td>
    
    <td>
      Console + drain
    </td>
    
    <td>
      Console + drain
    </td>
    
    <td>
      Console + drain + enrich
    </td>
  </tr>
</tbody>
</table>

### By context

<table>
<thead>
  <tr>
    <th>
      Context
    </th>
    
    <th>
      Best fit
    </th>
    
    <th>
      Why
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        HTTP route in Nuxt / Next / Hono / Express / …
      </strong>
    </td>
    
    <td>
      <code>
        useLogger(event)
      </code>
      
       via <a href="https://www.evlog.dev/integrate/frameworks/overview">
        framework integration
      </a>
    </td>
    
    <td>
      One wide event per request, auto-emitted on response end
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        HTTP handler without a framework
      </strong>
    </td>
    
    <td>
      <code>
        createRequestLogger({ method, path })
      </code>
    </td>
    
    <td>
      Same shape as framework middleware, manual emit
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        CLI tool / one-shot script
      </strong>
    </td>
    
    <td>
      <code>
        log.*
      </code>
      
       for steps + <code>
        createLogger
      </code>
      
       for the run summary — see <a href="https://www.evlog.dev/integrate/frameworks/standalone">
        Standalone
      </a>
    </td>
    
    <td>
      Pretty in dev, structured in CI, one summary event for the whole run
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Published library
      </strong>
    </td>
    
    <td>
      <code>
        createLogger
      </code>
      
       only — never <code>
        initLogger
      </code>
      
       — see <a href="https://www.evlog.dev/integrate/frameworks/standalone">
        Standalone
      </a>
    </td>
    
    <td>
      Don't pollute the host app's global config or force a drain on consumers
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Background job / queue worker / cron
      </strong>
    </td>
    
    <td>
      <code>
        createLogger({ jobId, queue })
      </code>
      
       per invocation — see <a href="https://www.evlog.dev/integrate/frameworks/standalone">
        Standalone
      </a>
    </td>
    
    <td>
      One wide event per job run, perfect for retry analysis
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Cloudflare Worker / edge function
      </strong>
    </td>
    
    <td>
      <code>
        createWorkersLogger(req)
      </code>
      
       or <code>
        createRequestLogger
      </code>
      
       — see <a href="https://www.evlog.dev/integrate/frameworks/cloudflare-workers">
        Cloudflare Workers
      </a>
    </td>
    
    <td>
      Per-request event, no <code>
        process
      </code>
      
       globals required
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        AWS Lambda
      </strong>
    </td>
    
    <td>
      <code>
        initLogger
      </code>
      
       once + <code>
        createLogger
      </code>
      
       per invocation — see <a href="https://www.evlog.dev/integrate/frameworks/aws-lambda">
        AWS Lambda
      </a>
    </td>
    
    <td>
      Cold-start init, per-event scope, drain flush in the handler
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Batch / pipeline step
      </strong>
    </td>
    
    <td>
      <code>
        createLogger({ step })
      </code>
      
       per stage
    </td>
    
    <td>
      One event per stage with inputs and outputs side by side
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        AI agent / LLM call
      </strong>
    </td>
    
    <td>
      <code>
        createLogger
      </code>
      
       + <a href="https://www.evlog.dev/use-cases/ai-sdk/overview">
        <code>
          createAILogger
        </code>
      </a>
    </td>
    
    <td>
      Token usage, tool calls, streaming metrics on the same wide event
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Library function called inside a request
      </strong>
    </td>
    
    <td>
      <code>
        useLogger(event)
      </code>
      
       from caller, or accept a logger as argument
    </td>
    
    <td>
      Inherit the parent's request context, contribute to the same wide event
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Shared workspace package
      </strong>
    </td>
    
    <td>
      Treat it like a library — see <a href="https://www.evlog.dev/integrate/frameworks/standalone">
        Standalone
      </a>
    </td>
    
    <td>
      Host app owns <code>
        initLogger
      </code>
      
       / drain; packages use <code>
        createLogger
      </code>
      
       or accept a logger
    </td>
  </tr>
</tbody>
</table>

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

None of these is an "upgrade" of another. Use `log` and `createLogger` in the same file when it makes sense, since they share the global drain, redaction, and types.

</callout>

## What all three share

All three modes share the same foundation:

- **Pretty output** in development, **JSON** in production (default, no configuration needed)
- **Drain pipeline** to send events to Axiom, Sentry, PostHog, and more. See [Integrate / Adapters](https://www.evlog.dev/integrate/adapters/overview)
- **Structured errors** with `why`, `fix`, and `link`, plus optional backend-only **internal** for logs
- **Sampling** (head + tail) to control log volume in production
- **Redaction** that wipes secrets before they ever leave the process
- **Zero dependencies**, ~6 kB gzip

## Where to go from here

After the three modes, the rest of Learn covers the concepts that show up across every mode:

- [Structured Errors](https://www.evlog.dev/learn/structured-errors): `why`, `fix`, `link`, `internal`, and how `createError` differs from `throw new Error`
- [Catalogs](https://www.evlog.dev/learn/catalogs): typed error / audit catalogs that survive refactors
- [Lifecycle](https://www.evlog.dev/learn/lifecycle): exactly what happens between `emit()` and your drain
- [Sampling](https://www.evlog.dev/learn/sampling): keep all errors and slow requests; drop healthy noise
- [Typed Fields](https://www.evlog.dev/learn/typed-fields): augment `RequestLogger` so `log.set` is autocompleted
- [Redaction](https://www.evlog.dev/learn/redaction): the rules that strip `authorization`, `password`, `token`, etc. before drain

When you're done with Learn, head to [Integrate](https://www.evlog.dev/integrate/frameworks/overview) to wire evlog into your stack, or run [`evlog map`](https://www.evlog.dev/cli/map) first to find missing evlog patterns in supported entry points.

---

- [Simple Logging](https://www.evlog.dev/learn/simple-logging)
- [Wide Events](https://www.evlog.dev/learn/wide-events)
- [Lifecycle](https://www.evlog.dev/learn/lifecycle)


## Sitemap

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