Frameworks

Standalone TypeScript

Using evlog in standalone TypeScript — scripts, CLI tools, queues, cron jobs, and any TypeScript process.

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.

Quick Start

1. Install

bun add evlog

2. Initialize and create loggers

scripts/sync-job.ts
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()
Always call drain.flush() before the process exits to ensure all buffered events are sent.

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 — there is no automatic lifecycle to hook into.

Wide Events

Build up context progressively, then emit:

scripts/migrate-users.ts
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()
Terminal output
14:58:15 INFO [migrate] user-migration
  ├─ migrated: 1250
  ├─ found: 1250
  ├─ status: complete
  └─ task: user-migration

Error Handling

Use createError for structured errors:

scripts/sync-job.ts
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)
}

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,
})
See the Adapters docs for all available drain adapters (Axiom, OTLP, PostHog, Sentry, Better Stack).
See the full bun-script example for a complete working script.