Skip to content

Commit

Permalink
Rewrite the metrics library
Browse files Browse the repository at this point in the history
Fixes cilium#2376

Signed-off-by: Anna Kapuscinska <[email protected]>
  • Loading branch information
lambdanis committed Jun 17, 2024
1 parent dd602b0 commit 36ff590
Show file tree
Hide file tree
Showing 7 changed files with 532 additions and 0 deletions.
61 changes: 61 additions & 0 deletions pkg/metrics/metrics/customcollector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// 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
}

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() {
// no-op
}

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

package metrics

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

type GranularCustomMetric[L FilteredLabels] interface {
Desc() *prometheus.Desc
MustMetric(value float64, commonLvs *L, extraLvs ...string) prometheus.Metric
IsConstrained() bool
}

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

func NewGranularCustomCounter[L FilteredLabels](opts MetricOpts) (GranularCustomMetric[L], error) {
labels, constrained, err := getVariableLabels[L](&opts)
if err != nil {
return nil, err
}

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

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

// Desc implements GranularCustomMetric.
func (c *granularCustomCounter[L]) Desc() *prometheus.Desc {
return c.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
}
39 changes: 39 additions & 0 deletions pkg/metrics/metrics/filteredlabels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Tetragon

package metrics

type FilteredLabels interface {
Keys() []string
Values() []string
}

type NilLabels struct{}

func (l NilLabels) Keys() []string { return []string{} }

func (l NilLabels) Values() []string { return []string{} }

type ProcessLabels struct {
Namespace string
Workload string
Pod string
Binary string
}

func NewProcessLabels(namespace, workload, pod, binary string) *ProcessLabels {
return &ProcessLabels{
Namespace: namespace,
Workload: workload,
Pod: pod,
Binary: binary,
}
}

func (l ProcessLabels) Keys() []string {
return []string{"namespace", "workload", "pod", "binary"}
}

func (l ProcessLabels) Values() []string {
return []string{l.Namespace, l.Workload, l.Pod, l.Binary}
}
154 changes: 154 additions & 0 deletions pkg/metrics/metrics/granularmetric.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Tetragon

package metrics

import (
"slices"

"github.com/cilium/tetragon/pkg/metrics"
"github.com/prometheus/client_golang/prometheus"
)

type initMetricFunc func(...string)

// initAllCombinations initializes a metric with all possible combinations of label values.
func initAllCombinations(initMetric initMetricFunc, labels []ConstrainedLabel) {
initCombinations(initMetric, labels, make([]string, len(labels)), 0)
}

// initCombinations is a helper function that recursively initializes a metric with possible combinations of label
// values. There are a few assumptions about the arguments:
// - initMetric is not nil
// - labels and lvs have the same length
// - cursor is in the range [0, len(labels)]
// If any of these is not met, the function will silently return.
func initCombinations(initMetric initMetricFunc, labels []ConstrainedLabel, lvs []string, cursor int) {
if initMetric == nil || len(labels) != len(lvs) || cursor < 0 || cursor > len(labels) {
// The function was called with invalid arguments. Silently return.
return
}
if cursor == len(labels) {
initMetric(lvs...)
return
}
for _, val := range labels[cursor].Values {
lvs[cursor] = val
initCombinations(initMetric, labels, lvs, cursor+1)
}
}

// initForDocs initializes the metric for the purpose of generating documentation.
// TODO: we need filtered labels here too
func initForDocs(initMetric initMetricFunc, constrained []ConstrainedLabel, unconstrained []UnconstrainedLabel) {
lvs := make([]string, len(constrained)+len(unconstrained))
for i := range constrained {
for _, val := range constrained[i].Values {
lvs[i] = val
initMetric(lvs...)
}
lvs[i] = ""
}
for i := range unconstrained {
lvs[len(constrained)+i] = unconstrained[i].ExampleValue
initMetric(lvs...)
lvs[len(constrained)+i] = ""
}
}

// GranularCounter wraps prometheus.CounterVec and implements CollectorWithInit.
type GranularCounter[L FilteredLabels] struct {
metric *prometheus.CounterVec
constrained bool
initFunc func()
initForDocs func()
}

// NewGranularCounter creates a new GranularCounter.
//
// The init argument is a function that initializes the metric with some label values. Doing so allows us to keep
// resources usage predictable. If the metric is constrained (i.e. type parameter is NilLabels and there are no
// unconstrained labels) and init is nil, then the metric will be initialized with all possible combinations of labels.
// If the metric is unconstrained, it won't be initialized by default.
//
// Pass an init function if:
// a) metric is constrained but not all combinations of labels make sense (e.g. there is a hierarchy between labels or
// two labels represent the same thing in different formats, or two labels are mutually exclusive, or ...)
// b) metric is unconstrained, but some of the unconstrained label values are known beforehand, so can be initialized.
func NewGranularCounter[L FilteredLabels](opts MetricOpts, init func()) (*GranularCounter[L], error) {
labels, constrained, err := getVariableLabels[L](&opts)
if err != nil {
return nil, err
}

promOpts := prometheus.CounterOpts{
Namespace: opts.Namespace,
Subsystem: opts.Subsystem,
Name: opts.Name,
Help: opts.Help,
ConstLabels: opts.ConstLabels,
}
var metric *prometheus.CounterVec
if slices.Contains(labels, "pod") && slices.Contains(labels, "namespace") {
// set up metric to be deleted when a pod is deleted
metric = metrics.NewCounterVecWithPod(promOpts, labels)
} else {
metric = prometheus.NewCounterVec(promOpts, labels)
}

initMetric := func(lvs ...string) {
metric.WithLabelValues(lvs...).Add(0)
}

// if metric is constrained, default to initializing all combinations of labels
if constrained && init == nil {
init = func() {
initAllCombinations(initMetric, opts.ConstrainedLabels)
}
}

return &GranularCounter[L]{
metric: metric,
constrained: constrained,
initFunc: init,
initForDocs: func() {
initForDocs(initMetric, opts.ConstrainedLabels, opts.UnconstrainedLabels)
},
}, nil
}

// Describe implements CollectorWithInit (prometheus.Collector).
func (m *GranularCounter[L]) Describe(ch chan<- *prometheus.Desc) {
m.metric.Describe(ch)
}

// Collect implements CollectorWithInit (prometheus.Collector).
func (m *GranularCounter[L]) Collect(ch chan<- prometheus.Metric) {
m.metric.Collect(ch)
}

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

// Init implements CollectorWithInit.
func (m *GranularCounter[L]) Init() {
if m.initFunc != nil {
m.initFunc()
}
}

// InitForDocs implements CollectorWithInit.
func (m *GranularCounter[L]) InitForDocs() {
if m.initForDocs != nil {
m.initForDocs()
}
}

// WithLabelValues is similar to WithLabelValues method from prometheus package, but takes generic FilteredLabels as
// the first argument. The following arguments are values of first constrained labels, then unconstrained labels.
func (m *GranularCounter[L]) WithLabelValues(commonLvs *L, extraLvs ...string) prometheus.Counter {
lvs := append((*commonLvs).Values(), extraLvs...)
return m.metric.WithLabelValues(lvs...)
}
18 changes: 18 additions & 0 deletions pkg/metrics/metrics/labels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Tetragon

package metrics

// ConstrainedLabel represents a label with constrained cardinality.
// Values is a list of all possible values of the label.
type ConstrainedLabel struct {
Name string
Values []string
}

// UnconstrainedLabel represents a label with unconstrained cardinality.
// ExampleValue is an example value of the label used for documentation.
type UnconstrainedLabel struct {
Name string
ExampleValue string
}
Loading

0 comments on commit 36ff590

Please sign in to comment.