Standalone TypeScript
For scripts, CLI tools, queue workers, cron jobs, and any TypeScript process that doesn't use a web framework, evlog provides createLogger and createRequestLogger from the core package.
createWorkersLogger).Set up evlog in my TypeScript project
Quick Start
1. Install
pnpm add evlog
bun add evlog
yarn add evlog
npm install evlog
2. Initialize and create loggers
import type { DrainContext } from 'evlog'
import { initLogger, log, createLogger } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'
const pipeline = createDrainPipeline<DrainContext>({ batch: { size: 10 } })
const drain = pipeline(createAxiomDrain())
initLogger({
env: { service: 'my-script', environment: 'production' },
drain,
})
// Every log is automatically drained
log.info({ action: 'sync_started' })
const syncLog = createLogger({ jobId: 'sync-001', source: 'postgres', target: 's3' })
syncLog.set({ recordsSynced: 150 })
syncLog.emit() // drained automatically
// Flush remaining events before exit
await drain.flush()
drain.flush() before the process exits to ensure all buffered events are sent.evlog/vite plugin replaces the initLogger() call with compile-time auto-initialization, strips log.debug() from production builds, and injects source locations.createLogger vs createRequestLogger
evlog provides two manual logger constructors:
createLogger(context) - For non-HTTP contexts (scripts, CLI, queues):
import { createLogger } from 'evlog'
const log = createLogger({ jobId: 'migrate-001', source: 'postgres' })
log.set({ recordsProcessed: 500 })
log.emit()
createRequestLogger(requestMeta) - For HTTP-like contexts where you want method/path/status tracking:
import { createRequestLogger } from 'evlog'
const log = createRequestLogger({
method: 'POST',
path: '/webhook/stripe',
})
log.set({ event: 'invoice.paid', customerId: 'cus_123' })
log.emit()
Both require manual log.emit() calls since there is no automatic lifecycle to hook into.
Wide Events
Build up context progressively, then emit:
import { initLogger, createLogger } from 'evlog'
initLogger({
env: { service: 'migrate' },
})
const log = createLogger({ task: 'user-migration' })
const users = await db.query('SELECT * FROM legacy_users')
log.set({ found: users.length })
let migrated = 0
for (const user of users) {
await newDb.upsert({ id: user.id, email: user.email, plan: user.plan })
migrated++
}
log.set({ migrated, status: 'complete' })
log.emit()
14:58:15 INFO [migrate] user-migration
├─ migrated: 1250
├─ found: 1250
├─ status: complete
└─ task: user-migration
Error Handling
Use createError for structured errors:
import { createError, parseError } from 'evlog'
try {
const result = await externalApi.sync()
if (!result.ok) {
throw createError({
message: 'Sync failed',
why: `API returned ${result.status}`,
fix: 'Check the API status page and retry',
})
}
} catch (error) {
log.error(error instanceof Error ? error : new Error(String(error)))
log.emit()
const { message, why, fix } = parseError(error)
console.error(`${message}\nWhy: ${why}\nFix: ${fix}`)
process.exit(1)
}
Configuration
See the Configuration reference for all available options (initLogger, middleware options, sampling, silent mode, etc.).
Drain & Enrichers
Configure drain in initLogger:
import type { DrainContext } from 'evlog'
import { initLogger } from 'evlog'
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'
const pipeline = createDrainPipeline<DrainContext>({
batch: { size: 50, intervalMs: 5000 },
retry: { maxAttempts: 3 },
})
const drain = pipeline(createAxiomDrain())
initLogger({
env: { service: 'my-script' },
drain,
})
Next Steps
- Wide Events: Design comprehensive events with context layering
- Adapters: Send logs to Axiom, Sentry, PostHog, and more
- Sampling: Control log volume with head and tail sampling
- Structured Errors: Throw errors with
why,fix, andlinkfields