Wide events and structured errors for TypeScript. One log per request, full context, errors that explain why and how to fix.
Simple API
Accumulate context with log.set, throw structured errors with why and fix. One wide event captures everything — success or failure.
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 }
})✓ One log with full context
✓ Actionable error with context
Agent Ready
Structured fields, machine-readable context, and actionable metadata — everything an AI agent needs to diagnose and resolve issues on its own.
Card declined by issuer — insufficient funds
Pro plan user (#1842) blocked on payment
Prompt for alternate payment method
stripe.com/docs/declines/codes
✓ Auto-created issue PAY-4521
Drain Pipeline
Batched writes, automatic retries with backoff, and fan-out to multiple destinations. Your logs flow through a pipeline that never blocks your response.
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.
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,
})Client Logs
Capture browser events and drain them to your server. Automatic batching, retries, and page-aware flushing — same pipeline, client to server.
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.
import { createBrowserLogDrain } from 'evlog/browser'
const drain = createBrowserLogDrain({
drain: {
endpoint: '/api/_evlog/ingest',
},
pipeline: {
batch: { size: 25, intervalMs: 2000 },
retry: { maxAttempts: 2 },
},
})
initLogger({ drain })BATCH · FLUSH
POST · BEACON
Sampling
Two-tier filtering: head sampling drops noise by level, tail sampling rescues critical events. Never miss errors, slow requests, or critical paths.
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/**' },
]
}
})5 kept·3 dropped· noise reduced without data loss
Frameworks
One module for Nuxt. First-class Next.js support. Standalone API for everything else.
Installation guideexport 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 }
})
Wide events, structured errors, dead simple setup.
Set up evlog in 10 minutes. Your future self will thank you.