Simple Logging
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.
log is auto-imported. No import statement needed.Setup
For standalone projects (non-Nuxt), initialize once at startup:
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:
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')
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:
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' })
10:23:45.612 INFO [my-app]
├─ action: user_login
├─ userId: 42
├─ method: oauth
└─ provider: github
Log Levels
| Level | Method | When to use |
|---|---|---|
info | log.info() | Normal operations: startup, shutdown, successful actions |
warn | log.warn() | Unexpected but recoverable situations: cache miss, retry, deprecation |
error | log.error() | Failures that need attention: API errors, timeouts, invalid state |
debug | log.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
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
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
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:
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' })
import { createLogger, format, transports } from 'winston'
const log = createLogger({
defaultMeta: { service: 'checkout' },
format: format.json(),
transports: [new transports.Console()],
})
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' })
import { consola } from 'consola'
const log = consola.withTag('checkout')
log.info('Starting checkout')
log.info('cart loaded', { items: 3, total: 9999 })
log.warn('inventory low', { sku: 'SKU-42' })
log.error('payment failed', { reason: 'card_declined' })
console.log('[checkout] Starting checkout')
console.log('[checkout] cart loaded', { items: 3, total: 9999 })
console.warn('[checkout] inventory low', { sku: 'SKU-42' })
console.error('[checkout] payment failed', { reason: 'card_declined' })
All four become this — no formatter, transport, or peer-dep wiring required:
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.
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.
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
- Wide Events: Accumulate context and emit comprehensive events
- Structured Errors: Throw errors with
why,fix, andlink - Configuration: All
initLoggeroptions - Adapters: Send events to Axiom, Sentry, PostHog, and more
- Standalone TypeScript: Scripts, workers, and libraries without a web framework
- evlog vs other loggers: Side-by-side with pino, winston, consola
Overview
evlog gives you three ways to log. Simple one-liners, wide events that accumulate context, and auto-managed request logging. Choose the right one for your use case.
Wide Events
Accumulate context over any unit of work and emit a single comprehensive event. Works for HTTP requests, scripts, background jobs, queue workers, and workflows.