package metrics import ( "context" "errors" "time" "github.com/prometheus/client_golang/prometheus" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/reference" ctrl "sigs.k8s.io/controller-runtime" ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" "edge-infra.dev/pkg/k8s/meta/status" "edge-infra.dev/pkg/k8s/runtime/conditions" ) // Metrics wraps the base Recorder to provide helper methods which automatically // get the object reference the metric is being recorded for and handles logging // of metric collection errors. type Metrics struct { Scheme *runtime.Scheme Recorder *Recorder } // New instantiates a Metrics struct with a new Recorder based on the options. // The metrics are registered with the controller-runtime metric registry. func New(mgr ctrl.Manager, prefix string, options ...Option) Metrics { rec := NewRecorder(prefix, options...) // Register the collectors with the prometheus registry found in k8s controller-runtime metrics package. ctrlmetrics.Registry.MustRegister(rec.Collectors()...) return Metrics{ Scheme: mgr.GetScheme(), Recorder: rec, } } func (m Metrics) RecordDuration(ctx context.Context, obj conditions.Getter, startTime time.Time) { if m.Recorder == nil { return } ref, err := reference.GetReference(m.Scheme, obj) if err != nil { ctrl.LoggerFrom(ctx).Error( err, "unable to get object reference to record duration", ) return } m.Recorder.RecordDuration(*ref, startTime) } // RecordSuspend records the suspension of the given obj based on the given suspend value. func (m Metrics) RecordSuspend(ctx context.Context, obj conditions.Getter, suspend bool) { if m.Recorder == nil { return } ref, err := reference.GetReference(m.Scheme, obj) if err != nil { ctrl.LoggerFrom(ctx).Error( err, "unable to get object reference to record suspension", ) return } if err := m.Recorder.RecordSuspend(*ref, suspend); err != nil { ctrl.LoggerFrom(ctx).Error( err, "unable to record suspension", ) return } } // RecordConditionsWithReason deletes all metrics based on the object's kind, name, and namespace // before recording the status of each active condition the object has. // The deletion behavior allows the recording function to record active conditions and its associated // values only, without keeping track of the explicit set of conditions and reasons the controller uses, // eliminating the need to set inactive condition-status-reason combinations. // NOTE: This function can only be used for controllers that pass `WithReason()` during setup. func (m Metrics) RecordConditionsWithReason(ctx context.Context, obj conditions.Getter) { if m.Recorder == nil { return } // Since the DeletePartialMatch happens within this function, gauge existence // also needs to be checked. if m.Recorder.reconcileConditionGaugeWithReason == nil { ctrl.LoggerFrom(ctx).Error( errors.New("gauge not set"), "reconcileConditionGaugeWithReason not set", ) return } ref, err := reference.GetReference(m.Scheme, obj) if err != nil { ctrl.LoggerFrom(ctx).Error( err, "unable to get object reference to record condition metric", ) return } labels := prometheus.Labels{ "kind": ref.Kind, "name": ref.Name, "namespace": ref.Namespace, } // Delete metrics for this object to reset the observed status and conditions m.Recorder.reconcileConditionGaugeWithReason.DeletePartialMatch(labels) conds := conditions.GetConditions(obj) for _, c := range conds { if err := m.Recorder.RecordConditionWithReason(*ref, c, !obj.GetDeletionTimestamp().IsZero()); err != nil { ctrl.LoggerFrom(ctx).Error( err, "unable to record condition", ) return } } } // RecordCondition records the status of the given conditionType for the given obj. func (m Metrics) RecordCondition(ctx context.Context, obj conditions.Getter, conditionType string) { if m.Recorder == nil { return } ref, err := reference.GetReference(m.Scheme, obj) if err != nil { ctrl.LoggerFrom(ctx).Error( err, "unable to get object reference to record condition metric", ) } rc := conditions.Get(obj, conditionType) if rc == nil { rc = conditions.UnknownCondition(conditionType, "", "") } if err := m.Recorder.RecordCondition(*ref, *rc, !obj.GetDeletionTimestamp().IsZero()); err != nil { ctrl.LoggerFrom(ctx).Error( err, "unable to record condition", ) return } } // RecordReadiness records the status.ReadyCondition status for the given obj. func (m Metrics) RecordReadiness(ctx context.Context, obj conditions.Getter) { m.RecordCondition(ctx, obj, status.ReadyCondition) } // RecordReconciling records the status.ReconcilingCondition status for the given obj. func (m Metrics) RecordReconciling(ctx context.Context, obj conditions.Getter) { m.RecordCondition(ctx, obj, status.ReconcilingCondition) } // RecordStalled records the status.StalledCondition status for the given obj. func (m Metrics) RecordStalled(ctx context.Context, obj conditions.Getter) { m.RecordCondition(ctx, obj, status.StalledCondition) }