feat(web-arm): reptide crash-signal prerequisites — 90d stream retention, push-only Loki vhost, error-rate alert #325
No reviewers
Labels
No labels
bug
enhancement
in-progress
needs-info
needs-triage
p0
ready-for-agent
ready-for-human
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/nixos!325
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "afk/323"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Prerequisites from this repo for reptide's crash-signal pipeline (Flutter → Supabase
log-event→ Loki). Two commits, per the issue.1.
hosts/web-arm/modules/loki.nix90d retention for reptide, 48h for everyone else — a per-stream override on
{app="reptide"}(2160h, priority 1) rather than a change to the global period. The compactor already hadretention_enabled = true.The public vhost becomes push-only. Both fleet pushers (
utils/modules/alloy/config.alloy,ansible/roles/base/defaults/main.yml) and reptide's edge function only ever POST to/loki/api/v1/push; Grafana queries Loki over localhost. So the vhost now exposes an exact-match push location behind the existing basic auth, an open/ready, and denies everything else with 403. That closes the read hole for any leaked pusher credential — the fleet's or reptide's.Certificate renewal is unaffected:
security.acme.defaults.dnsProvider = "hetzner"(DNS-01), so no HTTP-01 webroot path passes through the vhost.2.
hosts/web-arm/modules/grafana/alerting/reptide/Error-rate alert per MANUAL_SETUP.md §8: 5m error rate for
{app="reptide", env="production", level="error"}above 3× the same window 24h earlier, sustained 10m, routed by the existing default policy tocp_dominik_normal(the Matrix room in the doc is reptide-side wiring, not available here). No dashboard — Explore covers early triage until a real incident shows what is worth freezing into Nix.One deliberate deviation from the literal spec, worth a look: both queries end in
or vector(0). Without it, a silent 24h baseline returns an empty vector, the math step degrades to NoData, and the rule stays mute in exactly the case that matters — a healthy app that suddenly starts crashing. The trade-off is that a zero baseline makes the threshold zero, so after a quiet 24h any error sustained over 10m fires. Given this is a crash signal that seems right, but it is a real behaviour change from "3× baseline" and easy to revert if it turns out noisy.Verification
Nix is not installed in the AFK sandbox, so I bootstrapped a portable Nix (third-party binary,
DavHau/nix-portablev012, into$HOMEoutside the repo) to get the repo's build gate working. Everything below was run against packages from the host's own channel,nixos-26.05.Dry-build:
scripts/test-configuration web-armvia the pre-commit hook →:: web-arm OK, zero evaluation errors.Retention, against a real Loki 3.7.4 — the rendered config was started locally, and its own
GET /configreads back:2160h → 90dand48h → 2dare Go duration round-trips, so the values were parsed, not echoed. Three falsification controls confirm the parser is strict rather than tolerant:retention_streamm→field not found in type validation.plain;selectorr→field not found in type validation.StreamRetention; malformed selector{app="reptide"→invalid labels matchers: parse error at line 1, col 15. All exit 1. Loki LogQL-parses the selector at config-validation time.LogQL, same instance: with no data,
sum(rate(...)) or vector(0)returns a sample of0where the bare query returns[]— the behaviour the alert depends on. After pushing 5level="error"lines, query A =0.01666…(exactly 5/300); 3 additionallevel="info"lines did not move it (they landed —level="info"alone = 0.01, no level matcher = 0.02666…). With A = 0.01666… and B = 0,$C > 3 * $Dis true; on total silence0 > 3*0is false, soor vector(0)does not manufacture a false positive.nginx, real nginx 1.30.4 from the same channel, serving the three rendered location blocks verbatim against a stub upstream — all 11 rows pass:
POST /loki/api/v1/pushno credsPOST /loki/api/v1/pushvalid credsGET /readyno credsGET /loki/api/v1/query_rangeno creds / valid credsGET /loki/api/v1/labelsvalid credsGET /config,GET /metrics(± creds)GET /POST /loki/api/v1/push/(trailing slash) valid credsPOST /loki/api/v1/push?x=1valid credsGET /ready/(trailing slash)The stub upstream's own log confirms only the authenticated push,
/ready, and the query-string push ever reached it. Note row 5: valid credentials do not buy query access — that is the point of the change.Grafana provisioning: the rendered
rules.yamlcontains all five groups (System 5, Service 11, Website 45, Storage 8, Reptide 1 — the existing four are intact), 70 rules with 70 unique UIDs, andreptide-error-rate-spike-uidappears exactly once, soalert-cleanup.nixwill not reap it./readyreturning 503: does not reproduce.https://loki.cloonar.com/readyreturned200 readyon 6 consecutive probes before the change. Loki serves 503 there only while the ingester is warming up, which fits a transient observation shortly after a deploy.loki.cloonar.comshould stay on the blackbox blacklist regardless — after this changeGET /returns 403, which a plainhttp_200probe would read as down.Not verified here
Coredumpsruler alert intact) can only run after this merges and bento pulls.queryType: instantwith these expressions is verified only at the LogQL level.Operator steps (not doable from here — secrets are yours to edit)
reptide-prodhtpasswd line to the existingpromtail-nginx-passwordsecret (it already holds the fleet'spromtail@cloonar.comline; nginx reads multiple lines from one file): No new secret and no deploy ordering constraint — this can land before or after the merge.supabase secrets set LOKI_PUSH_URL="https://loki.cloonar.com/loki/api/v1/push" LOKI_USERNAME="reptide-prod" LOKI_PASSWORD=… LOKI_ENV="production", then deploy thelog-eventedge function.{app="reptide"}shows up in Grafana Explore.Closes #323
Reptide's crash events need 90d (the app's privacy doc allows <= 90d); fleet journals stay at 48h, so the retention is a per-stream override on {app="reptide"} rather than a change to the global period. Every automated consumer of loki.cloonar.com only ever pushes (fleet Alloy, ansible hosts, reptide's Supabase edge function) and Grafana queries over localhost, so the public vhost no longer needs to expose the query API. It now serves an exact-match push endpoint behind the existing basic auth plus an open /ready, and denies everything else -- which closes the read hole for any leaked pusher credential, reptide's or the fleet's. Refs #323Fires when the 5m error rate for {app="reptide", env="production"} exceeds 3x the same window 24h earlier, sustained for 10m, and routes through the default policy to cp_dominik_normal. Both Loki queries end in `or vector(0)`: without it a silent baseline returns an empty vector, the math step degrades to NoData and the rule stays mute in exactly the case that matters -- a healthy app that suddenly starts crashing. The trade-off is that a zero baseline makes the threshold zero, so after a quiet 24h any error sustained over 10m fires. No dashboard: Explore covers early triage until a real incident shows what is worth freezing into Nix. Refs #323[autoland] verdict: pass
CONCERNS (non-blocking) - landable. No PR checks exist (aggregate: none), so I ran the repo gate myself: scripts/test-configuration web-arm - dry-build OK. Verified against the rendered config, not the PR body: nginx emits exactly '= /loki/api/v1/push' (basic auth), '= /ready', and 'location / { return 403; }' - exact matches win over the prefix catch-all; DNS-01 ACME (lego.nix:11) so renewal is unaffected. Grafana renders all 5 groups, 70 rules, 70 unique UIDs, reptide-error-rate-spike-uid once, and alert-cleanup.nix derives its manifest from the same list so it will not reap it. Both repo pushers (alloy config.alloy:114, ansible main.yml:76) POST to exactly that path; the domain is already blackbox-blacklisted. Title and 'Closes #323' conform; diff is confined to the issue's four files.
CONCERN: loki.nix:77-80 (pre-existing, untouched) still sets table_manager.retention_deletes_enabled with retention_period 48h. Table-manager retention is table-granular and ignores per-stream overrides, so if it is live it would cap reptide at 48h and defeat the 90d goal. Very likely inert (table-manager is not in Loki 3.x's 'all' target), but the PR correctly notes retention enforcement was never proven - worth confirming once data crosses 48h. Not a reason to hold the merge.
Noted, not blocking: the 'or vector(0)' deviation degenerates the threshold to '> 0' after a quiet 24h (author flagged it, one-line revert); the annotation says '3x the same window 24h ago' but query B is a 24h-averaged baseline - the code matches the issue spec, only the prose is loose; the push location drops proxyWebsockets, so nginx now talks HTTP/1.0 upstream, harmless for buffered POSTs.
Operator steps (reptide-prod htpasswd line, Supabase secrets) remain outstanding and are not merge-blocking per the issue.