forked from cilium/cilium
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
hive,metrics: Add
cell.Metric
and metrics/metric
package
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
1 parent
6c552a9
commit 84ea383
Showing
8 changed files
with
1,071 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
}), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
} |
Oops, something went wrong.