feat(observability): add the alert render pipeline, alert templates and promtool tests - #188
Conversation
…alerts (#184) Add an Observability section to the root Makefile with the alerts, dashboards and test-alerts targets, port the condition alert rules and their promtool unit tests from go-crd-condition-metrics, and ignore the rendered output under observability/generated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd
…r fail to apply (#184) Add ManagedResourceNotConverging and ManagedResourceApplyFailing on the ocf_resource_apply_total and ocf_resource_apply_errors_total counters. Both are ratios of the resource's own applies with an absolute floor, keyed on the operator's static topology, so legitimate churn at scale and sporadic conflicts stay silent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd
…log, latency and leadership (#184) Add ControllerReconcileErrors, ControllerReconcilePanics, ControllerWorkqueueBacklog, ControllerReconcileLatencyHigh and OperatorLeaderMissing on the controller-runtime metrics every operator exposes, with thresholds expressed as ratios or quantiles so they hold at any scale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd
Add an observability job to the test workflow that installs a pinned promtool and runs make test-alerts on every push and pull request. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd
There was a problem hiding this comment.
🟡 Changes recommended
The PR as submitted has correctness and completeness gaps (broken/misleading observability docs/targets, and a likely incorrect label assumption in the workqueue alert that would prevent it from firing).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds an observability/ subtree intended to let framework consumers render Prometheus alert rules (and eventually dashboards) parameterized by metric namespace, and validates alert rules via promtool tests and CI.
Changes:
- Introduces templated Prometheus alert rule files under
observability/alerts/*.tpl.yamlplus promtool unit tests underobservability/alerts/tests/. - Adds root
Makefiletargets to render alerts/dashboards and to runpromtool check rules+promtool test rules. - Adds a new GitHub Actions job that installs
promtooland runsmake test-alerts; ignores rendered artifacts via.gitignore.
File summaries
| File | Description |
|---|---|
observability/README.md |
Entry-point README describing how to render and test observability artifacts. |
observability/alerts/crd_conditions.tpl.yaml |
Alert templates for CRD condition metrics. |
observability/alerts/ocf_resources.tpl.yaml |
Alert templates for OCF managed-resource apply metrics. |
observability/alerts/controller_runtime.tpl.yaml |
Alert templates for controller-runtime and leader-election metrics. |
observability/alerts/tests/crd_conditions_test.yaml |
promtool tests for CRD condition alerts. |
observability/alerts/tests/ocf_resources_test.yaml |
promtool tests for managed-resource apply alerts. |
observability/alerts/tests/controller_runtime_test.yaml |
promtool tests for controller-runtime alerts. |
Makefile |
Adds dashboards, alerts, and test-alerts render/test targets. |
.gitignore |
Ignores observability/generated/ render output. |
.github/workflows/test.yml |
Adds an observability CI job that installs promtool and runs make test-alerts. |
Review details
Suppressed comments (1)
observability/README.md:15
make observability-upis referenced here, but there is no such target in the Makefile, so this command will fail for readers. Either add the target or remove the reference.
Run the alert unit tests with `make test-alerts` (needs `promtool`), and bring up Prometheus and Grafana with the
simulator behind them with `make observability-up`.
- Files reviewed: 9/10 changed files
- Comments generated: 5
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…troller (#184) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd
|
Review round 1, disposition of the suppressed finding:
|
There was a problem hiding this comment.
🟡 Changes recommended
The new CI job’s promtool install step pipes into tar without specifying -f -, which can cause extraction to fail and break the observability workflow run.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 9/10 changed files
- Comments generated: 1
- Review effort level: Lite
… and alert per install (#184) controller_runtime_reconcile_time_seconds has 60 as its largest finite bucket and histogram_quantile caps there, so a threshold of 60 could never fire; lower it to 30, a bucket bound. The workqueue histogram has one bucket per decade, so move the backlog threshold to 100 seconds and say in the description that the p99 value is interpolated. Add the operator's namespace to every aggregation so two installs of one operator in a cluster are alerted on separately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd
… and simplify the failure ratio (#184) A lease name cannot be combined with the all-namespaces flag, so the leader quick check uses a field selector. The framework records no event for a failed apply, so the apply failure quick check lists the owners' Ready conditions instead. Write the failure ratio as errors over (errors + applies) or errors, and say that OperatorLeaderMissing is also silent when no replica is alive to export the gauge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd
…t PrometheusRule metadata (#184) The controller-runtime and managed resource rules carry no metric namespace placeholder, so rendering them per operator installed a duplicate copy per operator. Keep per-operator templates as <name>.tpl.yaml, named <metric-namespace>-<name>, and ship the shared rules as plain <name>.yaml files, copied through unchanged and named ocf-<name>. Rename ocf_resources to managed_resources (group managed-resources) so the object is ocf-managed-resources. Add PROMETHEUSRULE_NAMESPACE and PROMETHEUSRULE_LABELS for ruleSelectors such as kube-prometheus-stack's release label. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd
There was a problem hiding this comment.
🟡 Changes recommended
The Makefile currently can emit invalid PrometheusRule label YAML (unquoted non-string values) and does not run promtool rule unit tests against the NAMESPACE_LABEL=namespace rendered variant.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 9/10 changed files
- Comments generated: 2
- Review effort level: Lite
Kubernetes label values must be strings, so emit them quoted; otherwise a value such as release=1 would be parsed as an integer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd
) * feat(observability): add the alert render pipeline, alert templates and promtool tests (#188) * feat(observability): add the alert render pipeline and the condition alerts (#184) Add an Observability section to the root Makefile with the alerts, dashboards and test-alerts targets, port the condition alert rules and their promtool unit tests from go-crd-condition-metrics, and ignore the rendered output under observability/generated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * feat(observability): alert on managed resources that never converge or fail to apply (#184) Add ManagedResourceNotConverging and ManagedResourceApplyFailing on the ocf_resource_apply_total and ocf_resource_apply_errors_total counters. Both are ratios of the resource's own applies with an absolute floor, keyed on the operator's static topology, so legitimate churn at scale and sporadic conflicts stay silent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * feat(observability): alert on controller-runtime errors, panics, backlog, latency and leadership (#184) Add ControllerReconcileErrors, ControllerReconcilePanics, ControllerWorkqueueBacklog, ControllerReconcileLatencyHigh and OperatorLeaderMissing on the controller-runtime metrics every operator exposes, with thresholds expressed as ratios or quantiles so they hold at any scale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * ci: run the alert unit tests with promtool (#184) Add an observability job to the test workflow that installs a pinned promtool and runs make test-alerts on every push and pull request. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * docs(observability): explain why the workqueue rule aggregates by controller (#184) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * fix(observability): make the latency and backlog thresholds reachable and alert per install (#184) controller_runtime_reconcile_time_seconds has 60 as its largest finite bucket and histogram_quantile caps there, so a threshold of 60 could never fire; lower it to 30, a bucket bound. The workqueue histogram has one bucket per decade, so move the backlog threshold to 100 seconds and say in the description that the p99 value is interpolated. Add the operator's namespace to every aggregation so two installs of one operator in a cluster are alerted on separately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * fix(observability): correct the leader and apply failure quick checks and simplify the failure ratio (#184) A lease name cannot be combined with the all-namespaces flag, so the leader quick check uses a field selector. The framework records no event for a failed apply, so the apply failure quick check lists the owners' Ready conditions instead. Write the failure ratio as errors over (errors + applies) or errors, and say that OperatorLeaderMissing is also silent when no replica is alive to export the gauge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * feat(observability): install the shared rules once per cluster and set PrometheusRule metadata (#184) The controller-runtime and managed resource rules carry no metric namespace placeholder, so rendering them per operator installed a duplicate copy per operator. Keep per-operator templates as <name>.tpl.yaml, named <metric-namespace>-<name>, and ship the shared rules as plain <name>.yaml files, copied through unchanged and named ocf-<name>. Rename ocf_resources to managed_resources (group managed-resources) so the object is ocf-managed-resources. Add PROMETHEUSRULE_NAMESPACE and PROMETHEUSRULE_LABELS for ruleSelectors such as kube-prometheus-stack's release label. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * fix(observability): quote PrometheusRule label values (#184) Kubernetes label values must be strings, so emit them quoted; otherwise a value such as release=1 would be parsed as an integer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * feat(observability): add a local Prometheus and Grafana stack fed by a metrics simulator (#189) * feat(observability): add the simulator's controller-runtime metric lookalikes with a parity test (#185) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * feat(observability): add a metrics simulator that plays a scripted operator world (#185) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * feat(observability): add a docker compose Prometheus and Grafana stack fed by the simulator (#185) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * chore(observability): promote client_model to a direct dependency (#185) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * fix(observability): bind the dev stack to localhost and tidy simulator wording (#185) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * fix(observability): satisfy goconst in the simulator (#185) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * feat(observability): model persistent active workers and smoke test the scripted world (#185) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * fix(observability): harden the dev render and stack startup in the Makefile (#185) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * fix(observability): align the simulator with real controller-runtime emissions (#185) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * refactor(observability): source workqueue names from controller-runtime and drop the seeded rng (#185) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * fix(observability): harden dev stack startup and make the world test deterministic (#185) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * feat(observability): add the operator overview and conditions browser dashboards (#190) * feat(observability): add the conditions browser dashboard with stale-series protection (#186) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * feat(observability): add the operator overview dashboard (#186) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * test(observability): check rendered dashboards and alerts reference real metrics (#186) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * fix(observability): make dashboard filters and freshness keys consistent (#186) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * feat(observability): template the dashboard uids per metric namespace (#186) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * fix(observability): browser namespace handling for cluster-scoped owners and instant stats (#186) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * test(observability): tighten the template lint (#186) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * test(observability): exercise a cluster-scoped owner in the simulator world (#186) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * fix(observability): correct the status stat descriptions in the browser (#186) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * docs(observability): document the dashboards, alerts and local stack (#191) * docs(observability): document the dashboards, alerts and local stack (#187) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * docs(component): align the recorder's controller name with controller-runtime (#187) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * chore(plugin): sync docs/observability.md into the building-components skill (#187) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * chore(ai): reference docs/observability.md in the AI instructions (#187) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * docs(observability): correct CI, leader and Unknown-tile statements and dedupe the apply rationale (#187) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * docs(metrics): align GoDoc, e2e recorder names and the plugin skill with the controller-name rule (#187) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * test(e2e): give each primitives controller its own metrics recorder (#187) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * docs(observability): keep maintainer content in observability/README.md (#187) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * fix(observability): join condition alerts on the freshest series before matching status A former leader pod keeps exporting its last condition values until it restarts, so its stale False or Unknown series kept CustomResourceNotReady, CustomResourceConditionUnknown and CustomResourceConditionStuck firing even after the current leader reported the owner as Ready. max() only ever saw the series that survived the status matcher. Every rule now joins on the freshest series per owner across every status first, the same topk join the dashboards use, and applies its status matcher after. Adds a promtool test with continuously scraped stale and current series. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * fix(observability): key the shared alert rules on the scrape job The controller-runtime and managed-resource rules aggregated by namespace and controller only, so two operators scraped as different jobs in one namespace with a matching controller name (and, for the apply counters, matching topology labels) merged into one ratio, where a healthy operator dilutes a failing one below the threshold. Every aggregation now keeps job as well. OperatorLeaderMissing is unchanged: it keys on the lease name, which is unique within a namespace. Adds same-namespace, different-job tests for the reconcile error and both apply ratios. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * fix(observability): reject metric namespaces that render an invalid Grafana uid The dashboard uids are <METRIC_NAMESPACE>_<file name>, and Grafana limits a uid to 40 characters of [A-Za-z0-9_-]. Rendering accepted any nonempty namespace, so a long one, or one with a colon, rendered cleanly and was rejected by Grafana at import. require_metric_namespace now enforces metric name characters and a 17 character cap, the room the longest file name leaves, and observability_test.go pins that arithmetic to the dashboard file names. Also carries the documentation for the previous two rule changes: the freshest-series join and job keying. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * fix(observability): validate NAMESPACE_LABEL, drop the unset sentinel, clean render output METRIC_NAMESPACE defaulted to the sentinel "unset", which reserved a namespace the documented grammar allows; it now defaults to empty and the check tests for that. NAMESPACE_LABEL is substituted into PromQL as a label name, so it is now validated against the Prometheus label name grammar instead of rendering invalid queries on a typo. Both render targets remove the files they previously wrote before rendering, so a dashboard or rule file dropped or renamed by a framework upgrade is not installed again from the gitignored output. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * feat(observability): give the operator overview room and graph the updated rate The Overview row squeezed nine instant-value stat tiles into one line, so titles truncated and nothing showed a trend. It is now two lines: four stats with sparklines over the dashboard range (reconciles/s, error ratio, p99 reconcile time, p99 queue wait), then one owners panel with Ready, not Ready > 2m and Unknown as three coloured tiles, leader status and workers. Updated rate per resource was a table snapshot of the metric whose point is its trend; it is now a timeseries with a legend table sorted by last value, threshold lines at 0.05 and 0.5 and a zero floor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * feat(observability): draw container requests and limits on the process panels The CPU and Memory RSS panels join kube_pod_container_resource_requests and _limits to the operator's scrape target on (namespace, pod) and draw them as dashed lines, absent when kube-state-metrics is not scraped. The simulator serves lookalikes of both families on /ksm/metrics, scraped by the dev Prometheus as its own job with honor_labels the way kube-state-metrics' ServiceMonitor is, so the local stack shows the lines. The template lint test now knows both families and `up`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * feat(observability): order the operator dashboard by priority and rebuild the conditions row Rows now follow what an on-call reader asks first: Overview, Conditions, Managed resources, Reconciliation, Workqueue, API client, Process. The conditions row no longer grows with kinds times condition types times statuses: a stacked timeseries of owners by Ready status over time, a bar gauge of unhealthy conditions only (owners per kind, condition and status for every condition that is not True, empty when healthy), and the Owners not Ready table at full width. Updated rate per resource takes the full width with its legend table below the graph, the per-resource ratio panels sit beneath it, and every ratio panel is pinned to 0 to 100% with a dashed threshold line so the alert bound is always visible. Timeseries panels grow from 8 to 9 rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * fix(observability): drop the Last calc from the Ready status legend The Overview tiles already carry the current counts, and the calc turned each legend entry into "Ready Last *: 53". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * refactor(observability): say CRs instead of owners in the operator dashboard The framework calls the reconciled custom resource the owner, and the term leaked into panel titles and descriptions where a dashboard reader has no reason to know it. Titles and descriptions now say CRs; the docs gloss the mapping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * docs(observability): name the panel that joins on condition correctly Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * feat(observability): give reconcile rate by result the full dashboard width It is the reconciliation row's most useful panel, so it now spans the row at 11 units high; the error ratio, latency and panics sit beneath it, then in-progress age and workers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * feat(observability): scope the operator dashboard by operator namespace and scrape job Every controller-runtime operator exports the same metric names, so the dashboard selects one operator through the scrape labels. It keyed on job alone, while the shared alert rules key on (namespace, job); two installs of one operator in different namespaces, which usually share a job name, merged on the dashboard. A namespace variable now leads the cascade (All also matches series without the label, so an operator scraped outside a cluster still renders), every selector carries it, and the job variable is labelled as the operator's scrape job with a description. Docs explain the variables. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * fix(observability): require a leading letter in METRIC_NAMESPACE and key=value rule labels A leading underscore passed the grammar but rendered the PrometheusRule name -demo-crd-conditions, which Kubernetes rejects; the namespace must now start with a letter. A PROMETHEUSRULE_LABELS entry without = rendered as key: "key" instead of failing, so a typo silently produced a rule the operator's selector never matches; it is now an error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * fix(observability): say most reconciles, not every, in ManagedResourceNotConverging The rule fires above a 50% updated share, so "rewritten on every reconcile" overstated a 51% ratio. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * fix(simulator): keep lastTransitionTime on reason-only flips and report shutdown errors A reason-only change keeps lastTransitionTime under meta.SetStatusCondition, so the simulator no longer resets it and now produces the equal-valued reason series a real operator does. Shutdown errors are returned instead of discarded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * fix(observability): keep the scrape job in the condition rules and the browser Two installs of an operator exporting the same metric namespace and the same CR identity deduped and merged across each other: the condition rules' freshest-series join and aggregation carried no install identity, and the browser had no job variable, so the operator dashboard's and the alerts' deep links could land on the other install. The rules now keep job in every grouping, the browser has a job variable (All by default) that every query and both link producers carry, and a cross-job test covers the case. The reason-change fixture keeps lastTransitionTime the way a real reason-only update does. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd * fix(observability): scope condition panels by job only and exercise every simulator scenario With NAMESPACE_LABEL=namespace the namespace label on condition series is the CR's, so the operator-namespace variable misfiltered every condition panel; those panels are now scoped by job alone, and the docs say why job is the whole install identity the condition rules need. The simulator's backlog and panic scenario timers are fields the scripted-world test shortens, and it now asserts the queue gauges, wait histogram, busy workers and the panic counter. The equal-timestamp reason tie after a leader change is documented as a known limitation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Towards #184
Summary
Adds the
observability/tree with a make render pipeline, three templated Prometheus alert rule files, promtool unit tests for every rule, and a CI job that runs them.Make targets (new
##@ Observabilitysection in the rootMakefile):make alerts METRIC_NAMESPACE=<ns>renders everyobservability/alerts/*.yamlintoobservability/generated/alerts/<name>.yaml, as onePrometheusRuleobject per file by default or as plain rule files withALERT_FORMAT=rules. Per-operator templates (<name>.tpl.yaml) are rendered and named<metric-namespace>-<name>; shared rule files (<name>.yaml, no placeholder) are copied through and namedocf-<name>, so they are installed once per cluster rather than once per operator.NAMESPACE_LABEL(defaultexported_namespace) selects the label that carries the custom resource namespace;PROMETHEUSRULE_NAMESPACEandPROMETHEUSRULE_LABELS(comma-separatedkey=value) set thePrometheusRulemetadata for ruleSelectors such as kube-prometheus-stack's release label. MissingMETRIC_NAMESPACEor an unknownALERT_FORMATfail with a usage hint.make dashboards METRIC_NAMESPACE=<ns>rendersobservability/dashboards/*.tpl.jsonintoobservability/generated/dashboards/. The loop tolerates an empty glob, so the target already works before the first dashboard template lands.make test-alertsrenders every template with the test namespace under both namespace label variants (shared rule files are copied as is), lints them withpromtool check rules --lint=all --lint-fatal, and runspromtool test rules --diffonobservability/alerts/tests/*.yaml.observability/generated/is gitignored;observability/README.mdcarries the render commands.Rule files (the per-operator template uses
{{operator_namespace}}and{{namespace_label}}; every alert isseverity: warningwith no routing labels; the shared rules aggregate by the operator'snamespaceas well so two installs of one operator are alerted on separately):crd_conditions.tpl.yaml, groupcrd-conditions:CustomResourceNotReady,CustomResourceConditionUnknown,CustomResourceConditionStuck. Ported unchanged fromgo-crd-condition-metricsso the per-owner condition alerts ship from this repository; each carries adashboard_urlinto thecrd_conditions_browserdashboard.managed_resources.yaml(shared,ocf-managed-resources), groupmanaged-resources:ManagedResourceNotConverging,ManagedResourceApplyFailing. Both are ratios of the resource's own applies with an absolute floor, keyed on(namespace, controller, owner_kind, component, resource, kind), so legitimate churn at scale and sporadic conflicts stay silent while a hot loop or a systematically failing resource type fires.controller_runtime.yaml(shared,ocf-controller-runtime), groupcontroller-runtime:ControllerReconcileErrors,ControllerReconcilePanics,ControllerWorkqueueBacklog,ControllerReconcileLatencyHigh,OperatorLeaderMissing. Thresholds are ratios or p99 quantiles per controller and sit on real histogram bucket bounds (30 s reconcile time, 100 s queue wait), so they hold whether the operator manages three owners or three thousand.CI: a new
observabilityjob in.github/workflows/test.ymlinstalls a pinned promtool (3.14.0) and runsmake test-alerts. promtool stays out ofmake allso contributors without it can still run everything else.Verification
make all: passes (fmt, lint, test, test-scaffold, test-examples, build-examples).make test-alerts: lintSUCCESSfor all three rule files under bothexported_namespaceandnamespace, andSUCCESSfor all three unit test files (15 test cases, including a per-install case for two installs of one operator and the negative cases: churn at scale and a single edit do not fireManagedResourceNotConverging; sporadic conflicts do not fireManagedResourceApplyFailing; a resource that only ever failed still fires).make alerts METRIC_NAMESPACE=demo: writescrd_conditions.yaml,controller_runtime.yaml,managed_resources.yamlholdingPrometheusRuleobjectsdemo-crd-conditions,ocf-controller-runtime,ocf-managed-resourcesthat parse as YAML with the groups underspec; withPROMETHEUSRULE_NAMESPACE=monitoring PROMETHEUSRULE_LABELS=release=kps,team=platformthe metadata carries the namespace and both labels.make alerts METRIC_NAMESPACE=demo ALERT_FORMAT=rules && promtool check rules observability/generated/alerts/*.yaml:SUCCESS: 5 rules found,3 rules found,2 rules found.make alerts: exits non-zero withError: METRIC_NAMESPACE is required.and the usage hint.make alerts METRIC_NAMESPACE=demo ALERT_FORMAT=nope: exits non-zero with theALERT_FORMATerror.make dashboards METRIC_NAMESPACE=demo: exits 0 with no dashboard templates present.actionlint .github/workflows/test.yml: clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01JPihvXVfS997iGmGabTGsd