I Fixed an Outage That Never Happened — When a Static Manifest Judges Runtime
It was the day I'd set aside to review monitoring. An ordinary sprint that began with "audit our observability stack." Metric scrape jobs, alert rules, Grafana dashboards, Loki logging — I read the manifests line by line.
Then I stopped at one line.
# infra/k3s/monitoring/alertmanager.yaml
route:
receiver: 'null' # every alert lands here
receiver: 'null'. In Alertmanager this means "take any alert that fires and throw it away, sending it nowhere." ServiceDown, OOMKilled, CircuitBreakerOpen — seventeen rules, all alive, and the delivery path was capped off at null. A comment said "Discord alerts were migrated to the Identity service webhook," but following the call graph showed that Identity webhook was for user feedback only. Nothing to do with alerts.
The conclusion looked obvious. Our system hasn't been able to send a single alert for days.
Problem
All seventeen alert rules were falling into a null receiver. If something broke, no alert would reach anyone — or so I was certain.
So I diagnosed it, and I fixed it. I wired a real Discord receiver into Alertmanager, changed route.receiver from null to discord, mounted a secret volume. I corrected the false comment too. CI was green, and it cleared the monitoring gate (BLOCKING). The PR merged.
I felt rather pleased with myself. I'd written in the ADR that I had "found a six-day alert blackout that had been silently swallowing notifications."
It took less than a day to learn that all of it was wrong.
The File I Was Reading Wasn't Deployed
The next day, the operator (still me, but this time the one actually attached to the server) diagnosed the live cluster. And one sentence brought the whole thing down.
The live Alertmanager is sending alerts to Discord just fine, though?
How could that be? The manifest I read clearly said receiver: 'null'.
The answer was in the deployment topology. The infra/k3s/ directory in the AlgoSu repo was a non-deployed reference mirror. The actual SSOT applied to the cluster lived in a separate repo, aether-gitops, and ArgoCD watched only that repo. On merge, AlgoSu's CI only bumped image tags into aether-gitops — it never propagated the manifests themselves.
The receiver: 'null' I read was a stale snapshot of a mirror that isn't even deployed. The live Alertmanager had been wired correctly months earlier (Sprint 130), running two receivers — discord-default and discord-critical — routing by severity, on v0.28.1.
What made it worse was my "fix." I had left v0.27.0 in the mirror and added a webhook_url_file approach — but v0.27.0 doesn't support file-based webhooks. In other words, had that mirror actually been the deployment, my fix would have been a broken config. I had diagnosed a "failure" in a healthy system and painted an unworkable setting on top of it.
What to Do With a Merged Misjudgment
A merged ADR was now recording a "nonexistent outage" as fact. My first impulse was to delete it.
I didn't. An ADR is the history of a decision, and a wrong decision is still history. Deleting the body would erase the context of "why did I judge it that way at the time." Instead I laid an ERRATA block at the top, leaving the body intact and overwriting only the facts. The next person (or agent) to read this ADR would see both the wrong conclusion and its correction at once.
I touched the mirror itself too. Rather than fully syncing it — keeping a mirror 100% aligned with live is more maintenance burden than value for a solo developer — I attached a "this is a non-deployed reference" banner and aligned only the broken parts with live. I demoted the mirror from "truth" to "reference."
Decision
Don't delete the wrong ADR — correct it with an ERRATA. Demote the mirror to a "reference" with a non-deployed banner, and align only the parts that diverge from live.
Oh, and the symptom that started this whole investigation — "logs don't seem to be getting collected." That one was real. Except the cause wasn't pipeline wiring; it was Loki running out of memory. A 512Mi limit couldn't survive a five-day log spike cycle and was being OOM-killed periodically. Raising the limit to 1Gi fixed it. The static config had been right all along. The problem was a runtime resource.
This Time I Looked at Live First
Lesson learned, I approached the next symptom differently. On Grafana's "Service Debug" dashboard, three panels (container CPU/memory, two Loki log panels) sat empty with no-data.
This time, instead of glaring at manifests, I queried the live cluster first. Two real causes surfaced.
(A) cAdvisor metrics weren't being scraped at all. container_cpu_usage_seconds_total was zero series across all namespaces. The Prometheus config had no scrape job for kubelet/cAdvisor. kube-state-metrics was running, but that only gives kube_* object state (how many pods are up) — not the containers' actual resource usage. They are different data.
(B) The Loki label model didn't match the dashboard. Logs were being collected fine (6 streams under {namespace="algosu"}). But the live promtail attaches a service label, while the dashboard was querying a pod label that didn't exist. On top of that, the variable value was submission-service while the actual label value was submission — not even a prefix match.
The order was reversed this time. I fixed live first in the deployment SSOT (aether-gitops), verified that container metrics came back from zero to 75 series, and only then aligned the non-deployed mirror to the same shape. Fix live first, reconcile the mirror after. That was the right direction for handling drift.
Having a Rule Is Not the Same as Delivering an Alert
With the dashboard fixed, I turned to the alert rules themselves. And the lesson from the first incident — "a rule existing doesn't mean an alert gets sent" — repeated once more, in a subtler form.
The CircuitBreakerOpen alert was only watching the circuit breaker of the ai-analysis service. submission and github-worker emit the same circuit breaker metric, but there was no alert. A blocked Saga or a severed GitHub sync would have gone unannounced. I consolidated all three services into one rule.
More interesting was the DLQReceived alert. It was watching a metric called algosu_submission_dlq_messages_total —
# searching the entire submission source for code emitting this metric
$ grep -r "submission_dlq_messages_total" services/submission/src
# (no results)
This metric was emitted nowhere. submission is a Saga orchestrator and doesn't emit a DLQ counter. DLQs happen in the consumer workers (github-worker, ai-analysis), and their metrics did exist. The rule was alive but the metric it pointed to was dead — an alert that gave a false sense of safety. On the dashboard it looked like "DLQ is being monitored," but it would never fire.
I replaced the unemitted placeholder with the real, emitted worker metric. In the process, Critic (the cross-review AI) caught two more things: comparing a counter as a raw > 0 value fires forever after the first event (→ use increase(...[5m]) > 0 for "recent events only"), and because prom-client lazily creates a label series on the first .inc(), increase has no baseline to compare against and misses the first DLQ (→ initialize the labels to 0 at process start). For rare critical events like DLQs, this detail is the difference between catching it and not.
Finally, I Sent Alerts to Their Own Channel
The review had one last gap. Operational alerts were reusing the user-feedback Discord webhook. Critical outage alerts, operational warnings, and user feedback were all pouring into the same "feedback bot" channel. The surest way to bury a critical in noise.
So I separated the channels: #algosu-alerts (default) and #algosu-alerts-critical (urgent), each with its own webhook. This was live cluster work, so this time the order of application mattered.
And I verified to the end. alertmanager_notifications_total{integration="discord"} rose from 0 to 2, and to 4 with the resolve, while failed_total stayed at 0. The metrics proved delivery succeeded — but metrics can't distinguish which channel it arrived in. So finally, I looked at the two Discord channels with my own eyes. Critical in the critical channel, warning in the default channel, no crossover. That's where verification ended.
Result
From fixing an outage that didn't exist, to finding the real cause live, to sending alerts to their own channel. The cluster was the truth — not the manifest.
What Six Sprints Taught Me
- S231Misread a non-deployed mirror as the deployment, diagnosed a nonexistent outage완료
The Audit — and the Misjudgment
- S232Confirmed infra/k3s ≠ deployment SSOT, demoted the mirror + Loki OOM as the real cause완료
The Correction — ERRATA
- S234Missing cAdvisor scrape + promtail label mismatch, fix-live-then-reconcile-mirror완료
The Real Diagnosis — Live First
- S235CB unified across 3 services + DLQ placeholder replaced + channel-split design완료
Closing Alert Gaps
- S236critical/default two channels + eyeball verification of both arrivals완료
Channel Split, Live
Four principles kept recurring across this journey.
(i) A correct static config is not the same as a correct runtime
The first incident's alerting, and the missing logs, both turned out to have correct static manifests. Alert routing was healthy live; the logging pipeline was wired correctly. The real problems lived in runtime — I'd read a mirror that wasn't deployed (a nonexistent defect), or Loki was dying of OOM (a resource issue). These are the kinds of problems no amount of staring at manifests will reveal.
(ii) Confirm which manifest is deployed, first
The root of this whole mess was a single assumption: "this file I'm reading is the file that runs." The infra/k3s/ inside the monorepo looked so plausible that I never suspected it had been superseded by a separate GitOps repo. Draw the deployment topology first. What is the SSOT, what does CI propagate, what does ArgoCD watch.
(iii) The existence of a rule is not evidence of behavior
A null receiver silently swallows alerts — which is exactly why one line made me leap to a "blackout" conclusion (luckily, that one was a non-deployed mirror). And in the part that was deployed, a DLQ alert pointing at an unemitted metric would never fire. Both look like "monitoring" on a dashboard. An alert isn't done when the rule exists — you have to follow it all the way through: fire → route → send → arrive.
(iv) Fix live first, reconcile the mirror after
There was a right direction for handling drift. Fixing a non-deployed mirror first leaves the distance to live unchanged. You fix the deployment SSOT while verifying it live, then let the mirror follow into that confirmed shape. The mirror isn't the truth — it's the shadow of the truth.
The receiver: 'null' I read that day wasn't a lie. The file really did say that. What was wrong wasn't the file — it was me, for believing the file was live.
A manifest is a written intention. A cluster is the result of that intention being executed. Nothing guarantees the two are the same — especially when a whole GitOps repo sits between them.
Now, before I pass judgment on runtime, I ask first: "Is this actually the thing that's running?"
Related ADRs
Decision records behind this post
Full Monitoring System Audit + Log Collection Diagnosis + Alertmanager Discord Receiver Wiring
Audited the full AlgoSu observability stack (8 metric scrape jobs, 17 alert rules + 2 recording rules, 4 Grafana dashboards, Loki+Promtail logging, 8-dimension validation CI) and diagnosed the user's concern of 'service/error logs not being collected' by tracing the pipeline end to end. Conclusion: the logging pipeline is correctly wired at the static-config level (Promtail CRI+JSON → loki:3100; NetworkPolicy loki-ingress allows it and default-deny is Ingress-only so egress is unrestricted; Grafana loki uid consistent; level value lowercase-consistent across all services) → if logs are truly missing it is a runtime cause requiring live diagnosis (operator side). The real defect is the alert delivery path: alertmanager receiver='null' silently dropped all 17 rules, and the comment claiming 'switched to Identity webhook' was false (Identity discord is feedback-only). Per user decision, wired a real Alertmanager Discord receiver (discord_configs.webhook_url_file) + added ALERTMANAGER_DISCORD_WEBHOOK placeholder to monitoring-secrets (real seal is operator side). Fixed the Grafana Error Logs Only query's dead branches (level=~error|fatal|CRITICAL — no service ever emits fatal/CRITICAL) + fragile line filter into a direct level label filter. Fact-checked speculative gaps: github-worker-metrics Service exists (scrape valid), Identity discord=feedback, .bak files were git-untracked. Deliverable: docs/runbook/monitoring-system-audit.md (inventory + coverage matrix + live diagnosis procedure + gaps/debug/seeds). Verification: check-grafana-metrics.mjs passes.
sprint-232Alertmanager Mirror Drift Correction + Sprint 231 Documentation ERRATA (infra/k3s ≠ deployment SSOT)
Sprint 231's central conclusion ('alertmanager receiver:null → all 17 rules dropped / 6 days no alerts') was proven wrong by server-side live diagnosis. Root cause: AlgoSu infra/k3s/ is a non-deployed reference mirror; the actual deployment SSOT is aether-gitops (algosu/base/monitoring + overlays/prod). AlgoSu CI only bumps image tags in aether-gitops and does not propagate manifests, and ArgoCD (automated, selfHeal, Synced) only watches aether-gitops. Sprint 231 misread this non-deployed mirror's stale snapshot (receiver:null) as the deployed config. Live alertmanager was already healthy via Sprint 130 B-1 (alertmanager-native discord-default + discord-critical, identity-discord-secret/webhook-url, v0.28.1). Worse, the 'fix' Sprint 231 added to the mirror (monitoring-secrets/discord_webhook + v0.27.0 + webhook_url_file) was non-functional since v0.27.0 does not support file-based discord webhooks. The actual 'log non-collection' cause was Loki OOM (512Mi limit, ~5-day spike), fixed and verified via aether-gitops PR #7 (512Mi→1Gi) (C). This sprint: A-lite aligns the AlgoSu alertmanager mirror to live (v0.28.1 + discord-default/critical + identity-discord-secret/webhook-url + non-deployed banner) and removes the sealed-secrets placeholder; B adds ERRATA to the Sprint 231 ADR/runbook; B+ seeds the loki probe/securityContext live gap. Deployment-neutral (mirror/doc integrity restoration). Critic R1 CLEAN. Lesson: do not assert runtime defects from static manifests (especially a non-deployed mirror) (feedback-source-vs-live-drift).
sprint-234Monitoring Drift Cleanup (Grafana no-data + argocd metrics + promtail/loki SSOT alignment)
Three panels (Container CPU/Memory, two Loki log panels) of the live Grafana 'AlgoSu Service Debug' dashboard showed no-data. Live diagnosis confirmed two root causes: (A) Prometheus had no kubelet/cAdvisor scrape_config → container_* metrics entirely uncollected (0 series). (B) Live promtail emits a service label while the dashboard referenced a nonexistent pod label → log panels always 0. Fixed live first via aether-gitops PR #8 (+0a7156d): merged, ArgoCD Synced, live 3-way verification passed (container_* 75 series, log panels streaming). This sprint aligns the non-deployed reference mirror infra/k3s/monitoring to the final live form: prometheus-config gains kubernetes-cadvisor/nodes jobs (via apiserver proxy) + SA/ClusterRole/CRB, dashboard $service variable values unified to the Loki service label, Loki panels use service= selectors, job=~ at 12 sites, and promtail aligned to emit the service label (Critic R1 P2). Gate check-grafana-metrics passes. Critic R1 [P2] (mirror promtail missing service label) → R2 CLEAN. During the sprint the same monitoring-drift cleanup expanded: AlgoSu #414 (argocd-metrics/argocd-server-metrics scrape jobs + revisionHistoryLimit 10→5) and #415 (promtail kubernetes_sd→static_configs full SSOT replacement + loki allow_structured_metadata false→true paired alignment), plus server aether-gitops #8 (live no-data), #9 (CB panel stale fix + revisionHistoryLimit, ReplicaSet 126→73), #10 (cloudflared-monitoring GitOps adoption), and curl-test pod cleanup, completed in parallel.
sprint-235Monitoring Alert System Gap Hardening (CB/DLQ alert coverage + alert channel separation + on-call runbook)
A full monitoring-system audit + alert-system review surfaced four gaps, each hardened. (1) CircuitBreakerOpen alert watched only ai-analysis → unified with submission/github-worker (all three CB metrics exist; rule expanded only). (2) DLQReceived alert referenced the never-emitted placeholder algosu_submission_dlq_messages_total → fact-check confirmed submission is a saga orchestrator that emits no DLQ counter, so it was replaced with the actually-emitted worker metrics algosu_(github_worker|ai_analysis)_dlq_messages_total. (3) Operational alerts reused identity-discord-secret (the feedback-only webhook), mixing into the same Discord channel → split into critical/general two channels via alertmanager-discord-secret (2 keys webhook-url/webhook-url-critical); the mirror is pre-staged as the target state (real sealing/apply is server-side aether-gitops). (4) No on-call runbook → added oncall-alerts.md (per-alert diagnosis queries and response for all 13 alerts) + alert-channel-separation.md (live apply procedure for channel split). infra/k3s/monitoring is a non-deployed reference mirror — live-apply items go to runbook + carry-over. Gate check-grafana-metrics all dimensions [OK], check-doc-refs 437 files no broken refs.
sprint-236Operational alert channel critical/general split — live apply (alertmanager-discord-secret)
Live-applied to server-side aether-gitops the operational alert channel split that Sprint 235 had pre-staged as the 'target state' in the mirror. Previously alertmanager reused identity-discord-secret (the feedback-only webhook), so user feedback, operational warning, and critical all mixed into one channel → split via a new alertmanager-discord-secret (2 keys: webhook-url=general, webhook-url-critical=critical). To avoid an optional:false mount failure, applied as a 2-commit split (SealedSecret first → secret-existence gate → alertmanager manifest swap). End-to-end verification: 2-key mount, config alignment, critical/warning firing delivery (notifications_total 0→2→4, failed 0), resolve delivery, feedback isolation all passed, and the PM visually confirmed arrival in the two real Discord channels. identity-discord-secret is kept for feedback only (do not delete).
ADR-029