---
title: "evlog vs LogLayer: an API layer or a logger"
description: "Compare LogLayer transports, context and its wide-events mixin with evlog request events, and decide whether to keep your logger behind LogLayer."
canonical_url: "https://www.evlog.dev/compare/loglayer"
---
# evlog vs LogLayer: an API layer or a logger

> Compare LogLayer transports, context and its wide-events mixin with evlog request events, and decide whether to keep your logger behind LogLayer.

Do you want to keep the logger you have, or replace it? LogLayer is an API layer: it gives your code one fluent API and forwards each record through a [transport](https://loglayer.dev/transports/) to pino, winston, LogTape, consola or a service such as Datadog, so the library underneath stays swappable. evlog is the logger itself. It builds the events and delivers them through drain adapters.

## At a glance

<table>
<thead>
  <tr>
    <th>
      
    </th>
    
    <th>
      LogLayer
    </th>
    
    <th>
      evlog
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Role
    </td>
    
    <td>
      API in front of a logging library or service, one or several transports per instance
    </td>
    
    <td>
      The logger, with its own output and drain adapters
    </td>
  </tr>
  
  <tr>
    <td>
      Structured data
    </td>
    
    <td>
      <code>
        withContext()
      </code>
      
       persists across records, <code>
        withMetadata()
      </code>
      
       and <code>
        withError()
      </code>
      
       apply to one record
    </td>
    
    <td>
      <code>
        set()
      </code>
      
       accumulates on an operation logger until <code>
        emit()
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Wide events
    </td>
    
    <td>
      <code>
        @loglayer/mixin-wide-events
      </code>
      
      : <code>
        withWideEvents()
      </code>
      
       then <code>
        emitWideEvent()
      </code>
      
      , with an <code>
        AsyncLocalStorage
      </code>
      
       you create
    </td>
    
    <td>
      Built in: the request logger accumulates and the <a href="https://www.evlog.dev/integrate/frameworks/overview">
        framework integrations
      </a>
      
       emit at the end of each request
    </td>
  </tr>
  
  <tr>
    <td>
      Framework integrations
    </td>
    
    <td>
      Express, Fastify, Hono, Koa and Elysia: a request-scoped child logger and pino-http style <code>
        incoming request
      </code>
      
       and <code>
        request completed
      </code>
      
       records
    </td>
    
    <td>
      One event per request, with <code>
        useLogger()
      </code>
      
       and the framework's own accessor
    </td>
  </tr>
  
  <tr>
    <td>
      Routing
    </td>
    
    <td>
      Groups send records to chosen transports with per-group levels
    </td>
    
    <td>
      No per-subsystem routing
    </td>
  </tr>
  
  <tr>
    <td>
      Redaction
    </td>
    
    <td>
      <code>
        @loglayer/plugin-redaction
      </code>
      
      : configured paths, through fast-redact
    </td>
    
    <td>
      On by default in production once <code>
        initLogger()
      </code>
      
       has run: keys ending in <code>
        password
      </code>
      
      , <code>
        passwd
      </code>
      
      , <code>
        secret
      </code>
      
       or <code>
        token
      </code>
      
      , built-in patterns such as emails and card numbers, and configured paths
    </td>
  </tr>
  
  <tr>
    <td>
      Sampling
    </td>
    
    <td>
      A sampling plugin, and rate, per-level, <code>
        shouldEmit
      </code>
      
       and <code>
        forceKeep
      </code>
      
       options on the wide-events mixin
    </td>
    
    <td>
      Per-level rates and tail sampling with <code>
        keep
      </code>
      
       conditions
    </td>
  </tr>
  
  <tr>
    <td>
      Testing
    </td>
    
    <td>
      <code>
        MockLogLayer
      </code>
      
      , a logger whose methods do nothing
    </td>
    
    <td>
      A memory drain that captures emitted events
    </td>
  </tr>
</tbody>
</table>

Sources: the [LogLayer introduction](https://loglayer.dev/introduction), the [wide-events mixin](https://loglayer.dev/mixins/wide-events), the [redaction plugin](https://loglayer.dev/plugins/redaction), the [Express integration](https://loglayer.dev/integrations/express) and [unit testing](https://loglayer.dev/logging-api/unit-testing) pages, read for loglayer 9.4.0.

## Compare who emits the wide event

Both libraries accumulate fields and emit one record per operation. The difference is who wires that lifecycle. LogLayer's integrations write their two request records but do not emit a wide event. Its [Wide Events guide](https://loglayer.dev/guides/wide-events) has the application write Express middleware that runs the async context and calls `emitWideEvent()` when the response finishes. evlog's integrations open the request logger and emit it themselves.

Outside `asyncContext.run()`, LogLayer keeps nothing: with mixin 1.4.0, `emitWideEvent()` then writes a record that holds only its message, and no warning is printed. Run every request inside the context.

Each example below records one simulated checkout and its outcome. Install the corresponding packages and run the file with Node.js 24. Pass `decline` to exercise the error path.

LogLayer accumulates through the mixin inside an async context:

```typescript [checkout-loglayer.ts]
import { AsyncLocalStorage } from 'node:async_hooks'
import { LogLayer, StructuredTransport, useLogLayerMixin } from 'loglayer'
import { createWideEventMixin } from '@loglayer/mixin-wide-events'

const asyncContext = new AsyncLocalStorage()
useLogLayerMixin(createWideEventMixin({ asyncContext }))
const log = new LogLayer({ transport: new StructuredTransport({ logger: console }) })

await asyncContext.run({}, async () => {
  log.withWideEvents({ orderId: 'order-123', amount: 2999 })
  try {
    if (process.argv.includes('decline')) throw new Error('Payment declined')
    log.withWideEvents({ outcome: 'paid' })
    log.emitWideEvent({ message: 'checkout' })
  } catch (error) {
    log.withWideEventError(error)
    log.withWideEvents({ outcome: 'declined' })
    log.emitWideEvent({ message: 'checkout', level: 'error' })
    process.exitCode = 1
  }
})
```

evlog accumulates on the operation logger and emits in `finally`:

```typescript [checkout-evlog.ts]
import { createLogger, initLogger } from 'evlog'

initLogger({ env: { service: 'checkout' }, pretty: false })
const log = createLogger({ orderId: 'order-123', amount: 2999 })
try {
  if (process.argv.includes('decline')) throw new Error('Payment declined')
  log.set({ outcome: 'paid' })
} catch (error) {
  log.error(error instanceof Error ? error : new Error(String(error)))
  log.set({ outcome: 'declined' })
  process.exitCode = 1
} finally {
  log.emit()
}
```

LogLayer emits at `info` unless you pass a level, so the error path passes `level: 'error'`. evlog raises the event to `error` once `log.error()` has run, and adds `durationMs`. These examples compare how emission is owned, not output formats or speed.

## Keep LogLayer, or plan the migration

Keep LogLayer when its purpose is the one you need: keeping pino or winston swappable behind one API, routing subsystems to different transports with groups, or sending one record to several destinations. LogLayer can also sit in front of a logger you are not ready to replace.

If you migrate, plan these mappings before changing call sites:

<table>
<thead>
  <tr>
    <th>
      LogLayer
    </th>
    
    <th>
      evlog
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        withContext()
      </code>
    </td>
    
    <td>
      Initial context of <code>
        createLogger()
      </code>
      
      , or <code>
        set()
      </code>
      
       on the request logger
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        withMetadata().info()
      </code>
      
       for one record
    </td>
    
    <td>
      <code>
        log.info({ ... })
      </code>
      
       from <a href="https://www.evlog.dev/learn/simple-logging">
        Simple Logging
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      Wide-events mixin and your middleware
    </td>
    
    <td>
      A <a href="https://www.evlog.dev/integrate/frameworks/overview">
        framework integration
      </a>
      
      , which emits per request
    </td>
  </tr>
  
  <tr>
    <td>
      Groups and per-group levels
    </td>
    
    <td>
      No direct equivalent. Decide which records still need separate routing
    </td>
  </tr>
  
  <tr>
    <td>
      Redaction plugin paths
    </td>
    
    <td>
      <code>
        redact.paths
      </code>
      
      , on top of the default redaction in production
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        MockLogLayer
      </code>
    </td>
    
    <td>
      <code>
        enabled: false
      </code>
      
       to silence, or the memory drain to assert on events
    </td>
  </tr>
</tbody>
</table>

## Measure the stack you will run

A LogLayer call costs the LogLayer layer plus the logger behind its transport, so a measurement of LogLayer over pino is not a measurement of LogLayer alone. The [Performance reference](https://www.evlog.dev/reference/performance) explains evlog's benchmark boundaries. Compare both on the transport, payload and redaction you intend to deploy.

For the logger LogLayer most often sits in front of, see [evlog vs Pino](https://www.evlog.dev/compare/pino). The [logger overview](https://www.evlog.dev/reference/vs-other-loggers) covers the other choices.

---

- [evlog vs Pino](https://www.evlog.dev/compare/pino)
- [evlog vs LogTape](https://www.evlog.dev/compare/logtape)
- [Wide Events](https://www.evlog.dev/learn/wide-events)


## Sitemap

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