feat: expose Prometheus metrics for EC2 endpoint discovery#14318
Conversation
Add per-Backend Prometheus metrics for the EC2 EDS polling loop (FR-10): - kgateway_ec2_discovery_poll_total (counter) partitioned by result and reason, matching the EndpointsDiscovered status condition reasons - kgateway_ec2_discovery_endpoints_active (gauge), set to the matched instance count after each successful poll and retained on failure (NFR-3 graceful degradation) - kgateway_ec2_discovery_error_state (gauge), 1 when the last poll failed Metrics are recorded per Backend even though discovery batches AWS DescribeInstances calls by credential scope. Series for deleted Backends are removed via a backends-collection delete handler so stale gauges don't linger; this required adding DeletePartialMatch to the metrics Counter/Gauge interfaces. Signed-off-by: omar <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR adds Prometheus instrumentation for the EC2 backend endpoint-discovery polling loop so operators can monitor discovery health and current endpoint counts quantitatively (in addition to existing logs/status).
Changes:
- Introduces three per-Backend EC2 discovery metrics (poll counter, active endpoint gauge, error-state gauge) and records them on successful/failed polls.
- Adds per-series cleanup on Backend deletion by extending the metrics Counter/Gauge interfaces with partial-label deletion.
- Adds unit tests covering success, no-matching-instances, error-with-carry-forward, and metric cleanup behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| pkg/metrics/metrics.go | Extends Counter/Gauge with DeletePartialMatch and implements partial-label deletion for Prometheus vec-backed metrics. |
| pkg/kgateway/extensions2/plugins/backend/metrics.go | Defines and records EC2 discovery metrics (poll total, active endpoints, error state) and adds deletion cleanup helpers. |
| pkg/kgateway/extensions2/plugins/backend/metrics_test.go | Adds unit tests validating metric recording and deletion cleanup behavior. |
| pkg/kgateway/extensions2/plugins/backend/ec2.go | Wires EC2 poll success/error metric recording into the discovery loop and registers a delete handler to remove per-Backend series. |
| gathered := metricstest.MustGatherMetrics(t) | ||
| // Only the surviving Backend's gauge series remain; the deleted Backend leaves | ||
| // no stale per-Backend gauge behind. | ||
| gathered.AssertMetricsLabels(endpointsActiveMetric, [][]metrics.Label{ | ||
| backendIdentityLabels("default", "backend-b"), | ||
| }) | ||
| gathered.AssertMetricsLabels(errorStateMetric, [][]metrics.Label{ | ||
| backendIdentityLabels("default", "backend-b"), | ||
| }) | ||
| } |
| reason := ec2PollReasonNone | ||
| if endpointCount == 0 { | ||
| reason = string(kgateway.BackendReasonNoMatchingInstances) | ||
| } |
There was a problem hiding this comment.
currently yes that matches the status condition
There was a problem hiding this comment.
Wouldn't this give none while the EndpointsDiscovered is Discovered (BackendReasonDiscovered)?
| @@ -840,8 +868,11 @@ func ec2ConfigFromBackend(backend ir.BackendObjectIR) *ec2BackendConfig { | |||
There was a problem hiding this comment.
Does this cover metrics as well?
There was a problem hiding this comment.
this log is unreachable, removed
There was a problem hiding this comment.
My question was more around also reflecting these scenarios in metrics, as this situation can likely happen (?)
There was a problem hiding this comment.
I see what you mean now, addressed.
| // EC2 endpoint discovery metrics. The exposed names are | ||
| // kgateway_ec2_discovery_poll_total, kgateway_ec2_discovery_endpoints_active, and | ||
| // kgateway_ec2_discovery_error_state, following the kgateway_<subsystem>_<name> | ||
| // convention used elsewhere in the codebase. |
There was a problem hiding this comment.
Can we add a kgateway_ec2_discovery_poll_duration_seconds Histogram as well?
Add kgateway_ec2_discovery_poll_duration_seconds, a per-Backend histogram of the AWS DescribeInstances round-trip time for each discovery poll (labels namespace, name). Both successful and failed polls are observed so slow timeouts are visible. It is configured as a native histogram (bucket factor 1.1), matching the other duration histograms in the codebase, with classic buckets retained as a fallback for scrapers that lack native support, spanning a fast in-region listing up to the 30s refresh timeout. The duration is attributed to every Backend sharing a credential scope, consistent with poll_total being recorded per Backend. Adds DeletePartialMatch to the metrics Histogram interface so the new series is cleaned up alongside the gauges/counter on Backend deletion. Also simplifies the deletion handler to clean up by object-source identity unconditionally: the identity is always populated, every Backend is unique by namespace/name, and deleteEc2DiscoveryMetrics is a no-op for non-EC2 backends, so this is safe and avoids depending on the deleted Obj's type. Signed-off-by: omar <[email protected]>
An EC2 backend whose secret-auth credential cannot be resolved never builds an ec2Ir: buildTranslateFunc's loadAWSSecret returns an error (the secret lookup fails closed), so the EC2 branch breaks before setting awsIr. By the time ec2ConfigFromBackend dereferences ec2Ir, a non-nil ec2Ir therefore implies the secret resolved, making the ec2Ir.secret == nil guard (and its debug log) dead code. Unresolved-secret backends are already surfaced as CredentialError via discoveryStatusForBackend/ec2UnresolvedSecretCredential. Replace the guard with a comment explaining the invariant. Signed-off-by: omar <[email protected]>
A secret-auth EC2 backend whose secret cannot be resolved never enters the discovery poll loop (translation fails closed), so it previously recorded no metrics at all even though it reports a CredentialError status condition. This left the most common misconfiguration (a missing or typo'd secretRef) invisible to error_state alerting. Set error_state=1 (and endpoints_active=0) for these backends from discoveryStatusForBackend, where the CredentialError condition is already computed. The gauge Set is idempotent under KRT recompute and is overwritten once the secret resolves and the poll loop takes over; the delete handler clears it on backend removal. poll_total and poll_duration_seconds stay poll-scoped, since no poll actually occurs. Signed-off-by: omar <[email protected]>
| NativeHistogramBucketFactor: 1.1, | ||
| NativeHistogramMaxBucketNumber: 100, | ||
| NativeHistogramMinResetDuration: time.Hour, |
| reason := ec2PollReasonNone | ||
| if endpointCount == 0 { | ||
| reason = string(kgateway.BackendReasonNoMatchingInstances) | ||
| } |
There was a problem hiding this comment.
Wouldn't this give none while the EndpointsDiscovered is Discovered (BackendReasonDiscovered)?
| NativeHistogramMaxBucketNumber: 100, | ||
| NativeHistogramMinResetDuration: time.Hour, | ||
| }, | ||
| []string{ec2MetricNamespaceLabel, ec2MetricNameLabel}, |
There was a problem hiding this comment.
What do you think about adding result as a label? Cardinality should be low, while users would have the option to filter out fast rejections.
Add a result (success/error) label to kgateway_ec2_discovery_poll_duration_seconds so operators can isolate successful-poll latency from failures, whose latency is bimodal (fast credential/authorization rejections vs. slow timeouts). Only result is added, not reason: reason belongs on the counter, and native histograms allocate per label-set. Signed-off-by: omar <[email protected]>
recordEc2PollSuccess recorded reason=none when endpoints resolved, but
the EndpointsDiscovered status condition uses reason=Discovered for the
same outcome. This contradicted the poll_total contract that reason
values match the status condition. Use BackendReasonDiscovered so
poll_total{reason="Discovered"} joins cleanly to the condition, and
drop the now-unused ec2PollReasonNone constant.
Signed-off-by: omar <[email protected]>
| // EC2 endpoint discovery metrics. The exposed names are | ||
| // kgateway_ec2_discovery_poll_total, kgateway_ec2_discovery_endpoints_active, and | ||
| // kgateway_ec2_discovery_error_state, following the kgateway_<subsystem>_<name> | ||
| // convention used elsewhere in the codebase. |
Description
Implements the remaining open requirement (FR-10) from GG-1509: expose Prometheus metrics for the EC2 backend endpoint-discovery loop added in #13961. Operators previously had only structured logs and no quantitative signal for EC2 discovery health.
Four per-Backend metrics are recorded by the EC2 EDS polling loop, following the codebase's
kgateway_<subsystem>_<name>convention:kgateway_ec2_discovery_poll_total(counter) — total discovery refresh attempts per Backend, labelednamespace,name,result(success/error), andreason. Thereasonvalues match theEndpointsDiscoveredstatus-condition reasons (none,NoMatchingInstances,CredentialError,AuthorizationError,DiscoveryError). A successful poll that matches zero instances is recorded asresult=success, reason=NoMatchingInstances(not an error). A failed poll that still serves carried-forward endpoints (Degraded) records the underlying classification reason, notDegraded, so the counter always attributes a concrete cause.kgateway_ec2_discovery_endpoints_active(gauge) — active endpoint count, set after each successful poll. Left unchanged on a failed poll so it retains the last successful value (NFR-3 graceful degradation). Labels:namespace,name.kgateway_ec2_discovery_error_state(gauge) —1when the Backend's most recent discovery outcome was a failure,0on success. Noreasonlabel by design; reason-specific diagnosis is via the counter and the Backend status condition. Labels:namespace,name. This also covers the case where a secret-auth Backend's secret cannot be resolved (it never enters the poll loop but is set to1from the status path), so missing/typo'dsecretRefmisconfigurations are visible to alerting.kgateway_ec2_discovery_poll_duration_seconds(native histogram) — wall-clock duration of the AWSDescribeInstancesround trip per poll, attributed to every Backend in the credential scope. Both successful and failed polls are observed so slow timeouts are visible; configured as a native histogram (bucket factor 1.1) with classic buckets retained as a fallback, spanning a fast in-region listing up to the 30s refresh timeout. Labels:namespace,name.Metrics are recorded per Backend even though discovery batches AWS
DescribeInstancescalls by credential scope. Series for deleted Backends are removed via a delete handler on the backends collection so stale series don't linger indefinitely. This required adding aDeletePartialMatchmethod to the metricsCounter/Gauge/Histograminterfaces (no prior per-series deletion capability existed). The delete handler is registered unconditionally (not viametrics.RegisterEvents) so cleanup stays symmetric with the per-poll recording, which is guarded dynamically bymetrics.Active(); it cleans up by object-source identity, which is always populated and is a no-op for non-EC2 backends.Change Type
/kind feature
Changelog
Additional Notes
promlint.ec2ConfigFromBackend(translation already fails closed for unresolved secrets).devel/architecture/metrics.mddocuments only patterns.