Stop grepping
through chaos.

Wide events and structured errors for TypeScript. One log per request, full context, errors that explain why and how to fix.

request logs

Set context.
Get answers.

Accumulate context with log.set, throw structured errors with why and fix. One wide event captures everything, whether the request succeeds or fails.

Wide events Root cause Fix suggestion
Quick start guide
checkout.post.ts
export default defineEventHandler(async (event) => {
  const log = useLogger(event)

  log.set({ user: { id: user.id, plan: user.plan } })
  log.set({ cart: { items: 3, total: 9999 } })

  if (!charge.success) {
    throw createError({
      status: 402,
      why: 'Card declined by issuer',
      fix: 'Try a different card',
    })
  }

  return { orderId: charge.id }
})
output
INFOPOST/api/checkout(234ms)
user: { id: 1842, plan: "pro" }
cart: { items: 3, total: 9999 }
status: 200
requestId: "req_8f2k..."

One log with full context

ERRORPOST/api/checkout402
message: "Payment failed"
why: "Card declined by issuer"
fix: "Try a different card"
user: { id: 1842, plan: "pro" }

Actionable error with context

Built for agents.

Structured fields, machine-readable context, and actionable metadata that give AI agents everything they need to diagnose and resolve issues on their own. Enable the file system drain to write NDJSON logs locally and let agents read them directly.

Structured context Machine-parseable Local NDJSON files
Agent skills setup
outputERROR
ERRORPOST/api/payment402
message: "Payment processing failed"
why: "Card issuer declined: insufficient funds"
fix: "Retry with a different payment method"
user: { id: 1842, plan: "pro" }
links: ["stripe.com/docs/declines"]
AI Agent analyzing
Reading structured error context...
Root cause

Card declined by issuer — insufficient funds

User impact

Pro plan user (#1842) blocked on payment

Suggested fix

Prompt for alternate payment method

Documentation

stripe.com/docs/declines/codes

Auto-created issue PAY-4521

Send everywhere.

Batched writes, automatic retries with backoff, and fan-out to multiple destinations. Your logs flow through a pipeline that never blocks your response.

Batching Retry & backoff Fan-out
Explore adapters

Non-blocking

Pipeline runs in the background. Your response ships immediately.

Guaranteed delivery

Exponential backoff with jitter ensures logs reach every destination.

Bring your own drain

Write a simple function to send logs anywhere.

evlog-drain.ts
import { createDrainPipeline } from 'evlog/pipeline'
import { createAxiomDrain } from 'evlog/axiom'
import { createSentryDrain } from 'evlog/sentry'

const pipeline = createDrainPipeline({
  drains: [
    createAxiomDrain(),
    createSentryDrain(),
  ],
  batchSize: 50,
  flushInterval: 5000,
})
evlog
BATCH · RETRY · FANOUT
Axiom
OTLP
Sentry
PostHog
Better Stack
+ File System·Custom drains

See the full picture.

Capture browser events and drain them to your server. Automatic batching, retries, and page-aware flushing with the same pipeline from client to server.

Auto-batch sendBeacon Origin validation
Client logging guide

Automatic batching

Events are batched by size and time interval, reducing network overhead.

Page-aware delivery

Switches to sendBeacon when the page is hidden. No event left behind.

Server-side validation

Origin check, payload sanitization, and source tagging on every ingest.

browser-drain.ts
import { createBrowserLogDrain } from 'evlog/browser'

const drain = createBrowserLogDrain({
  drain: {
    endpoint: '/api/_evlog/ingest',
  },
  pipeline: {
    batch: { size: 25, intervalMs: 2000 },
    retry: { maxAttempts: 2 },
  },
})

initLogger({ drain })
BrowserEVENTS

BATCH · FLUSH

PipelineRETRY · BACKOFF

POST · BEACON

ServerVALIDATE · DRAIN
auto-flush on page visibility change

Keep what matters.

Two-tier filtering: head sampling drops noise by level, tail sampling rescues critical events. Never miss errors, slow requests, or critical paths.

Head sampling Tail sampling Per-level rates
Sampling guide
evlog.config.ts
initLogger({
  sampling: {
    // Head: per-level rates
    rates: {
      info:  10,   // keep 10%
      warn:  50,   // keep 50%
      error: 100,  // always
    },
    // Tail: force keep if match
    keep: [
      { status: 400 },
      { duration: 1000 },
      { path: '/api/critical/**' },
    ]
  }
})
log stream
HEADTAIL
INFO/api/users45ms
INFO/api/orders120ms
INFO/api/health12ms
WARN/api/payment340ms
INFO/api/search1240ms
ERROR/api/checkout450ms
INFO/api/feed32ms
INFO/api/critical/alert55ms

5 kept·3 dropped· noise reduced without data loss

Add logging,
not overhead.

Zero dependencies, 5.1 kB gzip, ~7µs per request. Benchmarked against pino, consola, and winston — evlog wins 5 out of 6 comparisons while producing richer, more useful output.

Zero-alloc hot path CodSpeed CI Open source benchmarks
Full benchmark results
benchmark
evlog
1.0M
consola
690K1.5x slower
pino
473K2.2x slower
winston
373K2.7x slower

ops/sec · higher is better · silent mode (no I/O)

evlog
819K
consola
477K1.7x slower
pino
283K2.9x slower
winston
132K6.2x slower

ops/sec · higher is better · silent mode (no I/O)

evlog
7.6M
pino
2.4M3.2x slower
winston
1.8M4.3x slower
consola
122K62.6x slower

ops/sec · higher is better · silent mode (no I/O)

0deps/5.1 kBgzip/12frameworks/tree-shakeable
why it's fast

1 event, not N log lines

Accumulate context, emit once. 75% less data downstream.

In-place mutations

No object spreads, no copies. Direct recursive merge.

Lazy allocation

Timestamps, sampling context — created only when needed.

No serialization until drain

Plain objects throughout. JSON.stringify runs once at the end.

Zero dependencies

No transitive deps. Nothing to audit, nothing to break.

Total overhead per request

create + 3x set + emit + sampling + enrichers

~7µs

0.007ms

Your stack. Covered.

Native integrations for every major framework. One import, zero config, same API everywhere. The Vite plugin adds auto-init, debug stripping, and source location to any Vite-based stack.

Framework integrations
Vite
server/api/checkout.post.ts
export default defineEventHandler(async (event) => {
  const log = useLogger(event)
  const { cartId } = await readBody(event)

  const cart = await db.findCart(cartId)
  log.set({ cart: { items: cart.items.length, total: cart.total } })

  const charge = await stripe.charge(cart.total)
  log.set({ stripe: { chargeId: charge.id } })

  if (!charge.success) {
    throw createError({
      status: 402,
      message: 'Payment failed',
      why: charge.decline_reason,
      fix: 'Try a different payment method',
    })
  }

  return { orderId: charge.id }
})

Better logging
by tonight.

Wide events, structured errors, dead simple setup. Set up evlog in 10 minutes. Your future self will thank you.

© 2026 - Made by HugoRCD