diff --git a/controllers/tenant/manager.go b/controllers/tenant/manager.go index 4cfba841..1ccb3e8b 100644 --- a/controllers/tenant/manager.go +++ b/controllers/tenant/manager.go @@ -20,6 +20,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" + "github.com/projectcapsule/capsule/pkg/metrics" ) type Manager struct { @@ -49,6 +50,10 @@ func (r Manager) Reconcile(ctx context.Context, request ctrl.Request) (result ct if apierrors.IsNotFound(err) { r.Log.Info("Request object not found, could have been deleted after reconcile request") + // If tenant was deleted or cannot be found, clean up metrics + metrics.TenantResourceUsage.DeletePartialMatch(map[string]string{"tenant": request.Name}) + metrics.TenantResourceLimit.DeletePartialMatch(map[string]string{"tenant": request.Name}) + return reconcile.Result{}, nil } diff --git a/controllers/tenant/resourcequotas.go b/controllers/tenant/resourcequotas.go index 5f5aca99..2e517404 100644 --- a/controllers/tenant/resourcequotas.go +++ b/controllers/tenant/resourcequotas.go @@ -23,6 +23,7 @@ import ( capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2" "github.com/projectcapsule/capsule/pkg/api" + "github.com/projectcapsule/capsule/pkg/metrics" "github.com/projectcapsule/capsule/pkg/utils" ) @@ -51,6 +52,18 @@ func (r *Manager) syncResourceQuotas(ctx context.Context, tenant *capsulev1beta2 if typeLabel, err = utils.GetTypeLabel(&corev1.ResourceQuota{}); err != nil { return err } + + // Remove prior metrics, to avoid cleaning up for metrics of deleted ResourceQuotas + metrics.TenantResourceUsage.DeletePartialMatch(map[string]string{"tenant": tenant.Name}) + metrics.TenantResourceLimit.DeletePartialMatch(map[string]string{"tenant": tenant.Name}) + + // Expose the namespace quota and usage as metrics for the tenant + metrics.TenantResourceUsage.WithLabelValues(tenant.Name, "namespaces", "").Set(float64(tenant.Status.Size)) + + if tenant.Spec.NamespaceOptions != nil && tenant.Spec.NamespaceOptions.Quota != nil { + metrics.TenantResourceLimit.WithLabelValues(tenant.Name, "namespaces", "").Set(float64(*tenant.Spec.NamespaceOptions.Quota)) + } + //nolint:nestif if tenant.Spec.ResourceQuota.Scope == api.ResourceQuotaScopeTenant { group := new(errgroup.Group) @@ -102,6 +115,19 @@ func (r *Manager) syncResourceQuotas(ctx context.Context, tenant *capsulev1beta2 r.Log.Info("Computed " + name.String() + " quota for the whole Tenant is " + quantity.String()) + // Expose usage and limit metrics for the resource (name) of the ResourceQuota (index) + metrics.TenantResourceUsage.WithLabelValues( + tenant.Name, + name.String(), + strconv.Itoa(index), + ).Set(float64(quantity.MilliValue()) / 1000) + + metrics.TenantResourceLimit.WithLabelValues( + tenant.Name, + name.String(), + strconv.Itoa(index), + ).Set(float64(hardQuota.MilliValue()) / 1000) + switch quantity.Cmp(resourceQuota.Hard[name]) { case 0: // The Tenant is matching exactly the Quota: diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go new file mode 100644 index 00000000..b9b06044 --- /dev/null +++ b/pkg/metrics/metrics.go @@ -0,0 +1,30 @@ +// Copyright 2020-2023 Project Capsule Authors. +// SPDX-License-Identifier: Apache-2.0 + +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "sigs.k8s.io/controller-runtime/pkg/metrics" +) + +var ( + metricsPrefix = "capsule_" + + TenantResourceUsage = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: metricsPrefix + "tenant_resource_usage", + Help: "Current resource usage for a given resource in a tenant", + }, []string{"tenant", "resource", "resourcequotaindex"}) + + TenantResourceLimit = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: metricsPrefix + "tenant_resource_limit", + Help: "Current resource limit for a given resource in a tenant", + }, []string{"tenant", "resource", "resourcequotaindex"}) +) + +func init() { + metrics.Registry.MustRegister( + TenantResourceUsage, + TenantResourceLimit, + ) +}