Skip to content

v0.5.0 — a CLI and a browser playground

Choose a tag to compare

@mrDlef mrDlef released this 17 Aug 23:39
· 101 commits to main since this release
d1b207a

Two ways to use the library without writing a line of PHP: a command you can
pipe a slow log through, and a page that runs it in your browser.

No hash moves. Every fingerprint v0.4.0 produced, v0.5.0 produces. This
is additive throughout.

A CLI

$ echo '{"query":{"term":{"service":"api"}},"size":50}' \
    | vendor/bin/os-query-digest --index logs-2026.08.13
idx:  logs-*
text: logs-* | q=(service:api) | size=50
sig:  logs-* | q=(service:?) | size=50
hash: q2:5b2210eb5318

--explain appends the rules table, --json emits the digest object, --hash
emits nothing but the fingerprint.

The reason it exists is --ndjson — one query per input line, one line of
output each, which is the shape sort | uniq -c | sort -rn expects:

$ os-query-digest --ndjson --hash < slow.ndjson | sort | uniq -c | sort -rn
      3 q2:3109618415cb
      1 q2:a3e42b3a6c70

Those three are not three slow queries to read: they are one shape, hit on two
different days, with two different service values. That is the question no
amount of reading individual slow queries answers.

A malformed line is reported on stderr and skipped — a slow log is untrusted
input, and stopping at the first mangled record would make the tool useless
exactly where it is needed. Exit codes: 0 ok, 1 an input could not be
parsed, 2 a bad invocation.

A playground

https://mrdlef.github.io/php-os-query-digest/ runs this library on your
query, in your browser, with no server involved: PHP itself compiled to
WebAssembly. Your query never leaves the page — there is nowhere to send it.

It opens on a precomputed example and downloads nothing; the moment you
change the query or an option it fetches a PHP 8.3 and runs the real library.
Measured in Chromium: 2.77 MB transferred, ~300 ms to a working interpreter,
0.5 ms per query after that.

Pin a query as a reference, then edit it: the page tells you whether the
fingerprint moved and which normalisation rule made the difference — the
question you actually have when two queries you thought were different share a
hash. Every state is a permalink, so a bug report can be a link.

What it shows is guarded by the offline suite: the library is shipped to the
browser as one file, and PlaygroundTest executes that file with real PHP
against the golden fixtures. "The browser runs the same library as composer require does" is checked by CI without a browser or a byte of wasm.

Options::fromArray()

Every front end that is not PHP configures through a string map — a CLI flag, a
YAML block, a query string:

Formatter::create(Options::fromArray([
    'normalization' => 'structural',
    'maxValues'     => 5,
    'aggNames'      => true,
]));

Unknown keys and wrong types throw InvalidOptionException rather than 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".

Normalization::fromLevel() and IndexNormalizer::fromMode() are its
counterparts, and Options::KEYS, Normalization::LEVELS and
IndexNormalizer::MODES are public so a help text or a <select> never
hard-codes a vocabulary that then drifts.

Also

  • The README leads with the problem and a real before/after, using a fixture as
    its showcase — so every number on the front page is pinned by the test suite.
  • Workflows moved off Node 20 actions.
  • Tested on PHP 7.4 → 8.5, PHPStan level: max, still no runtime dependencies.