Skip to content

Commit

Permalink
Extend metrics library
Browse files Browse the repository at this point in the history
Details of the goals and implementation of the metrics library are documented
in pkg/metrics/doc.go and in the code doc comments.

Fixes: cilium#2376

Signed-off-by: Anna Kapuscinska <[email protected]>
  • Loading branch information
lambdanis committed Jun 21, 2024
1 parent dd602b0 commit cc929de
Show file tree
Hide file tree
Showing 9 changed files with 966 additions and 78 deletions.
48 changes: 31 additions & 17 deletions pkg/metrics/bpfmetric.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,59 @@

package metrics

import "github.com/prometheus/client_golang/prometheus"
import (
"github.com/prometheus/client_golang/prometheus"
)

// The interface in this file provides a bridge between the new metrics library
// and the existing code defining metrics. It's considered deprecated - use the
// interface from custommetric.go instead.

// BPFMetric represents a metric read directly from a BPF map.
// It's intended to be used in custom collectors. The interface doesn't provide
// any validation, so it's up to the collector implementer to guarantee the
// metrics consistency.
type BPFMetric interface {
Desc() *prometheus.Desc
MustMetric(value float64, labelValues ...string) prometheus.Metric
}

type bpfCounter struct {
desc *prometheus.Desc
metric *granularCustomCounter[NilLabels]
}

// DEPRECATED: Use NewGranularCustomCounter instead.
func NewBPFCounter(desc *prometheus.Desc) BPFMetric {
return &bpfCounter{desc: desc}
return &bpfCounter{
metric: &granularCustomCounter[NilLabels]{
desc: desc,
constrained: false,
},
}
}

func (c *bpfCounter) Desc() *prometheus.Desc {
return c.desc
func (m *bpfCounter) Desc() *prometheus.Desc {
return m.metric.Desc()
}

func (c *bpfCounter) MustMetric(value float64, labelValues ...string) prometheus.Metric {
return prometheus.MustNewConstMetric(c.desc, prometheus.CounterValue, value, labelValues...)
func (m *bpfCounter) MustMetric(value float64, labelValues ...string) prometheus.Metric {
return m.metric.MustMetric(value, &NilLabels{}, labelValues...)
}

type bpfGauge struct {
desc *prometheus.Desc
metric *granularCustomGauge[NilLabels]
}

// DEPRECATED: Use NewGranularCustomGauge instead.
func NewBPFGauge(desc *prometheus.Desc) BPFMetric {
return &bpfGauge{desc: desc}
return &bpfGauge{
metric: &granularCustomGauge[NilLabels]{
desc: desc,
constrained: false,
},
}
}

func (g *bpfGauge) Desc() *prometheus.Desc {
return g.desc
func (m *bpfGauge) Desc() *prometheus.Desc {
return m.metric.Desc()
}

func (g *bpfGauge) MustMetric(value float64, labelValues ...string) prometheus.Metric {
return prometheus.MustNewConstMetric(g.desc, prometheus.GaugeValue, value, labelValues...)
func (m *bpfGauge) MustMetric(value float64, labelValues ...string) prometheus.Metric {
return m.metric.MustMetric(value, &NilLabels{}, labelValues...)
}
67 changes: 67 additions & 0 deletions pkg/metrics/customcollector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Tetragon

package metrics

import (
"github.com/prometheus/client_golang/prometheus"
)

type collectFunc func(chan<- prometheus.Metric)

type customCollector[L FilteredLabels] struct {
metrics []GranularCustomMetric[L]
collectFunc collectFunc
collectForDocsFunc collectFunc
}

// NewCustomCollector creates a new customCollector.
//
// If collectForDocs is nil, the collector will use collect function for both
// regular metrics server and generating documentation.
func NewCustomCollector[L FilteredLabels](
metrics []GranularCustomMetric[L], collect collectFunc, collectForDocs collectFunc,
) CollectorWithInit {
return &customCollector[L]{
metrics: metrics,
collectFunc: collect,
collectForDocsFunc: collectForDocs,
}
}

// Describe implements CollectorWithInit (prometheus.Collector).
func (c *customCollector[L]) Describe(ch chan<- *prometheus.Desc) {
for _, m := range c.metrics {
ch <- m.Desc()
}
}

// Collect implements CollectorWithInit (prometheus.Collector).
func (c *customCollector[L]) Collect(ch chan<- prometheus.Metric) {
if c.collectFunc != nil {
c.collectFunc(ch)
}
}

// IsConstrained implements CollectorWithInit.
func (c *customCollector[L]) IsConstrained() bool {
for _, m := range c.metrics {
if !m.IsConstrained() {
return false
}
}
return true
}

// Init implements CollectorWithInit.
func (c *customCollector[L]) Init() {
// since metrics are collected independently, there's nothing to initialize
}

// InitForDocs implements CollectorWithInit.
func (c *customCollector[L]) InitForDocs() {
// override Collect method if there's a separate one for docs
if c.collectForDocsFunc != nil {
c.collectFunc = c.collectForDocsFunc
}
}
152 changes: 152 additions & 0 deletions pkg/metrics/custommetric.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Tetragon

package metrics

import (
"github.com/prometheus/client_golang/prometheus"
)

// GranularCustomMetric represents a metric collected independently of
// prometheus package, for example in a BPF map. It's intended to be used in
// a custom collector (see customcollector.go). The interface doesn't provide
// any validation, so it's up to the collector implementer to guarantee the
// metrics consistency.
type GranularCustomMetric[L FilteredLabels] interface {
Desc() *prometheus.Desc
MustMetric(value float64, commonLvs *L, extraLvs ...string) prometheus.Metric
IsConstrained() bool
}

// getDesc is a helper function to retrieve the descriptor for a metric and
// check if the metric is constrained.
//
// See getVariableLabels for the labels order.
func getDesc[L FilteredLabels](opts *MetricOpts) (*prometheus.Desc, bool, error) {
labels, constrained, err := getVariableLabels[L](opts)
if err != nil {
return nil, false, err
}

desc := prometheus.NewDesc(
prometheus.BuildFQName(opts.Namespace, opts.Subsystem, opts.Name),
opts.Help,
labels,
opts.ConstLabels,
)
return desc, constrained, nil
}

// counter

type granularCustomCounter[L FilteredLabels] struct {
desc *prometheus.Desc
constrained bool
}

// NewGranularCustomCounter creates a new granularCustomCounter.
func NewGranularCustomCounter[L FilteredLabels](opts MetricOpts) (GranularCustomMetric[L], error) {
desc, constrained, err := getDesc[L](&opts)
if err != nil {
return nil, err
}

return &granularCustomCounter[L]{
desc: desc,
constrained: constrained,
}, nil
}

// MustNewGranularCustomCounter is a convenience function that wraps
// NewGranularCustomCounter and panics on error.
func MustNewGranularCustomCounter[L FilteredLabels](opts MetricOpts) GranularCustomMetric[L] {
m, err := NewGranularCustomCounter[L](opts)
if err != nil {
panic(err)
}
return m
}

// NewCustomCounter creates a new granularCustomCounter with no configurable labels.
func NewCustomCounter(opts MetricOpts) (GranularCustomMetric[NilLabels], error) {
return NewGranularCustomCounter[NilLabels](opts)
}

// MustNewCustomCounter is a convenience function that wraps NewCustomCounter
// and panics on error.
func MustNewCustomCounter(opts MetricOpts) GranularCustomMetric[NilLabels] {
return MustNewGranularCustomCounter[NilLabels](opts)
}

// Desc implements GranularCustomMetric.
func (m *granularCustomCounter[L]) Desc() *prometheus.Desc {
return m.desc
}

// MustMetric implements GranularCustomMetric.
func (m *granularCustomCounter[L]) MustMetric(value float64, commonLvs *L, extraLvs ...string) prometheus.Metric {
lvs := append((*commonLvs).Values(), extraLvs...)
return prometheus.MustNewConstMetric(m.desc, prometheus.CounterValue, value, lvs...)
}

// IsConstrained implements GranularCustomMetric.
func (m *granularCustomCounter[L]) IsConstrained() bool {
return m.constrained
}

// gauge

type granularCustomGauge[L FilteredLabels] struct {
desc *prometheus.Desc
constrained bool
}

// NewGranularCustomGauge creates a new granularCustomGauge.
func NewGranularCustomGauge[L FilteredLabels](opts MetricOpts) (GranularCustomMetric[L], error) {
desc, constrained, err := getDesc[L](&opts)
if err != nil {
return nil, err
}

return &granularCustomGauge[L]{
desc: desc,
constrained: constrained,
}, nil
}

// MustNewGranularCustomGauge is a convenience function that wraps
// NewGranularCustomGauge and panics on error.
func MustNewGranularCustomGauge[L FilteredLabels](opts MetricOpts) GranularCustomMetric[L] {
m, err := NewGranularCustomGauge[L](opts)
if err != nil {
panic(err)
}
return m
}

// NewCustomGauge creates a new granularCustomGauge with no configurable labels.
func NewCustomGauge(opts MetricOpts) (GranularCustomMetric[NilLabels], error) {
return NewGranularCustomGauge[NilLabels](opts)
}

// MustNewCustomGauge is a convenience function that wraps NewCustomGauge
// and panics on error.
func MustNewCustomGauge(opts MetricOpts) GranularCustomMetric[NilLabels] {
return MustNewGranularCustomGauge[NilLabels](opts)
}

// Desc implements GranularCustomMetric.
func (m *granularCustomGauge[L]) Desc() *prometheus.Desc {
return m.desc
}

// MustMetric implements GranularCustomMetric.
func (m *granularCustomGauge[L]) MustMetric(value float64, commonLvs *L, extraLvs ...string) prometheus.Metric {
lvs := append((*commonLvs).Values(), extraLvs...)
return prometheus.MustNewConstMetric(m.desc, prometheus.GaugeValue, value, lvs...)
}

// IsConstrained implements GranularCustomMetric.
func (m *granularCustomGauge[L]) IsConstrained() bool {
return m.constrained
}
63 changes: 63 additions & 0 deletions pkg/metrics/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Tetragon

// The metrics package provides a set of helpers (wrappers around
// [prometheus Go library](https://pkg.go.dev/github.com/prometheus/client_golang/prometheus))
// for defining and managing prometheus metrics.
//
// The package is designed to support the following functionality:
// - Group metrics based on their purpose and load groups independently.
// This gives us more control over what metrics are exposed and how
// cardinality is managed.
// - Define custom collectors, e.g. reading metrics directly from BPF maps.
// This decouples metrics from events passed through ringbuffer.
// - Let users configure high-cardinality dynamic labels, for both "regular"
// metrics and custom collectors.
// - Constrain metrics cardinality for metrics with known labels.
// - Initialize metrics with known labels on startup.
// This makes resources usage more predictable, as cardinality of these
// metrics won't grow.
// - Autogenerate reference documentation from metrics help texts.
// - Delete stale metrics. This will prevent growing cardinality.
// Currently we do it when a pod is deleted, but it should be easy to
// extend this to other cases.
// - Keep common labels consistent between metrics.
// This makes it easier to write queries.
//
// Here we describe the key parts of the metrics package. See also doc comments
// in the code for more details.
//
// `Group` interface and `metricsGroup` struct implementing it are
// wrappers around `prometheus.Registry` intended to define sub-registries of
// the root registry. In addition to registering metrics, it supports:
// - initializing metrics on startup
// - initializing metrics for generating docs
// - constraining metrics cardinality (constrained group contains only
// metrics with constrained cardinality)
//
// `MetricOpts` struct is a wrapper around `prometheus.Opts` that additionally
// supports defining constrained and unconstrained labels.
//
// `ConstrainedLabel` and `UnconstrainedLabel` structs represent metric labels.
//
// `FilteredLabels` interface represents configurable labels, passed to metrics
// via type parameter. The values are always unconstrained.
//
// `GranularCounter[L FilteredLabels]` (and analogous Gauge and Histogram)
// struct is a wrapper around `prometheus.CounterVec` (Gauge, Histogram) with
// additional properties:
// - cardinality can be constrained (meaning all label values are known)
// - support for configurable labels
// - metric is initialized at startup for known label values
// - metric is automatically included in generated docs
//
// `customCollector[L FilteredLabels]` struct represents a custom collector
// (e.g. reading metrics directly from a BPF map). It contains a list of
// metrics, collect function and an optional separate collect function for
// generating docs.
//
// `GranularCustomMetric[L FilteredLabels]` (and analogous Gauge) interface
// represents a custom metric (e.g. read directly form a BPF map). Similarly
// like "regular" metrics tracked by prometheus library, it supports
// constraining cardinality and configurable labels.
package metrics
Loading

0 comments on commit cc929de

Please sign in to comment.