Quick Start
This guide covers the core APIs you'll use most often with evlog.
useLogger, log, createError, parseError). No import statements needed.Get evlog running in 2 minutes
log (Simple Logging)
The simplest way to use evlog. Fire-and-forget structured logs, anywhere in your code:
import { log } from 'evlog'
log.info('auth', 'User logged in')
log.error({ action: 'payment', error: 'card_declined' })
log.warn('cache', 'Cache miss')
10:23:45.612 [auth] User logged in
10:23:45.613 ERROR [my-app] action=payment error=card_declined
10:23:45.614 [cache] Cache miss
Two call styles:
- Tagged:
log.info('tag', 'message')for quick, readable console output - Structured:
log.info({ key: value })for rich events that flow through the drain pipeline
createLogger (Wide Events)
When you need to accumulate context across multiple steps of an operation, whether a script, background job, queue worker, or workflow, use createLogger:
import { initLogger, createLogger } from 'evlog'
initLogger({ env: { service: 'sync-worker' } })
const log = createLogger({ jobId: job.id, queue: 'emails' })
log.set({ batch: { size: 50 } })
log.set({ batch: { processed: 50 } })
log.emit()
10:23:45.612 INFO [sync-worker] in 1204ms
├─ jobId: job_abc123
├─ queue: emails
└─ batch: size=50 processed=50
createLogger() accepts any initial context as a plain object. It returns a logger with set, error, info, warn, emit, and getContext.
For HTTP request contexts specifically, use createRequestLogger() which pre-populates method, path, and requestId:
import { createRequestLogger } from 'evlog'
const log = createRequestLogger({ method: 'POST', path: '/api/checkout' })
createLogger and createRequestLogger, you must call log.emit() manually. In framework integrations, this happens automatically.useLogger (Retrieve the Request Logger)
When using a framework integration (Nuxt, Hono, Express, etc.), the middleware automatically creates a wide event logger on request start and emits it on response end. useLogger(event) retrieves that logger from the request context:
import { useLogger } from 'evlog'
export default defineEventHandler(async (event) => {
const log = useLogger(event)
log.set({ user: { id: 1, plan: 'pro' } })
log.set({ cart: { items: 3, total: 9999 } })
const order = await processCheckout()
log.set({ orderId: order.id })
return { success: true, orderId: order.id }
})
10:23:45.612 INFO [my-app] POST /api/checkout 200 in 234ms
├─ user: id=1 plan=pro
├─ cart: items=3 total=9999
└─ orderId: ord_abc123
useLogger doesn't create a logger, the framework middleware already did that. It just retrieves it from the event context so you can add data with set().When to use what
Use log | Use createLogger() / createRequestLogger() | Use useLogger(event) |
|---|---|---|
| Quick one-off events | Scripts, jobs, workers, queues, HTTP without a framework | API routes with a framework integration |
| No context accumulation needed | Accumulate context over an operation | Retrieve the request-scoped logger |
| Client-side logging | Wide events (one log per operation) | Access the auto-managed wide event |
Service Identification
In multi-service architectures, differentiate which service a log belongs to using either route-based configuration or explicit service names.
Route-Based Configuration
Configure service names per route pattern in your nuxt.config.ts:
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
env: {
service: 'default-service',
},
routes: {
'/api/auth/**': { service: 'auth-service' },
'/api/payment/**': { service: 'payment-service' },
'/api/booking/**': { service: 'booking-service' },
},
},
})
Logs from routes matching these patterns will automatically include the configured service name:
21:57:10.442 INFO [auth-service] POST /api/auth/login 200 in 1ms
├─ requestId: 88ced16a-bef2-4483-86cb-2b4fb677ea52
├─ user: id=user_123 email=demo@example.com
└─ action: login
Explicit Service Parameter
Override the service name for specific routes using the second parameter of useLogger:
import { useLogger } from 'evlog'
export default defineEventHandler((event) => {
const log = useLogger(event, 'legacy-service')
log.set({ action: 'process_legacy_request' })
return { success: true }
})
useLogger parameter > Route configuration > env.service > Auto-detected from environmentcreateError (Structured Errors)
Use createError() to throw errors with actionable context:
import { createError } from 'evlog'
throw createError({
message: 'Payment failed',
status: 402,
why: 'Card declined by issuer',
fix: 'Try a different payment method',
link: 'https://docs.example.com/payments/declined',
})
{
"statusCode": 402,
"message": "Payment failed",
"data": {
"why": "Card declined by issuer",
"fix": "Try a different payment method",
"link": "https://docs.example.com/payments/declined"
}
}
Error Fields
| Field | Required | Description |
|---|---|---|
message | Yes | What happened (user-facing) |
status | No | HTTP status code (default: 500) |
why | No | Technical reason (for debugging) |
fix | No | Actionable solution |
link | No | Documentation URL for more info |
cause | No | Original error (if wrapping) |
internal | No | Backend-only fields for logs and wide events — never included in HTTP JSON or parseError() |
Frontend Integration
Use parseError() to extract all error fields on the client:
import { parseError } from 'evlog'
export async function checkout(cart: Cart) {
try {
await $fetch('/api/checkout', { method: 'POST', body: cart })
} catch (err) {
const error = parseError(err)
toast.add({
title: error.message,
description: error.why,
color: 'error',
actions: error.link
? [{ label: 'Learn more', onClick: () => window.open(error.link) }]
: undefined,
})
if (error.fix) {
console.info(`Fix: ${error.fix}`)
}
}
}
log (Client-Side)
The same log API works on the client side, outputting to the browser console:
<script setup lang="ts">
async function handleCheckout() {
log.info('checkout', 'User initiated checkout')
try {
await $fetch('/api/checkout', { method: 'POST' })
log.info({ action: 'checkout', status: 'success' })
} catch (err) {
log.error({ action: 'checkout', error: 'failed' })
}
}
</script>
export function useAnalytics() {
function trackEvent(event: string, data?: Record<string, unknown>) {
log.info('analytics', `Event: ${event}`)
if (data) {
log.debug({ event, ...data })
}
}
return { trackEvent }
}
How much of this do you need on day one?
Two calls. Everything else on this page is available when you want it, and nothing breaks while you do not.
log.info() behaves like console.log, so the first commit is a find-and-replace that changes no behaviour. From there, the two that pay for themselves are log.set(), which adds a field to the event already in flight, and createError(), which attaches a why and a fix to a failure instead of a bare stack.
Migrate one handler at a time. A route still calling console.log next to a route using log.set() is a normal intermediate state, not a broken one: they write to the same place and neither knows about the other. Pick the handler you last debugged in production and start there, or let evlog map rank them for you.
Typed fields, catalogs, sampling and drains are all opt-in, and each one is worth reaching for the day you feel its absence rather than the day you install.
Next Steps
evlog map: See what you just unlocked. Score coverage and find entry points that are still dark (npx @evlog/cli map)- Logging Overview: Understand all three logging modes
- Wide Events: Learn how to design effective wide events
- Typed Fields: Add compile-time type safety to your wide events
- Structured Errors: Master error handling with evlog
- Best Practices: Security guidelines and production tips