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, acting as 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, so 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.
// Sampling config — works the same across all frameworks
evlog: {
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
Configuration
Complete reference for all evlog configuration options including global logger settings, middleware options, environment context, and framework-specific overrides.
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.