feat(obs): wire the gate-sidecar scrape, and fix the alert that would have gone silent doing it
PR #28 gave the five fleet oauth2-proxy sidecars --metrics-address=0.0.0.0:44180 and a named `metrics` port. This wires a scraper to them and, in the process, found a real bug in the alert this branch added. PodMonitor, not ServiceMonitor. Measured: no Service exposes 44180 on any of the five (jackett/radarr/sonarr/qbittorrent/pihole all publish only their app port + authproxy 4180), so a ServiceMonitor would select nothing. A PodMonitor targets pod ports directly and needs no change to catalog/render.py or the apps/wave-* renders and goldens. Selector is the pod label homelab.keithjackson.dev/auth-mode=forward-auth — present on exactly those five gate pods, absent from crafty's, which still has no metrics listener. The property, not an app list. THE BUG: `up{container="oauth2-proxy"}` -> `up{container="oauth2-proxy"} == 1`. A target that is discovered but FAILING still publishes `up`, with value 0. The series exists, so count by (namespace) returned 1, `unless` removed the namespace, and OAuth2ProxyGateUnobserved went QUIET while nothing was being scraped at all — this branch's own disease reproduced inside the alert written to cure it. Not hypothetical: the PodMonitor lands five targets whose scrape is dropped, so the bare form would have silently cleared on this very commit. Proven by mutation: reverting the `== 1` fails the two new unit-test scenarios (got: nil) and leaves every other assertion green. The scrape does NOT work yet, and that is documented rather than papered over. From a pod in ns monitoring to the jackett pod, :44180 AND :4180 both time out; the shared proxy's :44180 answers. ns jackett's default-deny is podSelector:{} with policyTypes:[Ingress,Egress] and NO ingress key (deny-all), and the one allowance is fromEntities:[ingress] to 4180 — the Gateway's Envoy only. The endpoint itself is healthy: port-forwarded, /metrics returns http=200, 10208 bytes, all three oauth2_proxy_* families including requests_total{code="302"}. Same bug and fingerprint as platform/longhorn/networkpolicy-metrics.yaml records for Longhorn; the fix is one narrow rule per app, and it cannot ship from this tier because oauth2-proxy and apps-wave-0 are Flux SIBLINGS (both dependsOn platform-config), so naming ns jackett here would apply today and fail on a cold rebuild. Needs an owner in the renderer. Touches conformance/** : one KNOWN_EXTERNAL_MONITORS entry, which is the mechanism the failing check explicitly instructs ("DECLARE it ... with who creates it") and which flux-podmonitor.yaml already uses. It records that the port name was measured on prod instead of resolved from a Service, and that the targets are currently down. That makes this PR require a non-author review on the sensitive-path rule. Also: ratio alert still NOT shipped. oauth2_proxy_requests_total now exists on the endpoint but not in the TSDB, so writing it today would make it inert for exactly the reason it was refused on 2026-09-30. Runbook gains policy-vs-no-target triage and the radarr /signalr/messages/negotiate worked case — one orphaned browser tab, 206 identical 302s, nobody ever re-prompted. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
1 parent
ba79a1b59b
commit
bce101d03f
9 files changed
+446
-24
No files matched your search
@@ -173,3 +173,87 @@ Everything below was measured against live prod through the pinned container, re
|
||||
3. **Consider whether `--set-xauthrequest` apps should get `--api-routes`** so API paths answer
|
||||
401 instead of 302. That is the fix that would make this failure *loud* rather than merely
|
||||
observable, and it belongs with whoever owns the sidecar args.
|
||||
|
||||
---
|
||||
|
||||
# Round 2 — 2026-10-01: the scrape got wired, and found a bug in this branch's own alert
|
||||
|
||||
Added to the same PR (#29) rather than a fourth concurrent branch. PR #28 merged in the meantime
|
||||
and `main` moved three PRs ahead; `origin/main` is merged in here, and the `runbooks/kustomization.yaml`
|
||||
conflict was resolved by **keeping both** runbook entries (mine and `oauth2-proxy-session`).
|
||||
|
||||
## What changed
|
||||
|
||||
**`platform/oauth2-proxy/podmonitor.yaml` (new).** PR #28 gave the five fleet sidecars
|
||||
`--metrics-address=0.0.0.0:44180` and a named `metrics` port. No Service exposes 44180 on any of
|
||||
them, so a ServiceMonitor would have nothing to select — a **PodMonitor** targets pod ports directly
|
||||
and needs no renderer change. Selects on the pod label `homelab.keithjackson.dev/auth-mode:
|
||||
forward-auth`, which is on exactly the five fleet gate pods and absent from crafty's: the property,
|
||||
not an app list, so a sixth forward-auth app needs no edit.
|
||||
|
||||
**`auth-visibility-alerts.yaml`: `up{container="oauth2-proxy"}` → `up{container="oauth2-proxy"} == 1`.
|
||||
This is a real bug fix, and it is the most important thing in this round.** A target that is
|
||||
discovered but failing still publishes `up`, with value **0**. The series exists, so
|
||||
`count by (namespace)` returned 1, `unless` removed the namespace, and `OAuth2ProxyGateUnobserved`
|
||||
would have **gone quiet while nothing was being scraped at all** — this branch's own disease,
|
||||
reproduced inside the alert written to cure it. Not hypothetical: the PodMonitor lands five targets
|
||||
whose scrape is dropped, so the bare form would have silently cleared on this very commit.
|
||||
|
||||
**Two new unit-test scenarios** pinning that: a discovered-but-failing target must still fire, and a
|
||||
target that works and then breaks must re-fire rather than latch cleared.
|
||||
|
||||
**Docs corrected rather than appended to.** The 2026-09-30 claim "the sidecars export no metrics at
|
||||
all" is now only half true and was rewritten in `runbooks/README.md`, `platform/oauth2-proxy/README.md`
|
||||
and the alert header, each showing the two-step history, because each step looked like the end of it.
|
||||
The runbook gains the policy-vs-no-target triage (step 1b) and the **radarr `/signalr` worked case**.
|
||||
|
||||
**Deliberately NOT shipped: the ratio alert.** `oauth2_proxy_requests_total` now exists on the
|
||||
endpoint, but not in the TSDB, because the scrape is blocked. Writing the ratio rule today would make
|
||||
it inert for precisely the reason it was refused on 2026-09-30. It becomes writable the day a scrape
|
||||
succeeds, not the day a listener exists.
|
||||
|
||||
## What I verified
|
||||
|
||||
- **The endpoint serves — by payload, not by flag.** Port-forwarded to the jackett pod's 44180:
|
||||
`http=200 bytes=10208`, exporting `oauth2_proxy_requests_in_flight`, `oauth2_proxy_requests_total`
|
||||
(`code="200" 3`, `code="302" 2`) and `oauth2_proxy_response_duration_seconds`. `--metrics-address`
|
||||
being in `args` and `/metrics` returning a body are different claims; both now hold.
|
||||
- **The scrape path is DROPPED, measured from the right place.** From grafana in ns `monitoring`
|
||||
(beside Prometheus, same policy position) to the jackett pod IP: `:44180` → `wget: download timed
|
||||
out`; `:4180` → **also** timed out; the shared proxy's `:44180` → 200 with `oauth2_proxy_*` present.
|
||||
Cause, read from the objects: ns jackett's `default-deny` is `podSelector: {}`,
|
||||
`policyTypes: [Ingress, Egress]`, with an `egress` rule and **no `ingress` key** (deny-all ingress);
|
||||
the only allowance is CNP `jackett-allow-gateway-ingress`, `fromEntities: [ingress]` to port 4180.
|
||||
The shared proxy is scrapable only because ns `oauth2-proxy` has **no NetworkPolicy at all**.
|
||||
- **No Service exposes 44180** on any of the five (enumerated).
|
||||
- **The pod label selector is exact:** `auth-mode=forward-auth` on all five fleet gate pods, absent
|
||||
from crafty's. Crafty's sidecar still has no `--metrics-address` (args read in full).
|
||||
- **Flux ordering forbids shipping the fix from here:** `oauth2-proxy dependsOn=['platform-config']`
|
||||
and `apps-wave-0 dependsOn=['platform-config']` — siblings, no ordering. A NetworkPolicy naming ns
|
||||
`jackett` from this tier would apply today and fail on a cold rebuild.
|
||||
- **The `== 1` guard is non-vacuous — proven by mutation.** Reverting it to the bare `up` makes the
|
||||
two new scenarios FAIL (`got: nil` at 1h5m and 2h5m) and leaves every other assertion passing;
|
||||
restoring it returns the suite to SUCCESS under pinned promtool v3.14.0.
|
||||
- **Gate:** see the next section for the exit code and skips.
|
||||
|
||||
## What I did NOT verify
|
||||
|
||||
- **The PodMonitor has never been applied.** That it discovers the five pods is reasoned from the
|
||||
measured pod labels and port name, not observed — no target list has been read with it in place,
|
||||
because applying it is an operator action. **Expect `up == 0` on all five when it lands**, which is
|
||||
the documented state, not a regression.
|
||||
- **I did not fix the NetworkPolicy, and this PR therefore does not make the gates observable.** It
|
||||
makes them *discovered*, and makes the remaining gap loud instead of silent. The one narrow rule
|
||||
per app (ns monitoring → `auth-mode=forward-auth` → TCP 44180, per
|
||||
`platform/longhorn/networkpolicy-metrics.yaml`) has to be rendered with each app's own policy, in
|
||||
the catalog/renderer that is in flight on another branch. **It needs an owner.**
|
||||
- **Whether Prometheus will actually select a PodMonitor in ns `oauth2-proxy` targeting other
|
||||
namespaces.** `namespaceSelector.any: true` plus `podMonitorSelectorNilUsesHelmValues: false` and
|
||||
the fact that cross-namespace ServiceMonitors/PrometheusRules already work here say yes; it is not
|
||||
measured with this object.
|
||||
- **No session-lifecycle claim can be made from the current window.** `AuthSuccess` is **0** on both
|
||||
jackett and radarr since the PR #28 restart, so nobody has signed in and `--cookie-refresh=2m30s`
|
||||
has never executed a refresh. The cookie fix is unproven in the one dimension that matters most.
|
||||
- **The radarr `/signalr` finding is the coordinator's measurement, not mine** — I reproduced neither
|
||||
the 206/209 sample nor the jackett 0-in-13,612. They are recorded as reported.
|
||||
- **L2/L3 conformance still did not run** (`CLUSTER_AVAILABLE` unset). Nothing is certified.
|
||||
Reference in new issue
Block a user