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 — success or failure.

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 — everything an AI agent needs to diagnose and resolve issues on its own.

Structured context Machine-parseable Actionable output
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
+ Custom drains

See the full picture.

Capture browser events and drain them to your server. Automatic batching, retries, and page-aware flushing — same pipeline, 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

Your stack. Covered.

One module for Nuxt. First-class Next.js support. Standalone API for everything else.

Installation guide
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