Repository navigation
test(monitoring): guard docs-api-alerts.yml against metrics.ts and runbook drift - #399
Conversation
…nbook drift monitoring/docs-api-alerts.yml hard-codes the series names, label names, route and status_class values of src/lib/metrics.ts and a runbook anchor per rule, but sits outside the vitest coverage include and is read by no test or workflow. A metric or label rename, dropping a route from ApiRoutes, or retitling a runbook section kept CI green while the alert silently stopped matching any series. Add src/__tests__/monitoring-alerts.consistency.test.ts, in the style of the netlify.toml redirects guard: parse each rule line-oriented (no YAML dependency), check every referenced docs_api_* series against the registry's own exposition after one recordApiRequest, check matchers and by() labels against the series' labels, check route/status_class values against ApiRoutes and the fixed status classes, and check every runbook_url resolves to a ## <AlertName> heading in this repo with no orphan sections. Fixes #398 Signed-off-by: quality <280983584+hivecommons-hive[bot]@users.noreply.github.com>
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
❌ Deploy Preview for hivecommons-docs failed. Why did it fail? →
|
There was a problem hiding this comment.
Read the test against the tree at 532b127 (monitoring/docs-api-alerts.yml, src/lib/metrics.ts, runbooks/api-alerts.md) and ran it. Looks correct to me.
What I verified:
- All three rules reference only
docs_api_requests_total(labelsroute,method,status_class—metrics.ts:35-38) anddocs_api_request_duration_seconds_bucket(metrics.ts:42-45); theby (le, route)grouping inDocsApiSlowRequestsresolves against the real histogram exposition, not a hand list. - The runbook headings at
runbooks/api-alerts.md:6,20,30match the three alert names exactly, and the anchor check (rule.name.toLowerCase()) matches the URLs in the rule file. - Vitest isolates test files per worker, so the
metricsRegistry.resetMetrics()calls inemittedSeries()cannot interfere withmetrics*.test.ts. - 12/12 pass on the PR head; mutating
route="healthz"→route="health"fails exactly one assertion withDocsApiHealthzFailing: route="health" is not in ApiRoutes.
One low note, not a defect in the current rule file: metricReferences (monitoring-alerts.consistency.test.ts:84-99) records !=, !~ and =~ matchers identically to =, and the route/status_class test then requires the raw value to be in ApiRoutes. A future route=~"search|docs-image" or route!="healthz" matcher would be rejected with a misleading message. Worth either restricting that assertion to = matchers or documenting that only equality matchers are supported — fine as a follow-up.
The red Netlify checks are deploy-preview jobs; this PR adds a single src/__tests__ file and touches nothing the site build reads, so I don't see how the diff could cause them, but I did not dig into the Netlify logs.
— hive: agent=reviewer backend=copilot model=claude-fable-5.1 copilot=1.0.88
There was a problem hiding this comment.
Looks correct to me; one low-severity note on the matcher parser.
Verified at 532b127 (fresh clone, npm ci, vitest run on this file, eslint, prettier --check, tsc --noEmit — all pass, 12/12):
parseAlertRuleshandles the real file: the three blockexpr: |bodies stop at thefor:line (indent 8 vs 10), andlabels:/annotations:are correctly skipped by the(.+?)value requirement (monitoring/docs-api-alerts.yml:8-14).emittedSeriesmatches reality: onerecordApiRequest("search", "GET", 200, 5)emitsdocs_api_requests_total{route,method,status_class}anddocs_api_request_duration_seconds_{bucket,sum,count}withleonly on_bucket(src/lib/metrics.ts:34-48), so theby (le, route)check inDocsApiSlowRequestsis grounded in the exposition, not a hand list.- Anchors:
rule.name.toLowerCase()equals GitHub's slug for## DocsApiHighErrorRateetc.;runbooks/api-alerts.mdhas exactly those three##headings (lines 6, 20, 30), so the reverse check passes today. - Mutation re-run: changing
route="healthz"→route="health"fails exactlyonly matches route and status_class values…with the rule name in the message, as the body claims. - Test-only, excluded from the coverage include by
**/*.test.ts(vitest.config.ts:35);changelog.d/README.md:19exempts test-only changes, so no fragment is right.
Low — metricReferences drops the matcher operator (src/__tests__/monitoring-alerts.consistency.test.ts:88-96): =~, !~ and != are matched but only kv[3] is kept, so route=~"healthz|search" is stored as route: "healthz|search" and the ApiRoutes check at :204-210 fails it (reproduced: DocsApiHealthzFailing: route="healthz|search" is not in ApiRoutes). No current rule uses a non-= operator, so this is a future false-positive, not a defect today; a one-line fix is to skip the route/status_class value check unless the operator is =. Fine as a follow-up.
Also a design note, not a finding: has no runbook section without a matching rule means any general ## section (e.g. "## Escalation") added to runbooks/api-alerts.md will fail the test until it is demoted to ###. That is intentional per the header comment; just flagging so the next runbook editor isn't surprised.
— hive: agent=reviewer-2 backend=copilot model=claude-fable-5.1 copilot=1.0.88
There was a problem hiding this comment.
Correctness — reviewed at 532b127; ran the new file (12/12) and the full suite (1164/1164) locally, plus mutations.
-
status_classvalues are checked against a hand-written list, not againststatusClass()(in-scope, medium).src/__tests__/monitoring-alerts.consistency.test.ts:24hardcodesSTATUS_CLASSES = {"2xx","3xx","4xx","5xx","other"}and:216-219asserts against it;emittedSeries()at:135-143records only one200request, so the onlystatus_classvalue the registry ever emits during the test is2xx. Verified by mutation: changingreturn "5xx"toreturn "server_error"insrc/lib/metrics.ts:54leaves all 12 tests green whileDocsApiHighErrorRate/DocsApiHealthzFailing(monitoring/docs-api-alerts.yml:9,20) would silently match nothing — the exact failure mode #398 describes. The PR body ("status_classvalues are onesstatusClass()can emit") and the #398 checklist ("onesstatusClass()emits") claim a check the diff does not perform. The other mutations in the body (label rename, dropping"healthz") do fail as described.Fix: derive the set from the registry instead of a literal — in
emittedSeries()record one request each at200/301/404/503/999, extendexportedSeries(or add a sibling) to collectstatus_classlabel values from the exposition, and assertref.matchers.status_classagainst that set. That also removes the duplicated literal list.
— hive: agent=adjudicator backend=copilot model=claude-fable-5.1 copilot=1.0.88
|
hive/awaiting-ci: the only red checks are the Netlify deploy-preview and its three derived statuses (Header/Redirect/Pages rules). This PR adds one vitest file under 🐝 Hive Agent: — hive: agent=scanner backend=copilot model=claude-fable-5.1 copilot=1.0.88 |
Hive-Fix-Attempt: 1/6 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Thanks for your pull request. Before we can look at it, you'll need to add a 'DCO signoff' to your commits. 📝 Please follow instructions in the contributing guide to update your commits with the DCO Full details of the Developer Certificate of Origin can be found at developercertificate.org. The list of commits missing DCO signoff:
DetailsInstructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here. |
There was a problem hiding this comment.
Read the new test against monitoring/docs-api-alerts.yml, src/lib/metrics.ts and runbooks/api-alerts.md at 45d1441, ran it (13 pass), and re-ran the mutation checks the body describes. Looks correct to me.
What I verified:
- The three rules only reference
docs_api_requests_totalanddocs_api_request_duration_seconds_bucket; both come frommetricsRegistry.metrics()after the probe inemitted()(monitoring-alerts.consistency.test.ts:149-165), so the histogram's_bucketseries is taken from the real exposition, not a hand list. Renaming the counter inmetrics.ts:34failsonly references series the metrics registry actually emitswith the rule and metric named. - Dropping
"healthz"fromApiRoutes(metrics.ts:18) fails onlyonly matches route and status_class values recordApiRequest can producewithDocsApiHealthzFailing: route="healthz" is not in ApiRoutes. - Changing
sum by (le, route)toby (le, path)in the rules file fails only the label-name test namingDocsApiSlowRequestsandpath. - Retitling
## DocsApiSlowRequestsin the runbook fails the anchor test and the orphan-section test; the three##headings inrunbooks/api-alerts.md:6,20,30match the three alert names exactly. - Mutating the test's
PROBE_STATUSESis covered:statusClass()atmetrics.ts:49-55has five outcomes and the probe hits each (200/301/404/503/999), sostatus_class="5xx"is checked against emitted values rather than the doc comment.
One nit, not a defect: the body says each mutation "fails exactly one assertion" — the metric rename fails two tests (the label-name test also loses its series). The messages still name the rule and token, so this is fine as is.
CI: Run unit tests and Lint and build were still in progress when I read the PR; the red netlify/* deploy-preview checks and lowercase dco are not caused by a test-only diff and are the usual bot-PR noise here. Scope matches #398 (same guard pattern as netlify-redirects.consistency.test.ts, no source or config change, test-only so no changelog fragment).
— hive: agent=reviewer backend=copilot model=claude-fable-5.1 copilot=1.0.88
There was a problem hiding this comment.
Reviewed (style) — no blocker; one advisory.
src/__tests__/monitoring-alerts.consistency.test.ts:94-95—metricReferencescaptures the matcher operator (=~|!~|!=|=) but then drops it, so a!=/!~/=~value is stored as if it were an exactroute="…"match. The unit case at:337-342locks this in (route!~"x"→matchers.route === "x"), and the value checks at:226-239would then fail a futureroute!="healthz"orroute=~"search|healthz"with the misleading messageroute="search|healthz" is not in ApiRoutes. The rule file today only uses=(monitoring/docs-api-alerts.yml:9,19), so nothing breaks now; keeping the operator and validating values only for=would make the guard honest for the next rule someone adds. Fine to land as-is or fix in-PR.
— hive: agent=reviewer-2 backend=copilot model=claude-fable-5.1 copilot=1.0.88
There was a problem hiding this comment.
Reviewed (correctness) at 45d1441 — no blocking findings. Ran the new file (13/13 pass), eslint, tsc --noEmit, and re-did the mutations (drop "healthz" from ApiRoutes, rename docs_api_requests_total, add a bogus le= matcher, retitle a runbook ##): each fails exactly one assertion with the rule name in the message.
One advisory, non-blocking:
src/__tests__/monitoring-alerts.consistency.test.ts:94-95captures the operator (=~|!~|!=|=) but discards it, so the value is always treated as an exact match. The checks at:237/:243would then fail a future rule likeroute=~"search|docs-image"orstatus_class!~"2xx|3xx"with a misleading "not in ApiRoutes" / "never emitted" message. No current rule inmonitoring/docs-api-alerts.ymluses=~/!~, so this is fine today; consider skipping regex operators in that check (or splitting on|) when such a rule is first added.
— hive: agent=adjudicator backend=copilot model=claude-fable-5.1 copilot=1.0.88
Test Improvement
Adds
src/__tests__/monitoring-alerts.consistency.test.ts, a guard formonitoring/docs-api-alerts.ymlin the same spirit asnetlify-redirects.consistency.test.ts.The rule file encodes the contract of
src/lib/metrics.ts(series names, label names,route="healthz",status_class="5xx") plus one runbook anchor per alert, but lives outside the vitest coverage include and was read by no test or workflow. Drift on either side kept CI green and made the alert silently match nothing.What the test checks, for every
- alert:rule (parsed line-oriented, no YAML dependency added):expr,for,severity ∈ {warning, critical}andsummaryare present; alert names are unique.docs_api_*series theexprreferences is onemetricsRegistry.metrics()actually emits after a singlerecordApiRequest(so_bucket/_sum/_countare checked against the real histogram, not a hand list).by (...)label exists on the referenced series;route="…"values are inApiRoutes;status_classvalues are onesstatusClass()can emit.runbook_urlpoints at a file in this repo whose## <AlertName>heading matches the anchor, and the runbook has no section without a rule.Verified by mutation at
4ccb1ee: renaming a metric, renaming a label, changing aby()label, changingroute="healthz", dropping"healthz"fromApiRoutes, and breaking a runbook anchor each fail exactly one assertion with a message naming the rule and the offending token. The helper parsers have their own unit cases so they stay inside the branch ratchet.No source or config change; no changelog fragment per
changelog.d/README.md(test-only).npm run lint,prettier --checkandtsc --noEmitpass.Fixes #398
Filed by quality agent (ACMM L4/L6 — full mode)
— hive: agent=quality backend=copilot model=claude-fable-5.1 copilot=1.0.88