The problem
Support kept getting "this search returned nothing" or "the price is wrong" with nothing but a trace-id. The request that actually goes upstream is assembled from cached mappings, per-account config and supplier credentials — nobody can rebuild it by hand.
The hotel product had tooling for this. The other four logged to Redis and MySQL with no way to read any of it back short of SSH-ing into Redis.
The design
Two decisions carry the whole tool.
Snapshot the request at the first line of each handler — before controllers backfill defaults and transformers rewrite date formats — and store it as a compact normalised plain-text string rather than a JSON blob.
The field table is derived from each endpoint's own validation schema, so no
field is silently dropped and types survive: null, empty string and absent stay
three distinct states.
A read-back endpoint mints a fresh JWT and returns a paste-and-run curl.
The whole logging path is fire-and-forget with per-call-site try/catch, proven
safe by renaming the log table on staging and confirming search still returned
200.
The result
Because the snapshot point and the schema-derived field table are the same decision in every gateway, the rollout was mechanical: one MR per gateway, five products on the same day, verified against real staging domains. Payloads are 44–75% smaller than JSON.
After deploy I found my own bug: five identical searches produced five rows with exactly one distinct payload. Fixed by moving the write inside each product's existing cache-MISS branch — reusing a signal already present in the code instead of adding a new Redis key.
The production search-log table now holds roughly 626,000 rows.