Options¶
use MrDlef\OsQueryDigest\{Formatter, Normalization, Options};
use MrDlef\OsQueryDigest\IndexNormalizer;
$formatter = Formatter::create(
Options::create()
->withNormalization(Normalization::structural())
->withMaxValues(5) // status:(500 or 502 or 503 or +8)
->withMaxClauses(12) // a and b and … +4 more
->withMaxLength(512) // hard cap on the line, never on the hash
->withIndexNormalizer(IndexNormalizer::datePatterns())
->withRedactor(fn ($field, $value) => $field === 'email' ? '<redacted>' : $value)
->withAggNames(false)
->withText(true) // false: emit idx/sig/hash only
->withHashLength(12)
);
The same options as a plain array — for a YAML file, a framework config block, or a CLI flag:
$formatter = Formatter::create(Options::fromArray([
'normalization' => 'structural',
'maxValues' => 5,
'aggNames' => true,
'text' => false,
]));
Unknown keys and wrong types throw InvalidOptionException instead of being
ignored: an option that silently does nothing is the bug you find months later,
in a dashboard that was never grouped the way the config claimed. Types are
taken as JSON gives them — "5" is rejected, because a front end that guesses
at "5" also accepts "five". redactor has no array form; a callable cannot
be expressed there.
withText(false), and what it does not promise¶
It removes the readable line from the digest — never rendered, so no accessor on
the object can hand out a value, and toArray() emits idx / sig / hash.
When the values may not leave the
building is the case it
exists for.
It is not, on its own, a guarantee that no literal is emitted.
Normalization::none() makes the signature equal the readable line, values
included, so the pair that emits none is withText(false) with any normalization
above none — which is the default, and the only combination worth calling
value-free.
An index name only you can read¶
datePatterns() collapses what any cluster does — dates, and standalone numeric
segments, which covers rolling indices and, pleasantly, multi-tenant numeric
prefixes:
tenant_0178_members → tenant_*_members
logs-2026.08.13 → logs-*
What it cannot collapse is a suffix whose meaning is yours: a content-versioned index, where the physical name carries a hash of the mapping and the alias moves over it on reindex. Left alone, every mapping change mints a fresh fingerprint for every query shape, and every dashboard built on the hash resets on the next deploy — the thing the normalizer exists to prevent.
use MrDlef\OsQueryDigest\IndexNormalizer;
Options::create()->withIndexNormalizer(IndexNormalizer::custom(
fn (string $index): string => preg_replace('/_[0-9a-f]{32}$/', '', $index),
));
tenant_0178_members_4f171971a955af948fae1c7a964c49b8 → tenant_*_members
tenant_0179_members_0000000000000000000000000000ffff → tenant_*_members
Your rule runs first, then the shipped one. The hook is for stripping what
this library cannot know is meaningless; dates and numeric segments are then
collapsed exactly as always. So the example lands on tenant_*_members, not on
tenant_0178_members — you do not reimplement what already works.
Three things it is worth knowing:
- It is called once per name. A request against
a,bgets two calls, and the deduplicating and sorting of a comma-separated list stays where it is. A rule may return''to drop one name from the list. - It is not trusted to return a string. Anything non-scalar reads as an
erased name rather than throwing: this runs in a logging path, where a
TypeErrorout of a closure would cost the log line and not just the digest. - A rule changes fingerprints — that is the point — so roll it out the way you would a prefix bump, not quietly on a Friday.
There is no shipped mode for this, deliberately. A rule that collapsed long hex runs generally would have to decide what a hash is — how long, which alphabet — and would move the fingerprint of every index name with hex anywhere in it. So the shipped rules stop where the cluster's own conventions stop: the tenant number is a number and collapses, and the suffix is left alone whatever it happens to be made of.
tenant_0178_members_4f171971a955af948fae1c7a964c49b8 → tenant_*_members_4f171971a955af948fae1c7a964c49b8
tenant_0179_members_9999999999999999999999999999aaaa → tenant_*_members_9999999999999999999999999999aaaa
Only you know where your suffix begins, which is why this is a callable rather than a third mode.