web/worker: add a process-level liveness heartbeat #80
No reviewers
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: Jinemi/thermograph#80
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/web-worker-heartbeat"
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?
ProdWebContainerSilentandProdWorkerContainerSilentfired repeatedly for ~7 hours on 2026-07-24/25 against a healthy prod. Root cause: both key on Docker container-stdout volume, which isn't a valid liveness signal for either role —web's real access logging lives in the file-tailedapp-jsonsource, not stdout, andworker's stdout is unconditionally dropped by Alloy's own relabel rule (its stdout is pure/healthz-poll noise by design).ProdWorkerContainerSilentin particular can never receive data, redeploy or not.observability/README.md's "Known gaps" section already names the fix: give each near-silent-stdout daemon a realaudit.log_heartbeat()beat, same pattern the subscription notifier already uses.What this does
backend/web/app.py: new_heartbeat_loop/_start_heartbeat, mirroringnotify.py's own_STOP/loop/start()shape. Wired into_lifespan()next to the existing notifier gate. Tags the beatdaemon=ROLE(web/worker/all), gated byTHERMOGRAPH_ENABLE_HEARTBEAT(default on).webreplica — a beat is a single local file append (no upstream quota, no DB, no lock), unlike the notifier's leader-election, which exists specifically to stop N workers hammering Open-Meteo/DB N-fold.workergets its own beat rather than relying solely on the notifier's existingdaemon="subscription-notifier"heartbeat, because the notifier thread only starts if it wins leader election — during that gap the container can be up and healthy with no notifier beat yet. The two together also give a differential signal: both silent → container down; only the notifier one silent → notifier/leader-election specifically wedged.THERMOGRAPH_ENABLE_HEARTBEAT=0added totests/conftest.pyanddocker-compose.test.yml, next to the existing notifier gate.tests/web/test_heartbeat.py.Not in this PR
The corresponding
observability/grafana/provisioning/alerting/rules-prod.ymlchange (newProdWebHeartbeatMissing/ProdWorkerHeartbeatMissingrules, pausing the two broken ones) ships separately, after confirming these beats are actually flowing on beta/prod — deploying the new rules before beats exist would make them immediately and correctly fire on "no data."Tests
pytest tests: 426 passed, 8 skipped, 2 pre-existing unrelated failures (missingduckdb, not touched by this change).