From 597341aac009f16ad1f0b141a121b8ab51f8a6fd Mon Sep 17 00:00:00 2001 From: Ryota Arai Date: Tue, 16 Sep 2025 09:28:55 +0900 Subject: [PATCH 1/4] feat: Expose HRQ limit and usage on metrics endpoint. --- internal/hrq/metrics.go | 81 +++++++++++++++++++++++++++++++++++ internal/setup/reconcilers.go | 4 ++ 2 files changed, 85 insertions(+) create mode 100644 internal/hrq/metrics.go diff --git a/internal/hrq/metrics.go b/internal/hrq/metrics.go new file mode 100644 index 000000000..1b7843d5b --- /dev/null +++ b/internal/hrq/metrics.go @@ -0,0 +1,81 @@ +package hrq + +import ( + "context" + "time" + + "github.com/go-logr/logr" + "github.com/prometheus/client_golang/prometheus" + corev1 "k8s.io/api/core/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/metrics" + + api "sigs.k8s.io/hierarchical-namespaces/api/v1alpha2" +) + +func RegisterMetrics(mgr ctrl.Manager) error { + if err := metrics.Registry.Register(&hrqCollector{ + client: mgr.GetClient(), + logger: mgr.GetLogger().WithValues("collector", "hrqCollector"), + timeout: time.Second * 10, + }); err != nil { + return err + } + return nil +} + +type hrqCollector struct { + timeout time.Duration + client client.Client + logger logr.Logger +} + +func (c *hrqCollector) desc() *prometheus.Desc { + return prometheus.NewDesc( + "hnc_hierarchicalresourcequota", + "HRQ hard/used like kube_resourcequota", + []string{"namespace", "hrq", "resource", "type"}, + nil, + ) +} + +func (c *hrqCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- c.desc() +} + +func (c *hrqCollector) Collect(ch chan<- prometheus.Metric) { + ctx, cancel := context.WithTimeout(context.Background(), c.timeout) + defer cancel() + + var hrqs api.HierarchicalResourceQuotaList + if err := c.client.List(ctx, &hrqs); err != nil { + c.logger.Error(err, "Failed to list HRQs during metrics collection") + return + } + + for _, hrq := range hrqs.Items { + for typeLabel, resList := range map[string]corev1.ResourceList{ + "hard": hrq.Status.Hard, + "used": hrq.Status.Used, + } { + for res, qty := range resList { + v, ok := qty.AsInt64() + if !ok { + c.logger.Info("Failed to get the quantity value", "namespace", hrq.Namespace, "hrq", hrq.Name, "resource", res) + continue + } + + ch <- prometheus.MustNewConstMetric( + c.desc(), + prometheus.GaugeValue, + float64(v), + hrq.Namespace, + hrq.Name, + string(res), + typeLabel, + ) + } + } + } +} diff --git a/internal/setup/reconcilers.go b/internal/setup/reconcilers.go index 7b1419992..133f929d5 100644 --- a/internal/setup/reconcilers.go +++ b/internal/setup/reconcilers.go @@ -109,6 +109,10 @@ func CreateReconcilers(mgr ctrl.Manager, f *forest.Forest, opts Options) error { if opts.HRQSyncInterval != 0 { go watchHRQDrift(f, opts.HRQSyncInterval, hrqr) } + + if err := hrq.RegisterMetrics(mgr); err != nil { + return fmt.Errorf("cannot register HRQ metrics: %w", err) + } } if err := ar.SetupWithManager(mgr); err != nil { From 58f71bd5be82627deb654cf7425bdd258eaecc66 Mon Sep 17 00:00:00 2001 From: Ryota Arai Date: Thu, 2 Oct 2025 00:56:38 +0900 Subject: [PATCH 2/4] docs: Add metrics documentation for HierarchicalResourceQuotas --- docs/user-guide/metrics.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 docs/user-guide/metrics.md diff --git a/docs/user-guide/metrics.md b/docs/user-guide/metrics.md new file mode 100644 index 000000000..87bb2cb74 --- /dev/null +++ b/docs/user-guide/metrics.md @@ -0,0 +1,27 @@ +# Metrics + +The metrics endpoint is exposed on `:8080` by default (customizable with the `-metrics-addr` flag). + +## Metric List + +### `hnc_hierarchicalresourcequota` + +This metric exposes resource limits and usage for HierarchicalResourceQuotas. + +#### Labels + +- `hrq`: Name of the HierarchicalResourceQuota +- `namespace`: Namespace of the HierarchicalResourceQuota +- `resource`: Resource type (e.g., `cpu`, `memory`, `pods`) +- `type`: Either `hard` (limit) or `used` (current usage) + +#### Example + +``` +# HELP hnc_hierarchicalresourcequota HRQ hard/used like kube_resourcequota +# TYPE hnc_hierarchicalresourcequota gauge +hnc_hierarchicalresourcequota{hrq="team-quota",namespace="team-a",resource="cpu",type="hard"} 100 +hnc_hierarchicalresourcequota{hrq="team-quota",namespace="team-a",resource="cpu",type="used"} 45 +hnc_hierarchicalresourcequota{hrq="team-quota",namespace="team-a",resource="memory",type="hard"} 536870912 +hnc_hierarchicalresourcequota{hrq="team-quota",namespace="team-a",resource="memory",type="used"} 268435456 +``` From 512aa2bd2fa5aa9323ac0c7e5bf2f26ae4cf5483 Mon Sep 17 00:00:00 2001 From: Ryota Arai Date: Thu, 16 Oct 2025 22:18:04 +0900 Subject: [PATCH 3/4] Use AsApproximateFloat64 to get the value. Co-authored-by: Toru Komatsu --- internal/hrq/metrics.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/hrq/metrics.go b/internal/hrq/metrics.go index 1b7843d5b..32fbadbd6 100644 --- a/internal/hrq/metrics.go +++ b/internal/hrq/metrics.go @@ -60,7 +60,7 @@ func (c *hrqCollector) Collect(ch chan<- prometheus.Metric) { "used": hrq.Status.Used, } { for res, qty := range resList { - v, ok := qty.AsInt64() + v := qty.AsApproximateFloat64() if !ok { c.logger.Info("Failed to get the quantity value", "namespace", hrq.Namespace, "hrq", hrq.Name, "resource", res) continue From a43d5391f7630226ec0c432fc3d8c8494e28febe Mon Sep 17 00:00:00 2001 From: Ryota Arai Date: Thu, 16 Oct 2025 22:19:47 +0900 Subject: [PATCH 4/4] fix: Remove unnecessary error handling for quantity value in HRQ metrics collection --- internal/hrq/metrics.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/internal/hrq/metrics.go b/internal/hrq/metrics.go index 32fbadbd6..1180362da 100644 --- a/internal/hrq/metrics.go +++ b/internal/hrq/metrics.go @@ -60,12 +60,7 @@ func (c *hrqCollector) Collect(ch chan<- prometheus.Metric) { "used": hrq.Status.Used, } { for res, qty := range resList { - v := qty.AsApproximateFloat64() - if !ok { - c.logger.Info("Failed to get the quantity value", "namespace", hrq.Namespace, "hrq", hrq.Name, "resource", res) - continue - } - + v := qty.AsApproximateFloat64() ch <- prometheus.MustNewConstMetric( c.desc(), prometheus.GaugeValue,