Logging

Simple Logging

evlog's general-purpose logger. A drop-in for console.log, pino, or consola, with the same level filtering, drain pipeline, redaction, and pretty/JSON output as wide events.

The log API is evlog's general-purpose logger. Use it the way you'd use pino, consola, or console.log — every call emits a structured event through the same drain pipeline as wide events. The two modes coexist; neither is an upgrade of the other.

Looking for the same API in CLIs, libraries, jobs, and edge? Start with Standalone TypeScript and Cloudflare Workers.
In Nuxt, log is auto-imported. No import statement needed.

Setup

For standalone projects (non-Nuxt), initialize once at startup:

src/index.ts
import { initLogger, log } from 'evlog'

initLogger({
  env: { service: 'my-app' },
})

log.info('app', 'Server started')
env.service defaults to 'app' if not specified. Only set it if you want a custom service name.

Two Call Styles

Tagged Logs

Pass a tag and a message for quick, readable output:

src/index.ts
import { log } from 'evlog'

log.info('auth', 'User logged in')
log.warn('cache', 'Cache miss for key user:42')
log.error('payment', 'Stripe webhook failed')
log.debug('router', 'Matched route /api/checkout')
Output (Pretty)
10:23:45.612 [auth] User logged in
10:23:45.613 [cache] Cache miss for key user:42
10:23:45.614 ERROR [payment] Stripe webhook failed
10:23:45.615 [router] Matched route /api/checkout

Structured Events

Pass an object for rich, queryable events that flow through the drain pipeline:

src/index.ts
import { log } from 'evlog'

log.info({ action: 'user_login', userId: 42, method: 'oauth', provider: 'github' })
log.error({ action: 'sync_failed', source: 'postgres', target: 's3', error: 'connection_timeout' })
Output (Pretty)
10:23:45.612 INFO [my-app]
  ├─ action: user_login
  ├─ userId: 42
  ├─ method: oauth
  └─ provider: github
Tagged logs are optimized for console readability. Structured events (object form) produce full wide events that flow through the drain pipeline to external services.

Log Levels

LevelMethodWhen to use
infolog.info()Normal operations: startup, shutdown, successful actions
warnlog.warn()Unexpected but recoverable situations: cache miss, retry, deprecation
errorlog.error()Failures that need attention: API errors, timeouts, invalid state
debuglog.debug()Development-only details: SQL queries, intermediate state, routing
log.debug() calls can be stripped from production builds using the Vite Plugin or the Nuxt module's strip option.

Common Patterns

Application Lifecycle

src/index.ts
import { log } from 'evlog'

log.info('app', 'Starting server on port 3000')
log.info({ action: 'db_connected', host: 'localhost', database: 'mydb', pool: 10 })
log.info('app', 'Ready to accept connections')

Background Tasks

src/jobs/cleanup.ts
import { log } from 'evlog'

log.info({ action: 'cron_started', job: 'cleanup', schedule: '0 */6 * * *' })
log.info({ action: 'cron_completed', job: 'cleanup', deleted: 42, duration: 1200 })

Utility Functions

src/utils/webhook.ts
import { log } from 'evlog'

function processWebhook(payload: WebhookPayload) {
  log.info({ action: 'webhook_received', type: payload.type, source: payload.source })

  if (!isValid(payload)) {
    log.warn({ action: 'webhook_invalid', type: payload.type, reason: 'missing_signature' })
    return
  }
}

Drain Integration

When using the object form, events are sent through the drain pipeline just like wide events:

src/index.ts
import { initLogger, log } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'

initLogger({
  env: { service: 'my-app' },
  drain: createAxiomDrain(),
})

log.info({ action: 'deploy', version: '1.2.3', region: 'us-east-1' })

Migrating from console / pino / consola / winston

Pick the tab matching your current logger to see the before call style. The after (evlog) snippet underneath is the same regardless of where you came from.

import pino from 'pino'

const log = pino({ name: 'checkout' })

log.info({ event: 'checkout_started' })
log.info({ event: 'cart_loaded', items: 3, total: 9999 })
log.warn({ event: 'inventory_low', sku: 'SKU-42' })
log.error({ event: 'payment_failed', reason: 'card_declined' })

All four become this — no formatter, transport, or peer-dep wiring required:

After (evlog)
import { initLogger, log } from 'evlog'

initLogger({ env: { service: 'checkout' } })

log.info({ event: 'checkout_started' })
log.info({ event: 'cart_loaded', items: 3, total: 9999 })
log.warn({ event: 'inventory_low', sku: 'SKU-42' })
log.error({ event: 'payment_failed', reason: 'card_declined' })

initLogger is one line at boot. The drain, redaction, sampling, pretty/JSON switching, and level filtering are all wired by default — no pino-pretty peer dep, no winston transport assembly, no consola reporter setup.

Want the full side-by-side (feature comparison tables, honest gaps, per-feature mapping)? See evlog vs pino, winston, consola.

Pairing with wide events

log and createLogger live inside the same logger. Use log.* for events that stand alone (startup messages, ad-hoc warnings, debug traces) and reach for createLogger when you want one event that captures an entire operation. They share the global drain, redaction, and types — pick per call.

scripts/sync-data.ts
import { initLogger, log, createLogger } from 'evlog'

initLogger({ env: { service: 'sync-worker' } })

log.info('sync', 'Worker starting')

const run = createLogger({ source: 'postgres', target: 's3' })
try {
  const records = await fetchRecords()
  run.set({ found: records.length })

  for (const record of records) {
    await syncOne(record)
    log.debug({ event: 'record_synced', id: record.id })
  }

  run.set({ status: 'complete', synced: records.length })
} catch (err) {
  log.error({ event: 'sync_failed' })
  run.error(err as Error)
  throw err
} finally {
  run.emit()
}

log.info('sync', 'Worker finished')

The log.* calls give you a real-time trail in development; the createLogger block gives your dashboard one queryable row per run. Both go through the same drain.

Next Steps