mr-dlef / os-query-digest
Human-readable, loggable digests and stable fingerprints for OpenSearch DSL queries.
Requires
- php: ^7.4 || ^8.0
- ext-json: *
Requires (Dev)
- ezimuel/ringphp: ^1.2
- friendsofphp/php-cs-fixer: ^3.95
- guzzlehttp/guzzle: ^7.4
- monolog/monolog: ^2.0 || ^3.0
- phpstan/phpstan: ^2.1
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^9.6 || ^10.5 || ^11.5
- psr/http-client: ^1.0
- psr/http-message: ^1.1 || ^2.0
- psr/log: ^1.1 || ^2.0 || ^3.0
- rector/rector: ^2.6
- symfony/yaml: ^5.4 || ^6.0 || ^7.0
Suggests
- ezimuel/ringphp: To digest every search on its way out of a ringphp-based client — elasticsearch-php 7.x, opensearch-php ≤ 2.3 — via MrDlef\OsQueryDigest\Http\Ring\DigestingHandler (^1.2)
- guzzlehttp/guzzle: To digest every search on its way out of a Guzzle client, via MrDlef\OsQueryDigest\Http\Guzzle\DigestMiddleware (^7.0)
- monolog/monolog: To replace raw search requests with their digest automatically, via MrDlef\OsQueryDigest\Monolog\DigestProcessor (^2.0 || ^3.0)
- psr/http-client: To digest every search on its way out of any PSR-18 client, via MrDlef\OsQueryDigest\Http\DigestingClient (^1.0)
- psr/log: To log what the transport integrations capture, via MrDlef\OsQueryDigest\Http\LoggingObserver (^1.1 || ^2.0 || ^3.0)
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-13 22:33:06 UTC
README
Human-readable, loggable digests and stable fingerprints for OpenSearch DSL queries.
A DSL query in a log line is a wall of nested braces: nobody greps it, nobody groups by it, and it costs a fortune in log volume. So the question you actually have during an incident — which kind of query is hurting us? — has no answer anywhere in your stack.
This gives it one!
📖 Documentation · ▶ Try it on your own query
Before. One search request, as it lands in your logs — and there are thousands of these a day:
{
"query": {
"bool": {
"filter": [
{ "term": { "service": "api" } },
{ "range": { "@timestamp": { "gte": "now-15m", "lt": "now" } } }
],
"must_not": [ { "term": { "status": 200 } } ]
}
},
"size": 50,
"sort": [ { "@timestamp": "desc" } ],
"_source": [ "@timestamp", "message", "status" ]
}
After. The same request, three ways:
text logs-* | q=(@timestamp >= now-15m and @timestamp < now and not status:200 and service:api) | size=50 sort=@timestamp:desc
sig logs-* | q=(@timestamp >= ? and @timestamp < ? and not status:? and service:?) | size=50 sort=@timestamp:desc
hash q5:fe168406e702
| what it is | what it is for | |
|---|---|---|
text |
the query in DQL, with real values | paste it into the Dashboards search bar |
signature |
the same line with literals erased | read the shape at a glance |
hash |
versioned fingerprint of the signature | terms aggregate on it |
The third one is the point. Log the hash next to took, and your log index
answers questions your dashboards cannot: which kind of query got slow this
week, which one showed up with Friday's deploy, which one was hammering the
cluster during the incident. Not which thousand queries were slow — which
shape was. OpenSearch, analysing OpenSearch.
That query and that fingerprint are not illustrations: they are
tests/fixtures/01-error-rate-filter, and the golden file pins the exact hash.
Install
Download the CLI
It digests a query you paste, a file of them a line at a time, or the slow log your cluster is already writing — every flag is in the guide:
curl -sSLO https://github.com/mrDlef/php-os-query-digest/releases/latest/download/os-query-digest.phar
chmod +x os-query-digest.phar
./os-query-digest.phar slowlog /var/log/opensearch/*_index_search_slowlog.log
That last one ranks the shapes in the file by what they cost you: no code to
change, nothing to deploy, no index to create. Needs PHP 7.4 → 8.5 on the
machine, and every release carries a .sha256 beside the file.
Or run it in Docker
cat *_index_search_slowlog.log \ | docker run -i --rm ghcr.io/mrdlef/os-query-digest slowlog
It reads standard input, so nothing has to be mounted. Mounting a log instead is one flag. Both artefacts are built from the same source as the package and mint the same fingerprints — CI digests one query each way and compares the two.
Then put it in your application
composer require mr-dlef/os-query-digest
Three ways in, and they mix:
- The Monolog processor replaces the request body with its digest wherever your application already logs one. One processor, no call sites.
- A PSR-18 decorator or a Guzzle middleware digests every search on its way out of the HTTP client, already joined to what it cost.
- The formatter, when you want the digest in your own hands:
use MrDlef\OsQueryDigest\Formatter; $digest = Formatter::create()->describe($request, 'logs-2026.08.13'); $logger->info('opensearch.search', [ 'q' => $digest->text(), 'hash' => $digest->hash(), 'took' => $response['took'], ]);
ext-json, and no runtime dependencies: Monolog, PSR-18 and Guzzle are
suggested, never required.
What you get
- Logs you can read, and fewer bytes of them. A 40-line body becomes one capped line — and it is DQL, so you select it, paste it into the Dashboards search bar, and you are looking at the same query.
- The dashboard is written already. An index template and four panels ship in the package — import them and you are looking at which shape costs you, which one regressed, and which one the last release added.
- It cannot break your logging. Nothing is required at runtime, the digest is lazy, and a request it cannot parse yields an error field rather than an exception. You lose the digest, never the log line.
- It says what kind of work each search is. Every digest carries a
kind—suggest,aggregate,scan,lookup,browse— read off the parsed request, so a top-N grouped by it reads as a sentence instead of two hundred hashes. - It tells you why. When two queries you thought were different share a
hash,
explain()names the rule that merged them. - It is verified, not asserted. Certified against real OpenSearch 2.19.6 and
3.8.0 nodes, checked against the official API specification, PHPStan at
level: max, mutation-tested, benchmarked, and run on PHP 7.4 → 8.5.
Documentation
mrdlef.github.io/php-os-query-digest — a guide for each way in, the reference, and the reasoning behind the normalisation rules.
- Getting started goes from nothing to a log line you can read.
- Hash stability comes before storing a fingerprint anywhere permanent: the prefix moves when the rules do, so a stored hash is never silently reinterpreted.
- Which normalisation level answers which question — the default fingerprints page 1 and page 3 of a search apart, which is right for ranking by latency and wrong for counting what your application searches for.
The site tracks the latest release, not main.
Contributing and security
CONTRIBUTING.md — the commands, and the rules that are not
guessable from the code.
SECURITY.md — report a vulnerability privately, not in an
issue.
License
LGPL-3.0-or-later — see LICENSE.
The LGPL builds on the GPL, so both texts ship: LICENSE holds the Lesser GPL and LICENSE.GPL the GPL it refers to.
In practice: you can use this library in a closed-source application without that application becoming subject to the licence. If you modify the library itself and distribute it, those changes stay under the LGPL.