diff --git a/metrics/metric_observer.go b/metrics/metric_observer.go index 594f0ea..cfb6751 100644 --- a/metrics/metric_observer.go +++ b/metrics/metric_observer.go @@ -6,32 +6,15 @@ import ( "github.com/openshift-online/async-routine" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" ) var _ async.RoutinesObserver = (*metricObserver)(nil) -type metricObserver struct{} - -var ( - runningRoutines = prometheus.NewGauge( - prometheus.GaugeOpts{ - Name: "async_routine_manager_routines", - Help: "Number of running routines.", - }, - ) - - runningRoutinesByName = prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Name: "async_routine_manager_routine_instances", - Help: "Number of running instances of a given routine.", - }, - []string{"routine_name", "data"}, - ) -) - -func init() { - prometheus.MustRegister(runningRoutines) - prometheus.MustRegister(runningRoutinesByName) +type metricObserver struct { + registerer prometheus.Registerer + runningRoutines prometheus.Gauge + runningRoutinesByName *prometheus.GaugeVec } func mapToString(m map[string]string) string { @@ -55,13 +38,13 @@ func mapToString(m map[string]string) string { } func (m *metricObserver) RoutineStarted(routine async.AsyncRoutine) { - runningRoutinesByName. + m.runningRoutinesByName. With(prometheus.Labels{"routine_name": routine.Name(), "data": mapToString(routine.GetData())}). Inc() } func (m *metricObserver) RoutineFinished(routine async.AsyncRoutine) { - runningRoutinesByName. + m.runningRoutinesByName. With(prometheus.Labels{"routine_name": routine.Name(), "data": mapToString(routine.GetData())}). Dec() } @@ -70,12 +53,46 @@ func (m *metricObserver) RoutineExceededTimebox(routine async.AsyncRoutine) { } func (m *metricObserver) RunningRoutineCount(count int) { - runningRoutines.Set(float64(count)) + m.runningRoutines.Set(float64(count)) } func (m *metricObserver) RunningRoutineByNameCount(name string, count int) { } -func NewMetricObserver() async.RoutinesObserver { - return &metricObserver{} +// MetricOption defines options for the metrics observer. +type MetricOption func(*metricObserver) + +// WithRegisterer configures the Prometheus registry where to register metrics. +func WithRegisterer(r prometheus.Registerer) MetricOption { + return func(observer *metricObserver) { + observer.registerer = r + } +} + +// NewMetricObserver returns an observer which tracks metrics for the async +// routines. +func NewMetricObserver(opts ...MetricOption) async.RoutinesObserver { + observer := &metricObserver{ + registerer: prometheus.DefaultRegisterer, + } + + for _, opt := range opts { + opt(observer) + } + + observer.runningRoutines = promauto.With(observer.registerer).NewGauge( + prometheus.GaugeOpts{ + Name: "async_routine_manager_routines", + Help: "Number of running routines.", + }, + ) + observer.runningRoutinesByName = promauto.With(observer.registerer).NewGaugeVec( + prometheus.GaugeOpts{ + Name: "async_routine_manager_routine_instances", + Help: "Number of running instances of a given routine.", + }, + []string{"routine_name", "data"}, + ) + + return observer } diff --git a/metrics/metric_observer_suite_test.go b/metrics/metric_observer_suite_test.go new file mode 100644 index 0000000..63369c6 --- /dev/null +++ b/metrics/metric_observer_suite_test.go @@ -0,0 +1,13 @@ +package metrics + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestMetricsObserver(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Metrics Suite") +} diff --git a/metrics/metric_observer_test.go b/metrics/metric_observer_test.go index e74d404..fc2ed28 100644 --- a/metrics/metric_observer_test.go +++ b/metrics/metric_observer_test.go @@ -1,19 +1,124 @@ package metrics import ( - "testing" + "bytes" + . "github.com/onsi/ginkgo/v2/dsl/core" + . "github.com/onsi/ginkgo/v2/dsl/table" + . "github.com/onsi/gomega" + "github.com/openshift-online/async-routine" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" + "go.uber.org/mock/gomock" ) -func TestPrometheusMetrics(t *testing.T) { - problems, err := testutil.GatherAndLint(prometheus.DefaultGatherer) - if err != nil { - t.Fatal(err) - } +var _ = Describe("Metrics Observer", func() { + DescribeTable("Updates metrics", + func(updateFn func(async.RoutinesObserver), expected string) { + registry := prometheus.NewPedanticRegistry() + observer := NewMetricObserver(WithRegisterer(registry)) - for _, p := range problems { - t.Errorf("found linting issue: %s: %s", p.Metric, p.Text) - } -} + updateFn(observer) + + err := testutil.GatherAndCompare(registry, bytes.NewBufferString(expected)) + Expect(err).NotTo(HaveOccurred()) + + problems, err := testutil.GatherAndLint(registry) + Expect(err).NotTo(HaveOccurred()) + Expect(problems).To(BeEmpty()) + }, + Entry("from initial state", + func(async.RoutinesObserver) {}, + `# HELP async_routine_manager_routines Number of running routines. +# TYPE async_routine_manager_routines gauge +async_routine_manager_routines 0 +`, + ), + Entry("when 1 routine is started", + func(observer async.RoutinesObserver) { + ctrl := gomock.NewController(GinkgoT()) + + routine := async.NewMockAsyncRoutine(ctrl) + routine.EXPECT(). + Name(). + Return("test"). + Times(1) + routine.EXPECT(). + GetData(). + Return(nil). + Times(1) + + observer.RoutineStarted(routine) + observer.RunningRoutineCount(1) + }, + `# HELP async_routine_manager_routine_instances Number of running instances of a given routine. +# TYPE async_routine_manager_routine_instances gauge +async_routine_manager_routine_instances{data="",routine_name="test"} 1 +# HELP async_routine_manager_routines Number of running routines. +# TYPE async_routine_manager_routines gauge +async_routine_manager_routines 1 +`), + Entry("when 2 routines (1 with data) are started", + func(observer async.RoutinesObserver) { + ctrl := gomock.NewController(GinkgoT()) + + routine := async.NewMockAsyncRoutine(ctrl) + routine.EXPECT(). + Name(). + Return("test"). + Times(1) + routine.EXPECT(). + GetData(). + Return(nil). + Times(1) + observer.RoutineStarted(routine) + + routine2 := async.NewMockAsyncRoutine(ctrl) + routine2.EXPECT(). + Name(). + Return("test2") + routine2.EXPECT(). + GetData(). + Return(map[string]string{"foo": "bar"}). + Times(1) + + observer.RoutineStarted(routine2) + observer.RunningRoutineCount(2) + }, + `# HELP async_routine_manager_routine_instances Number of running instances of a given routine. +# TYPE async_routine_manager_routine_instances gauge +async_routine_manager_routine_instances{data="",routine_name="test"} 1 +async_routine_manager_routine_instances{data="foo=bar",routine_name="test2"} 1 +# HELP async_routine_manager_routines Number of running routines. +# TYPE async_routine_manager_routines gauge +async_routine_manager_routines 2 +`, + ), + Entry("when 1 routine is started then stopped", + func(observer async.RoutinesObserver) { + ctrl := gomock.NewController(GinkgoT()) + + routine := async.NewMockAsyncRoutine(ctrl) + routine.EXPECT(). + Name(). + Return("test"). + Times(2) + routine.EXPECT(). + GetData(). + Return(nil). + Times(2) + + observer.RoutineStarted(routine) + observer.RoutineFinished(routine) + observer.RunningRoutineCount(0) + }, + `# HELP async_routine_manager_routine_instances Number of running instances of a given routine. +# TYPE async_routine_manager_routine_instances gauge +async_routine_manager_routine_instances{data="",routine_name="test"} 0 +# HELP async_routine_manager_routines Number of running routines. +# TYPE async_routine_manager_routines gauge +async_routine_manager_routines 0 +`, + ), + ) +})