_map_emit_error routed every MISCONFIGURED diagnostic to an ERROR log, so the
six user-typeable spellings of a JSON existence search (notes:*, notes.note:*,
notes.user:*, custom_fields:*, custom_fields.name:*, custom_fields.value:*)
each wrote one permanent ERROR line per request, and any authenticated user
could generate them in a loop.
Nothing is misconfigured. whoosh-compat decides EXISTS_REQUIRES_FAST from the
registry's own FieldSpec (kind plus fast) without consulting the index schema,
and field_descriptors() builds the JSON fields non-fast on purpose, so no
operator action can clear the condition. It is ordinary user error and now
gets the 400 with no alert. SCHEMA_FIELD_MISSING, the other MISCONFIGURED
kind, really is a registry-versus-schema comparison and keeps the ERROR.
The 400 and its user-facing message are unchanged. No log deduplication is
introduced; the classification is what was wrong, not the logging policy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This reverts ea883f416, which suppressed repeat MISCONFIGURED logs to
once per field per process.
A misconfigured field is a static condition an operator can fix in one
change, so the repetition is the prompt to fix it rather than noise to
suppress, and it stops on its own once the schema is corrected. Keeping
the suppression meant carrying machinery whose key boundedness and
check-then-add race both had to be reasoned about, to solve a problem
that ends when someone fixes the config.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
EXISTS_REQUIRES_FAST is MISCONFIGURED and reachable from ordinary query text
(notes.user:*), so the operator alert added with the Cause routing fired on
every such request. An alert that repeats on every user query is one operators
learn to filter out, which defeats routing MISCONFIGURED to an operator at all.
The condition is a static configuration fact: it stays true until an operator
changes the schema and reindexes, so the first log carries the same information
as the ten-thousandth. Deduped on (kind, field) in a per-process set; a restart
re-logs, re-surfacing the condition after a config change. The 400 is not
deduped: every request still gets its response and its message.
The key is bounded by the registry, not by query text. emit() only reports
MISCONFIGURED for a field it resolved, and FieldRegistry.resolve returns None
for any name or JSON subpath the registry does not declare.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The except QueryError arm converted every kind to a 400 on the strength of a
comment asserting the INTERNAL kinds could not occur. SCHEMA_FIELD_MISSING
fires on registry/schema drift, which deriving both from PUBLIC_FIELDS newly
makes possible, so a defect in our own wiring was reported to the user as a bad
query and never reached monitoring.
Diagnostics now route on Cause: INVALID_INPUT/UNSUPPORTED are a 400,
MISCONFIGURED is logged at error level naming the field and then a 400 (the
registry and the schema disagree, which only an operator can fix, but a request
is still waiting and the query cannot run either way), and INTERNAL is
re-raised rather than converted.
Messages, parse-time as well as emit-time, are built from the Diagnostic's
structured fields; d.message is documented as unstable developer output and
PATTERN_TOO_COMPLEX embedded raw backend error text in the 400 body.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>