Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor and extend the interface for metrics with configurable labels #1548

Merged
merged 3 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions pkg/metrics/eventmetrics/eventmetrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,10 @@ func handleProcessedEvent(pInfo *tracingpolicy.PolicyInfo, processedEvent interf
default:
eventType = "unknown"
}
EventsProcessed.ToProm().WithLabelValues(metrics.FilterMetricLabels(eventType, namespace, workload, pod, binary)...).Inc()
EventsProcessed.WithLabelValues(eventType, namespace, workload, pod, binary).Inc()
if pInfo != nil && pInfo.Name != "" {
policyStats.ToProm().
WithLabelValues(metrics.FilterMetricLabels(pInfo.Name, pInfo.Hook, namespace, workload, pod, binary)...).
policyStats.
WithLabelValues(pInfo.Name, pInfo.Hook, namespace, workload, pod, binary).
Inc()
}
}
Expand Down
226 changes: 226 additions & 0 deletions pkg/metrics/granularmetric.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Tetragon

package metrics

import (
"fmt"
"sync"

"github.com/cilium/tetragon/pkg/metrics/consts"
"github.com/cilium/tetragon/pkg/option"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/exp/slices"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey @lambdanis I think you reintroduced golang.org/x/exp/slices here, I don't know if it's intended since you removed that #1560.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just rebased one of my PR and noticed that go.mod needed to include the new dependency.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really don't understand how the tests pass here and not on my rebased PR... https://github.com/cilium/tetragon/actions/runs/6508448671/job/17677830078?pr=1562#step:5:610

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the tests passed on this PR a while ago, before some deps got updated?
Anyway, thanks for fixing this import.

)

var (
granularLabelFilter = NewLabelFilter(
consts.KnownMetricLabelFilters,
option.Config.MetricsLabelFilter,
)
)

type LabelFilter struct {
known []string
enabled map[string]interface{}
}

func NewLabelFilter(known []string, enabled map[string]interface{}) *LabelFilter {
return &LabelFilter{
known: known,
enabled: enabled,
}
}

// metric

type granularMetricIface interface {
filter(labels ...string) ([]string, error)
mustFilter(labels ...string) []string
}

type granularMetric struct {
labels []string
labelFilter *LabelFilter
eval sync.Once
}

func newGranularMetric(f *LabelFilter, labels []string) (*granularMetric, error) {
for _, label := range labels {
if slices.Contains(f.known, label) {
return nil, fmt.Errorf("passed labels can't contain any of the following: %v", f.known)
}
}
return &granularMetric{
labels: append(labels, f.known...),
labelFilter: f,
}, nil
}

// filter takes in string arguments and returns a slice of those strings omitting the labels not configured in the metric labelFilter.
// IMPORTANT! The filtered metric labels must be passed last and in the exact order of granularMetric.labelFilter.known.
func (m *granularMetric) filter(labels ...string) ([]string, error) {
offset := len(labels) - len(m.labelFilter.known)
if offset < 0 {
return nil, fmt.Errorf("not enough labels provided to filter")
}
result := labels[:offset]
for i, label := range m.labelFilter.known {
if _, ok := m.labelFilter.enabled[label]; ok {
result = append(result, labels[offset+i])
}
}
return result, nil
}

func (m *granularMetric) mustFilter(labels ...string) []string {
result, err := m.filter(labels...)
if err != nil {
panic(err)
}
return result
}

// counter

type GranularCounter interface {
granularMetricIface
ToProm() *prometheus.CounterVec
WithLabelValues(lvs ...string) prometheus.Counter
}

type granularCounter struct {
*granularMetric
metric *prometheus.CounterVec
opts prometheus.CounterOpts
}

func NewGranularCounter(f *LabelFilter, opts prometheus.CounterOpts, labels []string) (GranularCounter, error) {
metric, err := newGranularMetric(f, labels)
if err != nil {
return nil, err
}
return &granularCounter{
granularMetric: metric,
opts: opts,
}, nil
}

func MustNewGranularCounter(opts prometheus.CounterOpts, labels []string) GranularCounter {
counter, err := NewGranularCounter(granularLabelFilter, opts, labels)
if err != nil {
panic(err)
}
return counter
}

func (m *granularCounter) ToProm() *prometheus.CounterVec {
m.eval.Do(func() {
m.labels = m.mustFilter(m.labels...)
m.metric = NewCounterVecWithPod(m.opts, m.labels)
})
return m.metric
}

func (m *granularCounter) WithLabelValues(lvs ...string) prometheus.Counter {
filtered := m.mustFilter(lvs...)
return m.ToProm().WithLabelValues(filtered...)
}

// gauge

type GranularGauge interface {
granularMetricIface
ToProm() *prometheus.GaugeVec
WithLabelValues(lvs ...string) prometheus.Gauge
}

type granularGauge struct {
*granularMetric
metric *prometheus.GaugeVec
opts prometheus.GaugeOpts
}

func NewGranularGauge(f *LabelFilter, opts prometheus.GaugeOpts, labels []string) (GranularGauge, error) {
for _, label := range labels {
if slices.Contains(f.known, label) {
return nil, fmt.Errorf("passed labels can't contain any of the following: %v", f.known)
}
}
return &granularGauge{
granularMetric: &granularMetric{
labels: append(labels, f.known...),
},
opts: opts,
}, nil
}

func MustNewGranularGauge(opts prometheus.GaugeOpts, labels []string) GranularGauge {
result, err := NewGranularGauge(granularLabelFilter, opts, labels)
if err != nil {
panic(err)
}
return result
}

func (m *granularGauge) ToProm() *prometheus.GaugeVec {
m.eval.Do(func() {
m.labels = m.mustFilter(m.labels...)
m.metric = NewGaugeVecWithPod(m.opts, m.labels)
})
return m.metric
}

func (m *granularGauge) WithLabelValues(lvs ...string) prometheus.Gauge {
filtered := m.mustFilter(lvs...)
return m.ToProm().WithLabelValues(filtered...)
}

// histogram

type GranularHistogram interface {
granularMetricIface
ToProm() *prometheus.HistogramVec
WithLabelValues(lvs ...string) prometheus.Observer
}

type granularHistogram struct {
*granularMetric
metric *prometheus.HistogramVec
opts prometheus.HistogramOpts
}

func NewGranularHistogram(f *LabelFilter, opts prometheus.HistogramOpts, labels []string) (GranularHistogram, error) {
for _, label := range labels {
if slices.Contains(f.known, label) {
return nil, fmt.Errorf("passed labels can't contain any of the following: %v", f.known)
}
}
return &granularHistogram{
granularMetric: &granularMetric{
labels: append(labels, f.known...),
},
opts: opts,
}, nil
}

func MustNewGranularHistogram(opts prometheus.HistogramOpts, labels []string) GranularHistogram {
result, err := NewGranularHistogram(granularLabelFilter, opts, labels)
if err != nil {
panic(err)
}
return result
}

func (m *granularHistogram) ToProm() *prometheus.HistogramVec {
m.eval.Do(func() {
m.labels = m.mustFilter(m.labels...)
m.metric = NewHistogramVecWithPod(m.opts, m.labels)
})
return m.metric
}

func (m *granularHistogram) WithLabelValues(lvs ...string) prometheus.Observer {
filtered := m.mustFilter(lvs...)
return m.ToProm().WithLabelValues(filtered...)
}
72 changes: 72 additions & 0 deletions pkg/metrics/granularmetric_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Tetragon

package metrics

import (
"testing"

"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"

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

var (
sampleCounterOpts = prometheus.CounterOpts{
Namespace: consts.MetricsNamespace,
Name: "test_events_total",
Help: "The number of test events",
}
sampleSyscallCounterOpts = prometheus.CounterOpts{
Namespace: consts.MetricsNamespace,
Name: "test_syscalls_total",
Help: "The number of test syscalls",
}
)

func TestLabelFilter(t *testing.T) {
// define label filter and metrics
sampleLabelFilter := NewLabelFilter(
consts.KnownMetricLabelFilters,
map[string]interface{}{
"namespace": nil,
"workload": nil,
"pod": nil,
"binary": nil,
},
)
sampleCounter, err := NewGranularCounter(sampleLabelFilter, sampleCounterOpts, []string{})
assert.NoError(t, err)
sampleSyscallCounter, err := NewGranularCounter(sampleLabelFilter, sampleSyscallCounterOpts, []string{"syscall"})
assert.NoError(t, err)
// instantiate the underlying metrics
sampleCounter.ToProm()
sampleSyscallCounter.ToProm()
// check that labels are filtered correctly
sampleLabelValues := []string{"test-namespace", "test-deployment", "test-deployment-d9jo2", "test-binary"}
expectedLabelValues := []string{"test-namespace", "test-deployment", "test-deployment-d9jo2", "test-binary"}
assert.Equal(t, expectedLabelValues, sampleCounter.mustFilter(sampleLabelValues...))
assert.Equal(t, append([]string{"test-syscall"}, expectedLabelValues...), sampleSyscallCounter.mustFilter(append([]string{"test-syscall"}, sampleLabelValues...)...))

// define another label filter and metrics
sampleLabelFilter = NewLabelFilter(
consts.KnownMetricLabelFilters,
map[string]interface{}{
"namespace": nil,
"workload": nil,
},
)
sampleCounter, err = NewGranularCounter(sampleLabelFilter, sampleCounterOpts, []string{})
assert.NoError(t, err)
sampleSyscallCounter, err = NewGranularCounter(sampleLabelFilter, sampleSyscallCounterOpts, []string{"syscall"})
assert.NoError(t, err)
// instantiate the underlying metrics
sampleCounter.ToProm()
sampleSyscallCounter.ToProm()
// check that labels are filtered correctly
sampleLabelValues = []string{"test-namespace", "test-deployment", "test-deployment-d9jo2", "test-binary"}
expectedLabelValues = []string{"test-namespace", "test-deployment"}
assert.Equal(t, expectedLabelValues, sampleCounter.mustFilter(sampleLabelValues...))
assert.Equal(t, append([]string{"test-syscall"}, expectedLabelValues...), sampleSyscallCounter.mustFilter(append([]string{"test-syscall"}, sampleLabelValues...)...))
}
49 changes: 0 additions & 49 deletions pkg/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,10 @@
package metrics

import (
"fmt"
"net/http"
"sync"

"golang.org/x/exp/slices"

"github.com/cilium/tetragon/pkg/logger"
"github.com/cilium/tetragon/pkg/metrics/consts"
"github.com/cilium/tetragon/pkg/option"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
Expand All @@ -22,54 +17,10 @@ var (
registryOnce sync.Once
)

type GranularCounter struct {
counter *prometheus.CounterVec
CounterOpts prometheus.CounterOpts
labels []string
register sync.Once
}

func MustNewGranularCounter(opts prometheus.CounterOpts, labels []string) *GranularCounter {
for _, label := range labels {
if slices.Contains(consts.KnownMetricLabelFilters, label) {
panic(fmt.Sprintf("labels passed to GranularCounter can't contain any of the following: %v. These labels are added by Tetragon.", consts.KnownMetricLabelFilters))
}
}
return &GranularCounter{
CounterOpts: opts,
labels: append(labels, consts.KnownMetricLabelFilters...),
}
}

func (m *GranularCounter) ToProm() *prometheus.CounterVec {
m.register.Do(func() {
m.labels = FilterMetricLabels(m.labels...)
m.counter = NewCounterVecWithPod(m.CounterOpts, m.labels)
})
return m.counter
}

func EnableMetrics(address string) {
reg := GetRegistry()

logger.GetLogger().WithField("addr", address).Info("Starting metrics server")
http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{Registry: reg}))
http.ListenAndServe(address, nil)
}

// The FilterMetricLabels func takes in string arguments and returns a slice of those strings omitting the labels it is not configured for.
// IMPORTANT! The filtered metric labels must be passed last and in the exact order of consts.KnownMetricLabelFilters.
func FilterMetricLabels(labels ...string) []string {
offset := len(labels) - len(consts.KnownMetricLabelFilters)
if offset < 0 {
logger.GetLogger().WithField("labels", labels).Debug("Not enough labels provided to metrics.FilterMetricLabels.")
return labels
}
result := labels[:offset]
for i, label := range consts.KnownMetricLabelFilters {
if _, ok := option.Config.MetricsLabelFilter[label]; ok {
result = append(result, labels[offset+i])
}
}
return result
}
Loading