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: #2376

Signed-off-by: Anna Kapuscinska <[email protected]>
  • Loading branch information
lambdanis committed Jun 21, 2024
1 parent dd602b0 commit 3f6b4de
Show file tree
Hide file tree
Showing 7 changed files with 844 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) {

Check failure on line 25 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / golangci-lint

undefined: MetricOpts

Check failure on line 25 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / generated-files

undefined: MetricOpts

Check failure on line 25 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / analyze

undefined: MetricOpts
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) {

Check failure on line 48 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / golangci-lint

undefined: MetricOpts

Check failure on line 48 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / generated-files

undefined: MetricOpts

Check failure on line 48 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / analyze

undefined: MetricOpts
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] {

Check failure on line 62 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / golangci-lint

undefined: MetricOpts

Check failure on line 62 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / generated-files

undefined: MetricOpts

Check failure on line 62 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / analyze

undefined: MetricOpts
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) {

Check failure on line 71 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / golangci-lint

undefined: MetricOpts

Check failure on line 71 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / generated-files

undefined: MetricOpts

Check failure on line 71 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / analyze

undefined: MetricOpts
return NewGranularCustomCounter[NilLabels](opts)
}

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

Check failure on line 77 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / golangci-lint

undefined: MetricOpts

Check failure on line 77 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / generated-files

undefined: MetricOpts

Check failure on line 77 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / analyze

undefined: MetricOpts
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) {

Check failure on line 105 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / golangci-lint

undefined: MetricOpts

Check failure on line 105 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / generated-files

undefined: MetricOpts

Check failure on line 105 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / analyze

undefined: MetricOpts
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] {

Check failure on line 119 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / golangci-lint

undefined: MetricOpts

Check failure on line 119 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / generated-files

undefined: MetricOpts

Check failure on line 119 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / analyze

undefined: MetricOpts
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) {

Check failure on line 128 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / golangci-lint

undefined: MetricOpts

Check failure on line 128 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / generated-files

undefined: MetricOpts

Check failure on line 128 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / analyze

undefined: MetricOpts
return NewGranularCustomGauge[NilLabels](opts)
}

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

Check failure on line 134 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / golangci-lint

undefined: MetricOpts

Check failure on line 134 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / generated-files

undefined: MetricOpts

Check failure on line 134 in pkg/metrics/custommetric.go

View workflow job for this annotation

GitHub Actions / analyze

undefined: MetricOpts
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
}
56 changes: 56 additions & 0 deletions pkg/metrics/filteredlabels.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Tetragon

package metrics

import (
"github.com/cilium/tetragon/pkg/metrics/consts"
)

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

// FilteredLabelsWithExamples extends FilteredLabels with a method returning
// example label values, intended to be used when generating documentation.
type FilteredLabelsWithExamples interface {
FilteredLabels
ExampleValues() []string
}

type NilLabels struct{}

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

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

func (l NilLabels) ExampleValues() []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}
}

func (l ProcessLabels) ExampleValues() []string {
return []string{consts.ExampleNamespace, consts.ExampleWorkload, consts.ExamplePod, consts.ExampleBinary}
}
Loading

0 comments on commit 3f6b4de

Please sign in to comment.