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 for scripts, workers, or CLI tools.
- Install evlog: pnpm add evlog
- Import initLogger and createLogger (or createRequestLogger) from 'evlog'
- Call initLogger({ env: { service: 'my-script' } }) once at startup
- Create a logger per logical operation with createLogger({ jobId, source })
- Use log.set() to accumulate context as the operation progresses
- Call log.emit() manually when the operation completes
Docs: https://www.evlog.dev/integrate/frameworks/standalone Adapters: https://www.evlog.dev/integrate/adapters/overview
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