Sampling
At scale, logging everything gets expensive fast. Sampling lets you keep costs under control without losing visibility into what matters. evlog uses a two-tier approach: head sampling drops noise upfront, tail sampling rescues critical events after the fact.
Head Sampling
Head sampling randomly keeps a percentage of logs per level. It runs before the request completes — a coin flip at emission time.
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
sampling: {
rates: {
info: 10, // Keep 10% of info logs
warn: 50, // Keep 50% of warnings
debug: 0, // Drop all debug logs
error: 100, // Always keep errors (default)
},
},
},
})
import { createEvlog } from 'evlog/next'
export const { withEvlog, useLogger } = createEvlog({
service: 'my-app',
sampling: {
rates: {
info: 10,
warn: 50,
debug: 0,
error: 100,
},
},
})
import { initLogger } from 'evlog'
initLogger({
env: { service: 'my-app' },
sampling: {
rates: {
info: 10,
warn: 50,
debug: 0,
error: 100,
},
},
})
Each level is a percentage from 0 to 100. Levels you don't configure default to 100% (keep everything). Error defaults to 100% even when other levels are configured — you have to explicitly set error: 0 to drop errors.
10% rate means roughly 1 in 10 info logs are kept — not exactly 1 in 10.Tail Sampling
Head sampling is blind — it doesn't know if a request was slow, failed, or hit a critical path. Tail sampling fixes this by evaluating after the request completes and force-keeping logs that match specific conditions.
// Works the same across all frameworks
sampling: {
rates: { info: 10 },
keep: [
{ status: 400 }, // HTTP status >= 400
{ duration: 1000 }, // Request took >= 1s
{ path: '/api/payments/**' }, // Critical path (glob)
],
}
Conditions use >= comparison for status and duration, and glob matching for path. If any condition matches, the log is kept regardless of head sampling (OR logic).
Available Conditions
| Condition | Type | Description |
|---|---|---|
status | number | Keep if HTTP status >= value (e.g., 400 catches all 4xx and 5xx) |
duration | number | Keep if request duration >= value in milliseconds |
path | string | Keep if request path matches glob pattern (e.g., '/api/critical/**') |
How They Work Together
The two tiers complement each other:
- Request completes — evlog knows the status, duration, and path
- Tail sampling evaluates — if any
keepcondition matches, the log is force-kept - Head sampling applies — only if tail sampling didn't force-keep, the random percentage check runs
- Log emits or drops — kept logs go through enrichment and draining as normal
This means a request to /api/payments/charge that returns a 500 in 2 seconds will always be logged, even if info is set to 1%. The tail conditions rescue it.
sampling: {
rates: { info: 10 },
keep: [
{ status: 400 },
{ duration: 1000 },
],
}
POST /api/users 200 45ms → 10% chance (head sampling)
POST /api/users 500 45ms → always kept (status >= 400)
GET /api/products 200 2300ms → always kept (duration >= 1000)
POST /api/checkout 200 120ms → 10% chance (head sampling)
Custom Tail Sampling
For conditions beyond status, duration, and path, use the evlog:emit:keep hook in Nuxt/Nitro or the keep callback in other frameworks.
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:emit:keep', (ctx) => {
if (ctx.context.user?.plan === 'enterprise') {
ctx.shouldKeep = true
}
})
})
import { createEvlog } from 'evlog/next'
export const { withEvlog, useLogger } = createEvlog({
service: 'my-app',
sampling: {
rates: { info: 10 },
keep: [{ status: 400 }],
},
keep(ctx) {
if (ctx.context.user?.plan === 'enterprise') {
ctx.shouldKeep = true
}
},
})
import { evlog } from 'evlog/hono'
app.use(evlog({
keep(ctx) {
if (ctx.context.user?.plan === 'enterprise') {
ctx.shouldKeep = true
}
},
}))
The ctx object contains:
| Field | Type | Description |
|---|---|---|
status | number | undefined | HTTP response status |
duration | number | undefined | Request duration in ms |
path | string | undefined | Request path |
method | string | undefined | HTTP method |
context | Record<string, unknown> | All fields set via log.set() |
shouldKeep | boolean | Set to true to force-keep |
Production Example
A typical production configuration that balances cost and visibility:
export default defineNuxtConfig({
modules: ['evlog/nuxt'],
evlog: {
env: { service: 'my-app' },
},
$production: {
evlog: {
sampling: {
rates: {
info: 10,
warn: 50,
debug: 0,
error: 100,
},
keep: [
{ status: 400 },
{ duration: 1000 },
{ path: '/api/payments/**' },
{ path: '/api/auth/**' },
],
},
},
},
})
import { createEvlog } from 'evlog/next'
export const { withEvlog, useLogger } = createEvlog({
service: 'my-app',
sampling: {
rates: {
info: 10,
warn: 50,
debug: 0,
error: 100,
},
keep: [
{ status: 400 },
{ duration: 1000 },
{ path: '/api/payments/**' },
{ path: '/api/auth/**' },
],
},
})
import { initLogger } from 'evlog'
initLogger({
env: { service: 'my-app' },
sampling: {
rates: {
info: 10,
warn: 50,
debug: 0,
error: 100,
},
keep: [
{ status: 400 },
{ duration: 1000 },
{ path: '/api/payments/**' },
{ path: '/api/auth/**' },
],
},
})
$production override to keep full logging in development while sampling in production. In other frameworks, use your own environment check or config system.Next Steps
- Best Practices — Security and production checklist
- Wide Events — Design effective wide events
Typed Fields
Add compile-time type safety to your wide events with TypeScript module augmentation. Prevent typos and ensure consistent field names across your codebase.
Client Logging
Capture browser events with structured logging. Same API as the server, with automatic console styling, user identity context, and optional server transport.