Controller-manager Prometheus metrics - #245
Merged
Merged
Conversation
ljkiraly
reviewed
Sep 16, 2026
Collaborator
There was a problem hiding this comment.
Recommendations:
- Add
Collect-level unit tests for both collectors (gateway_collector_test.go,
distributiongroup_collector_test.go). Cover: Gateway-union resolution incl. empty-union fallback, per-Gateway endpoint counting with non-owned-slice filtering,resolveMaxEndpointsbranches (incl. unknown-type+Inf), andgateway_count/ per-Gatewayprogrammedemission with an accepted-by-different-controller exclusion. Usefakeclient +WithIndex(existing pattern) andprometheus/testutil. This is the single item separating this from a clean approve.
Nice-to-have:
- Add a brief note in
docs/controllers/gateway.mdthat the Accepted condition Message format is now a load-bearing contract (matched viagatewayutil.GatewayAcceptedMessagePrefix), so future edits toacceptedMessagemust preserve the prefix. The code comments capture this, but the controller doc is where a maintainer editing status messages would look. - Consider a follow-up to give Gateway "accepted by controller X" a machine-readable signal (e.g. a dedicated Reason or annotation) rather than relying on message-suffix matching. Not for this PR — it's a larger condition-contract change with existing tests asserting on message content — but worth a tracking issue.
Minor:
- The
--metrics-collect-timeoutflag help text is very long (a full paragraph). Consider trimming to one sentence and moving the race-condition rationale to the metrics doc, so--helpoutput stays scannable.
ljkiraly
approved these changes
Sep 21, 2026
ljkiraly
left a comment
Collaborator
There was a problem hiding this comment.
Any item resolved with well targeted tests.
Looks good.
ljkiraly
approved these changes
Sep 22, 2026
Move Gateway status-condition interpretation out of the gateway controller into internal/common/gatewayutil (IsGatewayAcceptedByController, IsGatewayProgrammed, GatewayAcceptedMessagePrefix), and de-methodize the DistributionGroup Gateway/route resolution helpers (ListReferencedGateways, listRoutesReferencingDG, getGatewayFrom*) so they take a client.Client + namespace instead of a reconciler receiver. Export DistributionGroup IsReady next to where the Ready condition is written. Lets these be reused by external consumers (the controller-manager metrics collectors) without depending on the reconciler packages, and removes the duplicated Accepted-check logic between the gateway and distributiongroup controllers. Signed-off-by: Lugossy Zoltán <zoltan.lugossy@est.tech>
Add pull-based prometheus.Collectors for Gateway and DistributionGroup state (gateway_count, gateway_programmed, distributiongroup_ready, distributiongroup_endpoints, distributiongroup_max_endpoints), reading from the manager's informer cache on scrape. Registered against the controller-runtime metrics registry when --metrics-bind-address is set. Adds a cache-sync gate so a scrape before the informer cache has synced fails fast rather than blocking or emitting a partial set, and a --metrics-collect-timeout flag bounding that wait. Includes prefix validation/Enabled helpers in internal/common/metrics and a local testing doc. Signed-off-by: Lugossy Zoltán <zoltan.lugossy@est.tech>
Enable secure metrics serving by default in config/default (mount the cert-manager metrics-server-cert, set --metrics-cert-path, substitute the metrics Service name/namespace into the cert dnsNames). Log collector registration in the controller-manager. Expand metrics-testing.md with the in-cluster Prometheus/Grafana scrape recipe and an OTEL-migration note. Signed-off-by: Lugossy Zoltán <zoltan.lugossy@est.tech>
Signed-off-by: Lugossy Zoltán <zoltan.lugossy@est.tech>
Cover Gateway-union resolution, non-owned-slice filtering, resolveMaxEndpoints branches, and accepted-by-different-controller exclusion via fake client + testutil. Export DistributionGroup's Ready condition constants for reuse in these tests. Also trims --metrics-collect-timeout help text and clarifies the Gateway Programmed condition message/docs. Signed-off-by: Lugossy Zoltán <zoltan.lugossy@est.tech>
Emit gateway_programmed for every Gateway destined for this controller by its GatewayClass (spec.gatewayClassName -> GatewayClass.spec.controllerName). Independent of the Accepted condition, so a class-ours Gateway reports 0/1 for its whole lifetime — including before acceptance or after an Accepted reset — instead of emitting no series. gateway_count is unchanged (still Accepted-by-us). Signed-off-by: Lugossy Zoltán <zoltan.lugossy@est.tech>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#247
Adds custom Prometheus metrics for the controller-manager, plus the small controller-package streamlining
needed to expose shared state to the collectors.
Metrics
Pull-based
prometheus.Collectors registered against the controller-runtimemetrics registry when
--metrics-bind-addressis set:<prefix>_gateway_countAccepted=Truemanaged by this controller<prefix>_gateway_programmedProgrammedcondition<prefix>_distributiongroup_readyReadycondition<prefix>_distributiongroup_endpoints<prefix>_distributiongroup_max_endpoints+Infif unbounded)Collectors read live from the manager's informer cache on scrape (no
background loop, no reconcile coupling); deleted objects simply stop
appearing and Prometheus marks the series stale.
Notable design points
Cache-sync gate: a scrape landing before the informer cache has synced
fails fast with a single error instead of blocking or emitting a partial
metric set.
Collecthas no context, so a--metrics-collect-timeoutflag(default 5s, deliberately below Prometheus's 10s
scrape_timeout) boundsthe wait.
Label policy: separate
namespace/gateway_namespacelabels(kube-state-metrics convention) so all-namespace watches don't collide
identically-named objects.
Reusable prefix-validation /
Enabledhelpers ininternal/common/metricsfor future component metrics (LB, router, sidecar).
gateway_programmedgating: emitted for every Gateway destined for thiscontroller by its GatewayClass (
spec.gatewayClassName→GatewayClass.spec.controllerName), from theProgrammedcondition,independent of
Accepted. This intentionally differs fromgateway_count(Accepted-by-us): it reports
0/1for the whole lifetime a Gateway isclass-ours — including before acceptance or after an Accepted reset — so a
Gateway stuck un-accepted is visible as
programmed=0rather than emitting noseries. The stale case during a discouraged
gatewayClassNamechange (aprevious owner left
Programmed=True) is reported as-is; the metric makes nodata-plane-freshness claim.
Open question:
distributiongroup_readysemanticsdistributiongroup_readymirrors the DGReadystatus condition as-is, whichtoday means "the DG has assigned endpoints" (
len(desiredSlices) > 0), not"the DG has endpoints that are ready to serve traffic". Per-endpoint
LoadBalancerEndpoint.Ready(Pod readiness) is not consulted when the condition isset (
buildReadyConditionindistributiongroup/status.go) — a DG whose endpointsare all
NotReadystill reportsReady=True, and this metric will too.This is a likely mismatch with what the observability study (#153) anticipates a
"ready" signal to mean for a distribution group. The collector deliberately reports
the existing condition rather than inventing a second, metrics-only definition of
readiness under the same name — surfacing the discrepancy instead of hiding it.
Resolving it (should
Readyitself account for per-endpoint readiness, e.g. aper-Gateway "has ready endpoints" status field?) is a DistributionGroup reconciler
design decision, out of scope for this metrics PR. Flagging here so #153/#236
reviewers can decide whether the metric's meaning is acceptable as shipped or should
block on a readiness redesign.
Streamlining
Extracts Gateway status-condition helpers into
internal/common/gatewayutil(
IsGatewayAcceptedByController,IsGatewayProgrammed) and de-methodizes theDistributionGroup Gateway/route resolution helpers so the collectors can reuse
the exact reconciler semantics without depending on the controller packages.
Removes the duplicated Accepted-check between the gateway and distributiongroup
controllers. No behavior change.
Testing
registration;
go build ./..., package tests, andmake lintall clean.secure metrics served over HTTPS, scraped by an in-cluster Prometheus via a
ServiceMonitor, all metrics confirmed with real values. Reproducible recipe —
including the Prometheus/Grafana walkthrough — in
docs/development/metrics-testing.md.In-cluster metrics
config/default) now serves metrics securely over HTTPS on:8443by default, using a cert-manager-issued cert (metrics-server-cert),fronted by the
controller-manager-metrics-serviceService.ServiceMonitorships in the base — it must not depend on the PrometheusOperator CRDs, so
kubectl apply -k config/defaultworks without the Operator.Wiring a Prometheus scrape (Helm stack + ServiceMonitor + verification) is
documented in
docs/development/metrics-testing.mdrather than shipped in themanifest.
prometheus/client_golangpromoted to a direct dependency.