diff --git a/pkg/metrics/eventmetrics/eventmetrics.go b/pkg/metrics/eventmetrics/eventmetrics.go index ccca7e0b44c..0ca21a758aa 100644 --- a/pkg/metrics/eventmetrics/eventmetrics.go +++ b/pkg/metrics/eventmetrics/eventmetrics.go @@ -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() } } diff --git a/pkg/metrics/granularmetric.go b/pkg/metrics/granularmetric.go new file mode 100644 index 00000000000..eb490be435e --- /dev/null +++ b/pkg/metrics/granularmetric.go @@ -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" +) + +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...) +} diff --git a/pkg/metrics/granularmetric_test.go b/pkg/metrics/granularmetric_test.go new file mode 100644 index 00000000000..933904946ea --- /dev/null +++ b/pkg/metrics/granularmetric_test.go @@ -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...)...)) +} diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index d0f91243ec9..cf525538b7e 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -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" ) @@ -22,33 +17,6 @@ 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() @@ -56,20 +24,3 @@ func EnableMetrics(address string) { 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 -} diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go deleted file mode 100644 index 3e682e50481..00000000000 --- a/pkg/metrics/metrics_test.go +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Tetragon - -package metrics_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/cilium/tetragon/pkg/metrics" - "github.com/cilium/tetragon/pkg/option" -) - -func TestFilterMetricLabels(t *testing.T) { - option.Config.MetricsLabelFilter = map[string]interface{}{ - "namespace": nil, - "workload": nil, - "pod": nil, - "binary": nil, - } - assert.Equal(t, []string{"type", "namespace", "workspace", "pod", "binary"}, metrics.FilterMetricLabels("type", "namespace", "workspace", "pod", "binary")) - assert.Equal(t, []string{"syscall", "namespace", "workspace", "pod", "binary"}, metrics.FilterMetricLabels("syscall", "namespace", "workspace", "pod", "binary")) - assert.Equal(t, []string{"namespace", "workspace", "pod", "binary"}, metrics.FilterMetricLabels("namespace", "workspace", "pod", "binary")) - - option.Config.MetricsLabelFilter = map[string]interface{}{ - "namespace": nil, - "workload": nil, - } - assert.Equal(t, []string{"type", "namespace", "workspace"}, metrics.FilterMetricLabels("type", "namespace", "workspace", "pod", "binary")) - assert.Equal(t, []string{"syscall", "namespace", "workspace"}, metrics.FilterMetricLabels("syscall", "namespace", "workspace", "pod", "binary")) - assert.Equal(t, []string{"namespace", "workspace"}, metrics.FilterMetricLabels("namespace", "workspace", "pod", "binary")) - - option.Config.MetricsLabelFilter = map[string]interface{}{ - "namespace": nil, - "workload": nil, - "pod": nil, - "binary": nil, - } - assert.Equal(t, []string{"type", "syscall"}, metrics.FilterMetricLabels("type", "syscall")) -} diff --git a/pkg/metrics/syscallmetrics/syscallmetrics.go b/pkg/metrics/syscallmetrics/syscallmetrics.go index 3e796963af7..9de85efd0d9 100644 --- a/pkg/metrics/syscallmetrics/syscallmetrics.go +++ b/pkg/metrics/syscallmetrics/syscallmetrics.go @@ -46,8 +46,8 @@ func Handle(event interface{}) { } if syscall != "" { - syscallStats.ToProm(). - WithLabelValues(metrics.FilterMetricLabels(syscall, namespace, workload, pod, binary)...). + syscallStats. + WithLabelValues(syscall, namespace, workload, pod, binary). Inc() } }