Next.js
Practical patterns for using evlog with Next.js (App Router). Each section shows how to activate a feature and the recommended way to use it.
Production Configuration
A real-world lib/evlog.ts with enrichers, batched drain, tail sampling, and route-based service names:
import type { DrainContext } from 'evlog'
import { createEvlog } from 'evlog/next'
import { createUserAgentEnricher, createRequestSizeEnricher } from 'evlog/enrichers'
import { createAxiomDrain } from 'evlog/axiom'
import { createDrainPipeline } from 'evlog/pipeline'
// 1. Enrichers — add derived context to every event
const enrichers = [createUserAgentEnricher(), createRequestSizeEnricher()]
// 2. Pipeline — batch events before sending
const pipeline = createDrainPipeline<DrainContext>({ batch: { size: 50, intervalMs: 5000 } })
// 3. Drain — send batched events to Axiom
const drain = pipeline(createAxiomDrain({
dataset: 'logs',
token: process.env.AXIOM_TOKEN!,
}))
export const { withEvlog, useLogger, log, createError } = createEvlog({
service: 'my-app',
// 4. Head sampling — keep 10% of info logs
sampling: {
rates: { info: 10 },
keep: [
{ status: 400 }, // Always keep errors
{ duration: 1000 }, // Always keep slow requests
{ path: '/api/critical/**' }, // Always keep critical paths
],
},
// 5. Route-based service names
routes: {
'/api/auth/**': { service: 'auth-service' },
'/api/payment/**': { service: 'payment-service' },
'/api/booking/**': { service: 'booking-service' },
},
// 6. Custom tail sampling — business logic
keep: (ctx) => {
const user = ctx.context.user as { premium?: boolean } | undefined
if (user?.premium) ctx.shouldKeep = true
},
// 7. Enrich every event with user agent, request size, and deployment info
enrich: (ctx) => {
for (const enricher of enrichers) enricher(ctx)
ctx.event.deploymentId = process.env.VERCEL_DEPLOYMENT_ID
ctx.event.region = process.env.VERCEL_REGION
},
drain,
})
Wide Events
Build up context progressively through your handler. One request = one wide event:
import { withEvlog, useLogger } from '@/lib/evlog'
export const POST = withEvlog(async (request: Request) => {
const log = useLogger()
const body = await request.json()
// Stage 1: User context
log.set({
user: { id: body.userId, plan: 'enterprise' },
})
// Stage 2: Cart context
log.set({
cart: { items: body.items.length, total: body.total, currency: 'USD' },
})
// Stage 3: Payment context
const payment = await processPayment(body)
log.set({
payment: { method: payment.method, cardLast4: payment.last4 },
})
return Response.json({ success: true, orderId: payment.orderId })
})
All fields are merged into a single wide event emitted when the handler completes:
10:23:45.612 INFO [my-app] POST /api/checkout 200 in 145ms
├─ user: id=usr_123 plan=enterprise
├─ cart: items=3 total=14999 currency=USD
├─ payment: method=card cardLast4=4242
└─ requestId: a1b2c3d4-...
Error Handling
Use createError for structured errors with why, fix, and link fields that help developers debug in both logs and API responses:
import { withEvlog, useLogger, createError } from '@/lib/evlog'
export const POST = withEvlog(async (request: Request) => {
const log = useLogger()
const body = await request.json()
log.set({ payment: { amount: body.amount } })
if (body.amount <= 0) {
throw createError({
status: 400,
message: 'Invalid payment amount',
why: 'The amount must be a positive number',
fix: 'Pass a positive integer in cents (e.g. 4999 for $49.99)',
link: 'https://docs.example.com/api/payments#amount',
})
}
const result = await chargeCard(body)
if (!result.success) {
log.error(new Error(`Payment declined: ${result.reason}`))
throw createError({
status: 402,
message: 'Payment declined',
why: `Card declined by issuer: ${result.reason}`,
fix: 'Try a different payment method or contact your bank',
})
}
return Response.json({ success: true })
})
withEvlog() catches EvlogError and returns a structured JSON response (like Nitro does for Nuxt):
{
"name": "EvlogError",
"message": "Payment declined",
"status": 402,
"data": {
"why": "Card declined by issuer: insufficient_funds",
"fix": "Try a different payment method or contact your bank"
}
}
In the terminal, the error renders with colored output:
Error: Payment declined
Why: Card declined by issuer: insufficient_funds
Fix: Try a different payment method or contact your bank
Parsing Errors on the Client
Use parseError to extract the structured fields from any error — fetch responses, EvlogError, or plain Error objects:
'use client'
import { parseError } from 'evlog'
async function handleSubmit(formData: FormData) {
try {
const res = await fetch('/api/payment/process', {
method: 'POST',
body: JSON.stringify({ amount: Number(formData.get('amount')) }),
})
if (!res.ok) throw { data: await res.json(), status: res.status }
} catch (error) {
const { message, status, why, fix, link } = parseError(error)
// message: "Payment declined"
// why: "Card declined by issuer: insufficient_funds"
// fix: "Try a different payment method or contact your bank"
}
}
parseError normalizes any error shape into a flat { message, status, why?, fix?, link? } object, so your UI code never has to dig through nested data.data or check for different error formats.
Tail Sampling
Combine rule-based and custom tail sampling to always capture what matters, even when head sampling drops most logs:
export const { withEvlog, useLogger } = createEvlog({
service: 'my-app',
sampling: {
rates: { info: 10 }, // Only keep 10% of info logs
keep: [
{ status: 400 }, // Always keep 4xx/5xx
{ duration: 1000 }, // Always keep slow requests
{ path: '/api/critical/**' }, // Always keep critical paths
],
},
// Custom: always keep premium user requests
keep: (ctx) => {
const user = ctx.context.user as { premium?: boolean } | undefined
if (user?.premium) ctx.shouldKeep = true
},
})
The keep rules use OR logic — any match forces the event through regardless of head sampling.
Middleware
Set x-request-id and x-evlog-start headers so withEvlog() can correlate timing across the middleware → handler chain:
import { evlogMiddleware } from 'evlog/next'
export const proxy = evlogMiddleware()
export const config = {
matcher: ['/api/:path*'],
}
middleware.ts instead of proxy.ts. The evlog middleware works with both — import from evlog/next regardless.Client Provider
Wrap your root layout with EvlogProvider to enable client-side logging and transport:
import { EvlogProvider } from 'evlog/next/client'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<EvlogProvider service="my-app" transport={{ enabled: true }}>
{children}
</EvlogProvider>
</body>
</html>
)
}
Client Logging
Use log in any client component. Identity is preserved across all logs and transported to the server:
'use client'
import { log, setIdentity, clearIdentity } from 'evlog/next/client'
export function Dashboard({ user }: { user: { id: string } }) {
// Set identity once — all subsequent logs include it
useEffect(() => {
setIdentity({ userId: user.id })
return () => clearIdentity()
}, [user.id])
return (
<button onClick={() => log.info({ action: 'export_clicked', format: 'csv' })}>
Export
</button>
)
}
Browser Drain
For advanced use cases, send structured DrainContext events directly from the browser to a custom endpoint:
import { createBrowserLogDrain } from 'evlog/browser'
const drain = createBrowserLogDrain({
drain: { endpoint: '/api/evlog/browser-ingest' },
pipeline: { batch: { size: 10, intervalMs: 5000 } },
})
drain(drainEvent)
await drain.flush()
The server endpoint receives batched events:
export async function POST(request: Request) {
const events = await request.json()
// Forward to your drain pipeline, Axiom, etc.
return new Response(null, { status: 204 })
}
Run Locally
git clone https://github.com/HugoRCD/evlog.git
cd evlog/examples/nextjs
bun install
bun run dev
Open http://localhost:3000 to explore the example.
Retention
Configure how long logs are kept in NuxtHub and how they are automatically cleaned up with scheduled tasks, cron jobs, and retention policies.
SvelteKit
Using evlog with SvelteKit — automatic wide events, structured errors, drain adapters, enrichers, and tail sampling in SvelteKit applications.