Concepts

Filtering

Excluding, including, matching helpers, sampling and rate limiting.

Filters run after enrichment and before the sinks, so they can see every property an event will carry. A filter is (event) => boolean; true keeps the event. All filters must agree.

import { configure, byExcluding, byIncludingOnly, Matching } from '@unhingged/logit';

configure({
  filters: [
    byExcluding(Matching.withProperty('RequestPath', (p: string) => p.startsWith('/health'))),
    byExcluding(Matching.fromSource('next.telemetry')),
  ],
});

The two filters#

FilterSerilogKeeps
byExcluding(predicate)Filter.ByExcludingevents the predicate rejects
byIncludingOnly(predicate)Filter.ByIncludingOnlyevents the predicate accepts

Any function of the event is a filter on its own: filters: [(e) => e.level !== 'debug' || e.sourceContext === 'app.db'].

Matching#

Predicates for the usual cases, usable in filters and in conditional() sinks:

HelperMatches events…
Matching.fromSource('app.db')whose SourceContext is app.db or starts with app.db.
Matching.withProperty('UserId')that carry UserId
Matching.withProperty('StatusCode', (s: number) => s >= 500)whose StatusCode satisfies the predicate
Matching.withTemplate('HTTP {RequestMethod} …')written with exactly that template
Matching.messageMatches(/timeout/i)whose rendered message matches — renders the message, so keep it for rare paths

Sampling#

Keep a fraction of a noisy kind of event:

import { sampled } from '@unhingged/logit';

configure({ filters: [sampled(0.1, Matching.fromSource('http'))] }); // one request log in ten

Events the predicate does not match pass untouched. The random source is injectable for tests: sampled(0.5, pred, () => 0.3).

Rate limiting#

For the error that fires ten thousand times a second:

import { rateLimited } from '@unhingged/logit';

configure({ filters: [rateLimited(100, 1000, Matching.withTemplate('Connection to {Host} refused'))] }); // at most 100 per second

A fixed window: the count resets windowMs after the first matching event of the window. Beyond max, matching events are dropped silently. The clock is injectable.

Level overrides are filters too#

A per-source minimum (Levels) is the cheapest filter there is — it runs before anything is captured. Use it for "the framework at warn, my code at debug"; use filters for anything that needs a property.

Where filtering happens#

call → level check → parse + capture → bound properties → enrichers → FILTERS → sinks (each with its own minimum)

A sub-logger has its own filters, so routing — "errors to Seq, everything to the console" — is a sub-logger with a filter, see Sinks.

An expression language for all of this (RequestPath like '/health%'), Serilog.Expressions' role, is on the roadmap.