Skip to content

v1.3.38.4 -- 7-day ranges on the existing indexes

Fifth release of the v1.3.38 series ("dashboard paints in under a second"; scope in docs/operations/ux-performance-review.md). No schema change and, by design, no new index. This is a bridge until v1.3.40 (retention, ingest filter, hourly rollup) reshapes log_entries. argosVersion 1.3.38.3 -> 1.3.38.4.

PHASE 0: do the covering indexes exist?

Yes for everything that is a count. On a .backup copy of prod (480k caddy_access rows in the 7 d window; copy deleted after):

Figure As shipped (visits rows) On existing indexes
requests per hour by status class 5.7 s status BETWEEN a AND b on idx_log_entries_status_ts, GROUP BY hour: 0.22 s per class (the single status >= 100 ... GROUP BY status/100 form makes the planner pick idx_log_entries_timestamp and visit rows: 7 s)
total / by source 5.8 s / 0.16 s idx_log_entries_source_ts, idx_log_entries_timestamp: 0.09-0.15 s
top hosts 4.6 s (host_domain) by host_id on idx_log_entries_host_ts: 0.13 s (BETWEEN on that query also flips the planner to idx_timestamp, 7 s; pinned with INDEXED BY)
p50/p95/p99 durations 6.4 s no index carries duration_ms: newest 24 h only, 1.0 s
top paths 7.1 s no index carries path: newest 24 h, 0.14 s
bandwidth 6.6 s no index carries size_bytes: newest 24 h, 0.08 s
logs/stats avg + p95 5.8 s + 7.9 s newest 20,000 rows: 0.37 s + 0.05 s

Useful fact: 0 non-access rows carry status >= 100, so a status range implies source = caddy_access without touching the row.

What ships

  • dashboard.Queries.Traffic: ranges over 24 h without a host filter take trafficLong: four covering class queries for the hourly series, top hosts by host_id mapped through hosts, and response_times / top_paths / bandwidth_out_bytes over the newest 24 h (detail_window: "24h0m0s", detail_from, series_covers_range: true). With a host filter the whole response is bounded to the newest 24 h and series_covers_range is false (no index carries host and status together; the busiest host is half the table).
  • db.ComputeStats / db.ComputeTimeseries: filters that are only from/to (+ source=caddy_access) over more than 24 h, or unbounded, take the covering path: total, classes, sources and top hosts over the whole window; avg / p95 on the newest 20,000 rows (sample_n); top paths on the newest 24 h (detail_window). Any other filter keeps the row queries.
  • UI: each card that does not cover the full range says so in its own title; the Logs strip says "sampled, n=20,000". Cards that cover the range carry no note.
  • Planner regression tests with EXPLAIN QUERY PLAN on the test DB (must show COVERING INDEX <name>, must not SCAN).

Smoke gate

scripts/smoke/range-7d-latency.sh, three cold probes, each <= 5 s: traffic?range=7d, logs/stats?from=<7d>, traffic?range=7d&host_id=<busiest host by 24 h volume>.

Probe Before (1.3.38.3) After (1.3.38.4)
traffic?range=7d 52.8 s (FAIL) 1.83 s (PASS)
logs/stats 7d 35.4 s (FAIL) 1.18 s (PASS)
traffic?range=7d&host_id=<busiest> not measured (would visit ~270k rows) 0.61 s (PASS)
logs/timeseries 7d hourly 27 s unbounded (PHASE 0) 0.74 s
panel-boot.sh regression PASS PASS (0.211 s; purge +120.0 s)
dashboard-latency.sh regression PASS latency PASS (health 0.019 s, overview 0.003 s); X-Argos-Cache: stale instead of hit on health, see below

Response markers on prod after deploy: traffic?range=7d -> detail_window "24h0m0s", detail_from = now-24 h, series_covers_range true, 169 hourly buckets, 25 response-time buckets (the 24 h detail), 10 hosts, 20 paths. logs/stats 7d -> total 500,020, sample_n 20,000, classes 2xx 373,935 / 3xx 46,348 / 4xx 56,193 / 5xx 1,432 / other 22,112.

Deploy 21:01:45Z -> 21:04:08Z (143 s), image sha256:7226b4bb... -> sha256:a82d138e..., healthy at +16 s, only the panel recreated. Peak load 7.0 (frontend build window, as attributed in v1.3.38.3).

Regression finding: the warm set can be stale by a hair

The regression run of dashboard-latency.sh (EXPECT_CACHE=hit) found /api/dashboard/health served as X-Argos-Cache: stale (age just over 30 s; still answered in 19 ms from memory with a background refresh started). Cause: the warm set is refreshed by a ticker every TTL (30 s) and the four refreshes run sequentially on the single SQLite connection; while the 7 d smoke was holding that connection for 1-2 s per probe the health refresh landed a little after its 30 s mark, so the next read saw an age of 30.x s. Nothing was slow and nothing was wrong for the user, but the guarantee "pinned = always a hit" has a window equal to the refresh duration. Fix proposed for the next release: tick the pinned refresh at ⅘ of the TTL (24 s) so a value is renewed before it can age past 30 s under normal contention. Until then the smoke's EXPECT_CACHE=hit is strict by design and can flag this.

Gates before commit: gofmt -l clean, go vet ./..., go test ./..., npm run build, check-no-personal-data.sh, mkdocs build --strict.

Files changed

  • backend/internal/dashboard/queries.go, types.go (+ long_range_test.go).
  • backend/internal/db/logs.go (+ logs_long_test.go).
  • frontend/src/api/client.ts, frontend/src/pages/Dashboard.tsx, frontend/src/pages/Logs.tsx.
  • scripts/smoke/range-7d-latency.sh, docs/reference/api.md, CHANGELOG.md, docs/release-notes/v1.3.38.4.md, mkdocs.yml, docs/operations/verification-report.md, backend/cmd/argos/main.go, frontend/package.json.