SCRUM-237 — Dashboard log search and live tail¶
Plan ref: DSH-C8/C9 (docs/11-admin-plane-plan.md). Stacked on SCRUM-236.
What exists¶
| Piece | Notes |
|---|---|
internal/loki |
source.LogSource over Loki's HTTP API: Query (query_range backward, streams merged newest first; JSON lines → fields, message from msg), Tail (polls every 1.5 s from the last timestamp, deduped at the boundary), Services (label values, cached 30 s, last good set on error). Timeout, 16 MiB cap, errors without URLs |
LogQL builder (loki.Build) |
the only place LogQL is written: {service="…"[,level="…"]} from validated values (service allow-listed + grammar, level ∈ debug/info/warn/error), contains and request_id as |= literal line filters quoted with Go quoting (which is how LogQL parses strings); newlines refused. An injection string in contains stays a literal |
GET /logs (viewer) |
service, level, contains, request_id, from, to, limit (default 1 h / 200, limit ≤ 1000, range ≤ 7 d); {"entries":[{at,service,level,message,fields?}]}; 400 on invalid input, 502 on Loki errors |
GET /logs/tail (viewer, SSE) |
caps 2 per staff user, 10 global → 429 too_many_tails before any stream bytes; write deadline cleared; : connected, data: <json> events, : keep-alive every 15 s; stops on disconnect; 1 h max (client reconnects). gateway_dev already marks this route Stream |
How to verify¶
Tests: golden LogQL, escaping and the injection literal; Loki fixtures (two streams
merged, JSON line fields, error body, label-value cache); tail delivers each line
once and stops on cancel; /logs validation and shape; SSE through a real
httptest.Server with viewer tokens: connected, events, 3rd tail by one user 429,
11th global 429, slot freed after disconnect, keep-alive seen.
Results at time of writing¶
gofmt,go vet,go test -race(9 packages): pass.
How it was built¶
DeepSeek run scoped (Landlock) to services/dashboard (307 s, ~55k output tokens,
reasoning effort low). Claude review: LogQL quoting, limiter and SSE lifecycle
checked; no changes needed.