fix(server): extend query-window clamp to /api/logs; cap WATCHER_MAX_QUERY_HOURS (JEF-546)#142
Merged
thejefflarson merged 1 commit intoJul 27, 2026
Conversation
…QUERY_HOURS (JEF-546) query_logs floored `time` with `($n IS NULL OR time >= $n)` — unlike query_traces (JEF-532), the floor was skipped entirely when `from` was NULL and there was no max-lookback ceiling on an explicit `from` either. Combined with the `body ILIKE '%'||$n||'%'` filter, an authenticated caller searching a rare/absent term could walk the whole logs-retention window to satisfy `ORDER BY time DESC LIMIT 2000` — the same full-scan DoS JEF-532 closed for spans, reopened for logs. Separately, `max_lookback_hours()` guarded against `0`/negative env misconfig but not an absurdly large one — `WATCHER_MAX_QUERY_HOURS=999999999` resolves `now() - make_interval(hours => N)` to the distant past and silently re-opens the same full-scan. Fix: - query_logs: floor `time` with the same `GREATEST(COALESCE($from, now()-24h), now()-make_interval(hours=>$max))` SQL clamp query_traces uses, binding `max_lookback_hours()` as a new positional param. A `from` within the ceiling is honored unchanged. - max_lookback_hours(): extracted the parse+default logic into a pure `parse_max_lookback_hours()` helper and added `.clamp(1, 8760)` (1h..1yr) so every caller (traces/services/logs) inherits the cap. Tests: 4 unit tests on parse_max_lookback_hours (huge value clamps to 8760, zero/negative clamp to 1, unset/invalid defaults to 168, an in-range value passes through) plus a smoke.rs integration test (logs_from_beyond_max_lookback_is_clamped) mirroring traces_from_beyond_max_lookback_is_clamped: lowers the ceiling via WATCHER_MAX_QUERY_HOURS, inserts a log inside it and one outside it, and asserts an explicit far-past `from` still excludes the outside one. Also updated two pre-existing log-ingest smoke tests (ingest_and_query_a_log, ingest_a_log_batch_persists_every_row_in_one_call) that queried /api/logs with no `from` after ingesting logs at a fixed 1970 timestamp — the new default 24h floor now (correctly) excludes those from the unfiltered /api/logs response, so they now ingest at "now" instead, matching the trace tests' existing convention. Docs: WATCHER_MAX_QUERY_HOURS entry in docs/architecture.md now mentions /api/logs and the [1, 8760] clamp range. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
thejefflarson
deleted the
thejefflarson/jef-546-extend-query-window-clamp-to-apilogs-cap
branch
July 27, 2026 01:57
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes JEF-546.
Summary
Directly extends JEF-532 (#140):
/api/logs'query_logsflooredtimewith($n IS NULL OR time >= $n)— unlikequery_traces, the floor was skippedentirely when
fromwas NULL, and there was no max-lookback ceiling on anexplicit
fromeither. Combined with thebody ILIKE '%'||$n||'%'filter, anauthenticated caller searching a rare/absent term could walk the whole
logs-retention window to satisfy
ORDER BY time DESC LIMIT 2000— the samefull-scan DoS JEF-532 closed for spans, reopened here for logs.
Separately,
max_lookback_hours()guarded against0/negative env misconfigbut not an absurdly large one —
WATCHER_MAX_QUERY_HOURS=999999999resolvesnow() - make_interval(hours => N)to the distant past and silentlyre-opens the same full-scan.
Changes
query_logs: floortimewith the sameGREATEST(COALESCE($from, now()-24h), now()-make_interval(hours=>$max))SQL clamp
query_tracesuses, bindingmax_lookback_hours()as a newpositional param (
$9). Afromwithin the ceiling is honored unchanged(no regression) —
GREATESThandles that.max_lookback_hours(): extracted the parse+default logic into a pureparse_max_lookback_hours()helper and added.clamp(1, 8760)(1h .. 1yr)so every caller (traces/services/logs) inherits the cap.
docs/architecture.md: updated theWATCHER_MAX_QUERY_HOURSrow to mention/api/logsand the[1, 8760]clamp range.Test plan
cargo fmt --checkcargo checkcargo clippy --all-targets -- -D warningscargo test --locked(with a local Postgres viadocker compose up -d) — 62/62 smoke + 61/61 unit tests passparse_max_lookback_hours(huge value clamps to 8760,zero/negative clamp to 1, unset/invalid defaults to 168, an in-range value
passes through unchanged).
logs_from_beyond_max_lookback_is_clamped, mirroringtraces_from_beyond_max_lookback_is_clamped: lowers the ceiling viaWATCHER_MAX_QUERY_HOURS, inserts a log inside it and one outside it, andasserts an explicit far-past
fromstill excludes the outside one.(
ingest_and_query_a_log,ingest_a_log_batch_persists_every_row_in_one_call)that queried
/api/logswith nofromafter ingesting logs at a fixed 1970timestamp — the new default 24h floor now (correctly) excludes those from
the unfiltered
/api/logsresponse, so they now ingest at "now" instead,matching the pre-existing convention the trace-ingest tests already use.
Security
Ran soundcheck's
/security-review:pr-reviewpass on the diff: no Critical orHigh findings. All SQL parameters remain runtime-bound (
$1..$9); no stringinterpolation into query text; no new attack surface — this purely tightens
an existing DoS-prevention clamp.
🤖 Generated with Claude Code