Tail Sampling
Tail sampling is a decision made after the request runs, with full knowledge of its outcome (status, duration, errors, custom flags). It's how you keep all errors and slow requests while throwing away the bulk of healthy traffic — the opposite of head sampling, which decides up front before knowing what happens.
The full theory and config reference — built-in keep rules, custom predicates via evlog:emit:keep, combining head + tail sampling — lives at Sampling. This page covers the extension surface: how to plug your own keep logic into the pipeline.
Configure tail sampling on evlog
When the built-in rules aren't enough
The built-in declarative keep rules cover the typical cases (status code thresholds, duration thresholds, path matching, level matching). Drop to a custom hook when you need:
- Conditional logic on more than one field (e.g. "keep if
status >= 500ANDuser.plan === 'enterprise'") - Keep based on a derived value (e.g. "keep if
event.audit?.context.actor.role === 'admin'") - Stateful decisions (rare; needs care since sampling runs in the hot path)
Custom keep hook
The hook signature is the same regardless of framework. The wiring depends on your runtime.
nitroApp.hooks.hook('evlog:emit:keep', (ctx) => {
if (ctx.context.user?.plan === 'enterprise' && ctx.status >= 500) {
ctx.shouldKeep = true
}
})
import { definePlugin } from 'evlog/toolkit'
export const keepEnterpriseErrors = definePlugin({
name: 'keep-enterprise-errors',
keep(ctx) {
if (ctx.context.user?.plan === 'enterprise' && ctx.status >= 500) {
ctx.shouldKeep = true
}
},
})
// Then: initLogger({ plugins: [keepEnterpriseErrors] })
// or: app.use(evlog({ plugins: [keepEnterpriseErrors] }))
For non-trivial logic, prefer the plugin shape — it travels with the rest of your evlog config (drains, enrichers) and is reusable across frameworks.
Composing several keep predicates
Use composeKeep from evlog/toolkit to combine multiple predicates into one hook. Each predicate runs independently and the final shouldKeep is true if any of them set it:
import { composeKeep } from 'evlog/toolkit'
const keep = composeKeep([
({ duration, shouldKeep }) => duration && duration > 2000 ? true : shouldKeep,
({ event }) => event.level === 'error',
({ context, status }) => context.user?.plan === 'enterprise' && status >= 500,
])
Errors in individual predicates are isolated (logged with the [evlog/keep] prefix) so a buggy predicate cannot silently drop legitimate events.
What you receive
The keep hook gets a TailSamplingContext:
interface TailSamplingContext {
/** The event level (debug | info | warn | error) */
level: string
/** HTTP response status, if known */
status?: number
/** Request duration in milliseconds, if measured */
duration?: number
/** The full accumulated context (everything log.set'd) */
context: Record<string, unknown>
/** The fully enriched event ready to drain */
event: WideEvent
/** Mutable: set to true to force-keep this event */
shouldKeep: boolean
}
Setting shouldKeep = true forces the event through. Setting shouldKeep = false is a no-op (other predicates may still keep it; the head sampler decides the default).
Next steps
- Sampling — head sampling, tail sampling, the built-in declarative
keeprules - Plugins — when keep belongs in a multi-hook plugin
- Best Practices — keep all errors, double-check the rules
Custom enrichers
Write custom enrichers to add derived context to your wide events. Add deployment metadata, tenant IDs, feature flags, geo, or any computed data — the toolkit handles error isolation, undefined skipping, and the merge step.
Identity headers
Every drain request sent by evlog is tagged with User-Agent and X-Evlog-Source headers so receivers can identify and triage the traffic. Override or suppress them when your custom drain needs different identity.