Extend

Tail Sampling

Decide post-hoc whether to keep an event with full knowledge of its outcome (status, duration, errors). The opposite of head sampling — keep all errors and slow requests while throwing away healthy noise.
sampling decision· tail → head
tail rules: status ≥ 400 duration ≥ 1000 path: /api/payments/**
#1POST/api/users·200·45ms
dropped
#2POST/api/users·500·45ms
kept
#3GET/api/products·200·2300ms
kept
#4POST/api/payments/charge·200·120ms
kept
#5POST/api/checkout·200·120ms
kept
#6GET/api/health·200·12ms
dropped
tail-kept0 / 0
head-kept0 / 0
dropped0

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 >= 500 AND user.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
  }
})

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 keep rules
  • Plugins — when keep belongs in a multi-hook plugin
  • Best Practices — keep all errors, double-check the rules