Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ require (
github.com/openshift/api v0.0.0-20240926211938-f89ab92f1597
github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.76.2
github.com/prometheus/client_golang v1.23.0
github.com/prometheus/client_model v0.6.2
github.com/samber/lo v1.47.0
go.opentelemetry.io/contrib/bridges/otelzap v0.13.0
go.opentelemetry.io/contrib/bridges/prometheus v0.63.0
Expand Down Expand Up @@ -87,7 +88,6 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
Expand Down
26 changes: 26 additions & 0 deletions internal/controller/kuadrant_status_updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
kuadrantv1beta1 "github.com/kuadrant/kuadrant-operator/api/v1beta1"
"github.com/kuadrant/kuadrant-operator/internal/authorino"
"github.com/kuadrant/kuadrant-operator/internal/kuadrant"
operatormetrics "github.com/kuadrant/kuadrant-operator/internal/metrics"
)

const (
Expand Down Expand Up @@ -67,11 +68,23 @@ func (r *KuadrantStatusUpdater) Reconcile(ctx context.Context, _ []controller.Re

kObj := GetKuadrantFromTopology(topology)
if kObj == nil {
operatormetrics.SetKuadrantExists(false)
operatormetrics.ResetKuadrantMetrics()
return nil
}

// Kuadrant CR exists in the cluster
operatormetrics.SetKuadrantExists(true)

newStatus := r.calculateStatus(topology, logger, kObj, state)

// Emit Kuadrant readiness metric
isReady := meta.IsStatusConditionTrue(newStatus.Conditions, ReadyConditionType)
operatormetrics.SetKuadrantReady(kObj.Namespace, kObj.Name, isReady)

// Emit component readiness metrics
r.emitComponentMetrics(topology, kObj.Namespace, logger)

equalStatus := kObj.Status.Equals(newStatus, logger)
logger.V(1).Info("Status", "status is different", !equalStatus)
logger.V(1).Info("Status", "generation is different", kObj.Generation != kObj.Status.ObservedGeneration)
Expand Down Expand Up @@ -241,3 +254,16 @@ func checkAuthorinoAvailable(topology *machinery.Topology, logger logr.Logger) *

return nil
}

// emitComponentMetrics emits readiness metrics for Kuadrant-managed components (Authorino and Limitador).
// This is called during reconciliation when a Kuadrant CR exists to provide real-time visibility into component health.
// When no CR exists, component metrics are cleared via ResetKuadrantMetrics() instead.
func (r *KuadrantStatusUpdater) emitComponentMetrics(topology *machinery.Topology, namespace string, logger logr.Logger) {
// Check Authorino readiness
authorinoReady := checkAuthorinoAvailable(topology, logger) == nil
operatormetrics.SetComponentReady("authorino", namespace, authorinoReady)

// Check Limitador readiness
limitadorReady := checkLimitadorReady(topology, logger) == nil
operatormetrics.SetComponentReady("limitador", namespace, limitadorReady)
}
147 changes: 147 additions & 0 deletions internal/controller/policy_metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
Copyright 2025.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controllers

import (
"context"
"sync"

"github.com/prometheus/client_golang/prometheus"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/metrics"

"github.com/kuadrant/policy-machinery/controller"
"github.com/kuadrant/policy-machinery/machinery"

kuadrantgatewayapi "github.com/kuadrant/kuadrant-operator/internal/gatewayapi"
"github.com/kuadrant/kuadrant-operator/internal/kuadrant"
)

const (
policyKindLabel = "kind"
policyStatusLabel = "status"
)

var (
// policiesTotal tracks the total number of policies by kind
policiesTotal = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "kuadrant_policies_total",
Help: "Total number of Kuadrant policies by kind",
},
[]string{policyKindLabel})

// policiesEnforced tracks the enforcement status of policies
policiesEnforced = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "kuadrant_policies_enforced",
Help: "Number of Kuadrant policies by kind and enforcement status",
},
[]string{policyKindLabel, policyStatusLabel})
)

// PolicyStatus represents the enforcement status of a policy
type PolicyStatus string

const (
PolicyStatusTrue PolicyStatus = "true"
PolicyStatusFalse PolicyStatus = "false"
)

// PolicyMetricsReconciler emits Prometheus metrics for all Kuadrant policies
type PolicyMetricsReconciler struct{}

// NewPolicyMetricsReconciler creates a new PolicyMetricsReconciler
func NewPolicyMetricsReconciler() *PolicyMetricsReconciler {
return &PolicyMetricsReconciler{}
}

// Reconcile collects and emits metrics for all policies in the topology.
// This reconciler automatically discovers and tracks all policy types by grouping policies by their Kind.
// Currently includes core policies: AuthPolicy, RateLimitPolicy, DNSPolicy, TLSPolicy, and TokenRateLimitPolicy.
// Note: Extension policies (OIDCPolicy, PlanPolicy, TelemetryPolicy) are not part of the topology and are not tracked.
func (r *PolicyMetricsReconciler) Reconcile(ctx context.Context, _ []controller.ResourceEvent, topology *machinery.Topology, _ error, _ *sync.Map) error {
logger := controller.LoggerFromContext(ctx).WithName("policy_metrics").WithValues("context", ctx)

// Reset all metrics to zero before recalculating
policiesTotal.Reset()
policiesEnforced.Reset()

// Group all policies by kind for automatic discovery
policiesByKind := make(map[string][]machinery.Policy)

for _, policy := range topology.Policies().Items() {
kind := policy.GroupVersionKind().Kind
policiesByKind[kind] = append(policiesByKind[kind], policy)
}

// Emit metrics for each discovered policy kind
for kind, policies := range policiesByKind {
r.emitMetricsForPolicies(kind, policies)
}

logger.V(1).Info("policy metrics updated", "policyKinds", len(policiesByKind))
return nil
}

// emitMetricsForPolicies emits metrics for a list of policies of a given kind
func (r *PolicyMetricsReconciler) emitMetricsForPolicies(kind string, policies []machinery.Policy) {
total := len(policies)
policiesTotal.WithLabelValues(kind).Set(float64(total))

// Track enforcement status counts
enforcedCounts := map[PolicyStatus]int{
PolicyStatusTrue: 0,
PolicyStatusFalse: 0,
}

for _, policy := range policies {
status := r.getEnforcementStatus(policy)
enforcedCounts[status]++
}

// Emit enforcement metrics
for status, count := range enforcedCounts {
policiesEnforced.WithLabelValues(kind, string(status)).Set(float64(count))
}
}

// getEnforcementStatus returns the enforcement status of a policy based on its Enforced condition.
// A policy is considered enforced (true) only when it has an Enforced condition with status True.
// All other cases (no condition, condition False, condition Unknown, or unable to read status) are
// treated as not enforced (false).
func (r *PolicyMetricsReconciler) getEnforcementStatus(policy machinery.Policy) PolicyStatus {
policyWithStatusObj, ok := policy.(kuadrantgatewayapi.Policy)
if !ok {
return PolicyStatusFalse
}

conditions := policyWithStatusObj.GetStatus().GetConditions()
enforcedCondition := meta.FindStatusCondition(conditions, string(kuadrant.PolicyConditionEnforced))

if enforcedCondition == nil || enforcedCondition.Status != metav1.ConditionTrue {
return PolicyStatusFalse
}

return PolicyStatusTrue
}

func init() {
// Register metrics with controller-runtime's Prometheus registry
metrics.Registry.MustRegister(policiesTotal, policiesEnforced)
}
132 changes: 132 additions & 0 deletions internal/controller/policy_metrics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
Copyright 2025.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controllers

import (
"testing"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
gatewayapiv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"

kuadrantv1 "github.com/kuadrant/kuadrant-operator/api/v1"
"github.com/kuadrant/kuadrant-operator/internal/kuadrant"
)

func TestGetEnforcementStatus(t *testing.T) {
reconciler := NewPolicyMetricsReconciler()

tests := []struct {
name string
policy *kuadrantv1.AuthPolicy
expectedStatus PolicyStatus
}{
{
name: "enforced condition is true",
policy: &kuadrantv1.AuthPolicy{
Status: kuadrantv1.AuthPolicyStatus{
Conditions: []metav1.Condition{
{
Type: string(kuadrant.PolicyConditionEnforced),
Status: metav1.ConditionTrue,
Reason: string(kuadrant.PolicyReasonEnforced),
},
},
},
},
expectedStatus: PolicyStatusTrue,
},
{
name: "enforced condition is false",
policy: &kuadrantv1.AuthPolicy{
Status: kuadrantv1.AuthPolicyStatus{
Conditions: []metav1.Condition{
{
Type: string(kuadrant.PolicyConditionEnforced),
Status: metav1.ConditionFalse,
Reason: string(gatewayapiv1alpha2.PolicyReasonInvalid),
},
},
},
},
expectedStatus: PolicyStatusFalse,
},
{
name: "enforced condition is unknown - treated as false",
policy: &kuadrantv1.AuthPolicy{
Status: kuadrantv1.AuthPolicyStatus{
Conditions: []metav1.Condition{
{
Type: string(kuadrant.PolicyConditionEnforced),
Status: metav1.ConditionUnknown,
Reason: string(kuadrant.PolicyReasonUnknown),
},
},
},
},
expectedStatus: PolicyStatusFalse,
},
{
name: "no enforced condition",
policy: &kuadrantv1.AuthPolicy{
Status: kuadrantv1.AuthPolicyStatus{
Conditions: []metav1.Condition{
{
Type: string(gatewayapiv1alpha2.PolicyConditionAccepted),
Status: metav1.ConditionTrue,
Reason: string(gatewayapiv1alpha2.PolicyReasonAccepted),
},
},
},
},
expectedStatus: PolicyStatusFalse,
},
{
name: "empty status",
policy: &kuadrantv1.AuthPolicy{},
expectedStatus: PolicyStatusFalse,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
status := reconciler.getEnforcementStatus(tt.policy)
if status != tt.expectedStatus {
t.Errorf("expected status %v, got %v", tt.expectedStatus, status)
}
})
}
}

func TestPolicyStatusConstants(t *testing.T) {
// Verify the policy status constants are correctly defined
if PolicyStatusTrue != "true" {
t.Errorf("expected PolicyStatusTrue to be 'true', got %s", PolicyStatusTrue)
}
if PolicyStatusFalse != "false" {
t.Errorf("expected PolicyStatusFalse to be 'false', got %s", PolicyStatusFalse)
}
}

func TestMetricLabels(t *testing.T) {
// Verify the metric label constants
if policyKindLabel != "kind" {
t.Errorf("expected policyKindLabel to be 'kind', got %s", policyKindLabel)
}
if policyStatusLabel != "status" {
t.Errorf("expected policyStatusLabel to be 'status', got %s", policyStatusLabel)
}
}
Loading
Loading