Skip to content

Commit

Permalink
hive,metrics: Add cell.Metric and metrics/metric package
Browse files Browse the repository at this point in the history
This commit adds the metric cell type which can be used to make metrics
available to hive. The cell works by giving it a constructor function
just like providing a normal type. The returned type should contain
public metric fields, the cell will enforce this. The cell will make
both the populated metric struct as well as all of the individual
metrics available in the DAG. The idea being that users would use the
struct so they can access just the metrics they are interested in. The
metrics registry will collect all individual metrics and register them.

We currently have the concept of metrics being enabled/disabled by
default unless configured otherwise. To support this concept in a
modular way we need additional metadata for metrics. Therefor all
metrics should be defined with the new `metric` package which contains
types that wrap existing prometheus metrics and it adds our own options
such as `Disabled` and `ConfigName`.

Signed-off-by: Dylan Reimerink <dylan.reimerink@isovalent.com>
  • Loading branch information
dylandreimerink committed Jun 2, 2023
1 parent 6c552a9 commit 84ea383
Show file tree
Hide file tree
Showing 8 changed files with 1,071 additions and 5 deletions.
138 changes: 138 additions & 0 deletions pkg/hive/cell/metric.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package cell

import (
"fmt"
"reflect"

"github.com/prometheus/client_golang/prometheus"
"go.uber.org/dig"

"github.com/cilium/cilium/pkg/hive/internal"
pkgmetric "github.com/cilium/cilium/pkg/metrics/metric"
)

var (
withMeta pkgmetric.WithMetadata
collector prometheus.Collector
)

// Metric constructs a new metric cell.
//
// This cell type provides `S` to the hive as returned by `ctor`, it also makes each individual field
// value available via the `hive-metrics` value group. Infrastructure components such as a registry,
// inspection tool, or documentation generator can collect all metrics in the hive via this value group.
//
// The `ctor` constructor must return a struct or pointer to a struct of type `S`. The returned struct
// must only contain public fields. All field types should implement the
// `github.com/cilium/cilium/pkg/metrics/metric.WithMetadata`
// and `github.com/prometheus/client_golang/prometheus.Collector` interfaces.
func Metric[S any](ctor func() S) Cell {
var nilOut S
outTyp := reflect.TypeOf(nilOut)
if outTyp.Kind() == reflect.Ptr {
outTyp = outTyp.Elem()
}

if outTyp.Kind() != reflect.Struct {
panic(fmt.Errorf(
"cell.Metric must be invoked with a constructor function that returns a struct or pointer to a struct, "+
"a constructor which returns a %s was supplied",
outTyp.Kind(),
))
}

// Let's be strict for now, could lift this in the future if we ever need to
if outTyp.NumField() == 0 {
panic(fmt.Errorf(
"cell.Metric must be invoked with a constructor function that returns exactly a struct with at least 1 " +
"metric, a constructor which returns a struct with zero fields was supplied",
))
}

withMetaTyp := reflect.TypeOf(&withMeta).Elem()
collectorTyp := reflect.TypeOf(&collector).Elem()
for i := 0; i < outTyp.NumField(); i++ {
field := outTyp.Field(i)
if !field.IsExported() {
panic(fmt.Errorf(
"The struct returned by the constructor passed to cell.Metric has a private field '%s', which "+
"is not allowed. All fields on the returning struct must be exported",
field.Name,
))
}

if !field.Type.Implements(withMetaTyp) {
panic(fmt.Errorf(
"The struct returned by the constructor passed to cell.Metric has a field '%s', which is not metric.WithMetadata.",
field.Name,
))
}

if !field.Type.Implements(collectorTyp) {
panic(fmt.Errorf(
"The struct returned by the constructor passed to cell.Metric has a field '%s', which is not prometheus.Collector.",
field.Name,
))
}
}

return &metric[S]{
ctor: ctor,
}
}

type metric[S any] struct {
ctor func() S
}

type metricOut struct {
dig.Out

Metrics []pkgmetric.WithMetadata `group:"hive-metrics,flatten"`
}

func (m *metric[S]) provideMetrics(metricSet S) metricOut {
var metrics []pkgmetric.WithMetadata

value := reflect.ValueOf(metricSet)
typ := value.Type()
if typ.Kind() == reflect.Pointer {
value = value.Elem()
typ = typ.Elem()
}

if typ.Kind() != reflect.Struct {
return metricOut{}
}

for i := 0; i < typ.NumField(); i++ {
if withMeta, ok := value.Field(i).Interface().(pkgmetric.WithMetadata); ok {
metrics = append(metrics, withMeta)
}
}

return metricOut{
Metrics: metrics,
}
}

func (m *metric[S]) Info(container) Info {
n := NewInfoNode(fmt.Sprintf("📈 %s", internal.FuncNameAndLocation(m.ctor)))
n.condensed = true

return n
}

func (m *metric[S]) Apply(container container) error {
// Provide the supplied constructor, so its return type is directly accessible by cells
container.Provide(m.ctor, dig.Export(true))

// Provide the metrics provider, which will take the return value of the constructor and turn it into a
// slice of metrics to be consumed by anyone interested in handling them.
container.Provide(m.provideMetrics, dig.Export(true))

return nil
}
3 changes: 2 additions & 1 deletion pkg/hive/example/hello_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ var helloHandlerCell = cell.Module(
cell.Provide(newHelloHandler),
)

func newHelloHandler() HTTPHandlerOut {
func newHelloHandler(ExampleMetrics exampleMetrics) HTTPHandlerOut {
return HTTPHandlerOut{
HTTPHandler: HTTPHandler{
Path: "/hello",
Handler: func(w http.ResponseWriter, req *http.Request) {
ExampleMetrics.ExampleCounter.Inc()
w.Write([]byte("Hello\n"))
},
},
Expand Down
9 changes: 5 additions & 4 deletions pkg/hive/example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ import (
var (
// Create a hive from a set of cells.
Hive = hive.New(
serverCell, // An HTTP server, depends on HTTPHandler's
eventsCell, // Example event source (ExampleEvents)
helloHandlerCell, // Handler for /hello
eventsHandlerCell, // Handler for /events
serverCell, // An HTTP server, depends on HTTPHandler's
eventsCell, // Example event source (ExampleEvents)
helloHandlerCell, // Handler for /hello
eventsHandlerCell, // Handler for /events
exampleMetricsCell, // Register the metrics

// Constructors are lazy and only invoked if they are a dependency
// to an "invoke" function or an indirect dependency of a constructor
Expand Down
27 changes: 27 additions & 0 deletions pkg/hive/example/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package main

import (
"github.com/cilium/cilium/pkg/hive/cell"
"github.com/cilium/cilium/pkg/metrics/metric"
)

var exampleMetricsCell = cell.Metric(newExampleMetrics)

type exampleMetrics struct {
ExampleCounter metric.Counter
}

func newExampleMetrics() exampleMetrics {
return exampleMetrics{
ExampleCounter: metric.NewCounter(metric.CounterOpts{
ConfigName: "misc_total",
Namespace: "cilium",
Subsystem: "example",
Name: "misc_total",
Help: "Counts miscellaneous events",
}),
}
}
166 changes: 166 additions & 0 deletions pkg/metrics/metric/counter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package metric

import (
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
)

func NewCounter(opts CounterOpts) Counter {
return &counter{
Counter: prometheus.NewCounter(opts.toPrometheus()),
metric: metric{
enabled: !opts.Disabled,
opts: Opts(opts),
},
}
}

type Counter interface {
prometheus.Counter
WithMetadata

Get() float64
}

type counter struct {
prometheus.Counter
metric
}

func (c *counter) Collect(metricChan chan<- prometheus.Metric) {
if c.enabled {
c.Counter.Collect(metricChan)
}
}

func (c *counter) Get() float64 {
var pm dto.Metric
err := c.Counter.Write(&pm)
if err == nil {
return *pm.Counter.Value
}
return 0
}

// Inc increments the counter by 1. Use Add to increment it by arbitrary
// non-negative values.
func (c *counter) Inc() {
if c.enabled {
c.Counter.Inc()
}
}

// Add adds the given value to the counter. It panics if the value is < 0.
func (c *counter) Add(val float64) {
if c.enabled {
c.Counter.Add(val)
}
}

func NewCounterVec(opts CounterOpts, labelNames []string) DeletableVec[Counter] {
return &counterVec{
CounterVec: prometheus.NewCounterVec(opts.toPrometheus(), labelNames),
metric: metric{
enabled: !opts.Disabled,
opts: Opts(opts),
},
}
}

type counterVec struct {
*prometheus.CounterVec
metric
}

func (cv *counterVec) CurryWith(labels prometheus.Labels) (Vec[Counter], error) {
vec, err := cv.CounterVec.CurryWith(labels)
if err == nil {
return &counterVec{CounterVec: vec, metric: cv.metric}, nil
}
return nil, err
}

func (cv *counterVec) GetMetricWith(labels prometheus.Labels) (Counter, error) {
if !cv.enabled {
return &counter{
metric: metric{enabled: false},
}, nil
}

promCounter, err := cv.CounterVec.GetMetricWith(labels)
if err == nil {
return &counter{
Counter: promCounter,
metric: cv.metric,
}, nil
}
return nil, err
}

func (cv *counterVec) GetMetricWithLabelValues(lvs ...string) (Counter, error) {
if !cv.enabled {
return &counter{
metric: metric{enabled: false},
}, nil
}

promCounter, err := cv.CounterVec.GetMetricWithLabelValues(lvs...)
if err == nil {
return &counter{
Counter: promCounter,
metric: cv.metric,
}, nil
}
return nil, err
}

func (cv *counterVec) With(labels prometheus.Labels) Counter {
if !cv.enabled {
return &counter{
metric: metric{enabled: false},
}
}

promCounter := cv.CounterVec.With(labels)
return &counter{
Counter: promCounter,
metric: cv.metric,
}
}

func (cv *counterVec) WithLabelValues(lvs ...string) Counter {
if !cv.enabled {
return &counter{
metric: metric{enabled: false},
}
}

promCounter := cv.CounterVec.WithLabelValues(lvs...)
return &counter{
Counter: promCounter,
metric: cv.metric,
}
}

func (cv *counterVec) SetEnabled(e bool) {
if !e {
cv.Reset()
}

cv.metric.SetEnabled(e)
}

type CounterOpts Opts

func (co CounterOpts) toPrometheus() prometheus.CounterOpts {
return prometheus.CounterOpts{
Name: co.Name,
Namespace: co.Namespace,
Subsystem: co.Subsystem,
Help: co.Help,
ConstLabels: co.ConstLabels,
}
}

0 comments on commit 84ea383

Please sign in to comment.