diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index 20d1a9bcc4..ae3b5cfaf4 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -29,6 +29,7 @@ import ( customresource "github.com/kanisterio/kanister/pkg/customresource" "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" "gopkg.in/tomb.v2" corev1 "k8s.io/api/core/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -48,6 +49,7 @@ import ( "github.com/kanisterio/kanister/pkg/eventer" "github.com/kanisterio/kanister/pkg/field" "github.com/kanisterio/kanister/pkg/log" + "github.com/kanisterio/kanister/pkg/metrics" "github.com/kanisterio/kanister/pkg/param" "github.com/kanisterio/kanister/pkg/progress" "github.com/kanisterio/kanister/pkg/reconcile" @@ -60,6 +62,7 @@ type Controller struct { config *rest.Config crClient versioned.Interface clientset kubernetes.Interface + counterVecs map[metrics.MetricType]*prometheus.CounterVec dynClient dynamic.Interface osClient osversioned.Interface recorder record.EventRecorder @@ -69,7 +72,8 @@ type Controller struct { // New create controller for watching kanister custom resources created func New(c *rest.Config) *Controller { return &Controller{ - config: c, + config: c, + counterVecs: metrics.InitAllCounterVecs(prometheus.DefaultRegisterer), } } @@ -364,6 +368,17 @@ func (c *Controller) handleActionSet(as *crv1alpha1.ActionSet) (err error) { if as.Status.State != crv1alpha1.StatePending { return nil } + + // ActionSet created + // Ideally a function such a getStatus() should return the labels corresponding to the current status of the system. + // These labels will be passed to a function in the metrics package + // Also, a list of events to update metrics should be created and incremented. + for _, a := range as.Spec.Actions { + if err := metrics.IncrementCounterVec(metrics.NewActionSetCreatedTotal(a.Name, as.GetNamespace())); err != nil { + log.Error().WithError(err).Print("Metrics Incrementation failed") + } + } + as.Status.State = crv1alpha1.StateRunning if as, err = c.crClient.CrV1alpha1().ActionSets(as.GetNamespace()).Update(context.TODO(), as, v1.UpdateOptions{}); err != nil { return errors.WithStack(err) diff --git a/pkg/metrics/events.go b/pkg/metrics/events.go new file mode 100644 index 0000000000..75e5e40ee1 --- /dev/null +++ b/pkg/metrics/events.go @@ -0,0 +1,107 @@ +package metrics + +// This file contains wrapper functions that will map a Prometheus metric names to its +// label field, help field and an associated Event. + +// MetricType will represent a Prometheus metric. +// A variable of type MetricType will hold the name of the Prometheus metric as reported. +type MetricType string + +const ( + ActionSetCreatedTotalType MetricType = "kanister_actionset_created_total" + ActionSetCompletedTotalType MetricType = "kanister_actionset_completed_total" + ActionSetFailedTotalType MetricType = "kanister_actionset_failed_total" +) + +// MetricTypeOpt is a struct for a Prometheus metric. +// Help and LabelNames are passed directly to the Prometheus predefined functions. +// EventFunc holds the constructor of the linked Event of a given MetricType. +type MetricTypeOpt struct { + EventFunc interface{} + Help string + LabelNames []string +} + +// Mapping a Prometheus MetricType to the metric MetricTypeOpt struct. +// Basically, a metric name is mapped to its associated Help and LabelName fields. +// The linked event function (EventFunc) is also mapped to this metric name as a part of MetricTypeOpt. +var MetricCounterOpts = map[MetricType]MetricTypeOpt{ + ActionSetCreatedTotalType: { + EventFunc: NewActionSetCreatedTotal, + Help: "The count of total ActionSets created", + LabelNames: []string{"actionType", "namespace"}, + }, + ActionSetCompletedTotalType: { + EventFunc: NewActionSetCompletedTotal, + Help: "The count of total ActionSets completed", + LabelNames: []string{"actionName", "actionType", "blueprint", "namespace", "state"}, + }, + ActionSetFailedTotalType: { + EventFunc: NewActionSetFailedTotal, + Help: "The count of total ActionSets failed", + LabelNames: []string{"actionName", "actionType", "blueprint", "namespace", "state"}, + }, +} + +// Event describes an individual event. +// eventType is the MetricType with which the individial Event is associated. +// Labels are the metric labels that will be passed to Prometheus. +// +// Note: The type and labels are private in order to force the use of the +// event constructors below. This helps to prevent an event from being +// accidentally misconstructed (e.g. with mismatching labels), which would +// cause the Prometheus library to panic. +type Event struct { + eventType MetricType + labels map[string]string +} + +// MetricType returns the event's type. +func (e *Event) Type() MetricType { + return e.eventType +} + +// Labels returns a copy of the event's labels. +func (e *Event) Labels() map[string]string { + labels := make(map[string]string) + for k, v := range e.labels { + labels[k] = v + } + return labels +} + +func NewActionSetCreatedTotal(actionType string, namespace string) Event { + return Event{ + eventType: ActionSetCreatedTotalType, + labels: map[string]string{ + "actionType": actionType, + "namespace": namespace, + }, + } +} + +func NewActionSetCompletedTotal(actionName string, actionType string, blueprint string, namespace string, state string) Event { + return Event{ + eventType: ActionSetCompletedTotalType, + labels: map[string]string{ + "actionName": actionName, + "actionType": actionType, + "blueprint": blueprint, + "namespace": namespace, + "state": state, + }, + } +} + +func NewActionSetFailedTotal(actionName string, actionType string, blueprint string, namespace string, state string) Event { + return Event{ + eventType: ActionSetFailedTotalType, + labels: map[string]string{ + "actionName": actionName, + "actionType": actionType, + "blueprint": blueprint, + "namespace": namespace, + "state": state, + }, + } +} diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go new file mode 100644 index 0000000000..dd041bef4c --- /dev/null +++ b/pkg/metrics/metrics.go @@ -0,0 +1,65 @@ +package metrics + +import ( + "errors" + "fmt" + + "github.com/kanisterio/kanister/pkg/log" + "github.com/prometheus/client_golang/prometheus" +) + +var counterVecs = make(map[MetricType]*prometheus.CounterVec) + +// Initialize a Prometheus CounterVec for one metric and register it +func initCounterVec(r prometheus.Registerer, t MetricType) (*prometheus.CounterVec, error) { + metricTypeOpts, ok := MetricCounterOpts[t] + + if !ok { + return nil, fmt.Errorf("Event type %s is not defined", t) + } + + opts := prometheus.CounterOpts{ + Name: string(t), + Help: metricTypeOpts.Help, + } + counterVec := prometheus.NewCounterVec(opts, metricTypeOpts.LabelNames) + + err := r.Register(counterVec) + if err != nil { + return nil, fmt.Errorf("%s not registered: %s ", t, err) + } + var alreadyRegisteredErr prometheus.AlreadyRegisteredError + if errors.As(err, &alreadyRegisteredErr) { + counterVec = alreadyRegisteredErr.ExistingCollector.(*prometheus.CounterVec) + } else if err != nil { + return nil, fmt.Errorf("Error registering Counter Vecs : %s ", err) + } + + return counterVec, nil +} + +// Initialize all the Counter Vecs and save it in a map +func InitAllCounterVecs(r prometheus.Registerer) map[MetricType]*prometheus.CounterVec { + for metricType := range MetricCounterOpts { + cv, err := initCounterVec(r, metricType) + if err != nil { + log.WithError(err).Print("Failed to register metric %s") + return nil + } + counterVecs[metricType] = cv + } + return counterVecs +} + +// Increment a Counter Vec metric +func IncrementCounterVec(e Event) error { + if counterVecs[e.eventType] == nil { + return fmt.Errorf("%s Event Type not found", e.eventType) + } + cv := counterVecs[e.eventType].With(e.labels) + if cv == nil { + return fmt.Errorf("%s Labels for %s Event Type not found", e.labels, e.eventType) + } + cv.Inc() + return nil +}