Skip to content
Closed
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
99 changes: 99 additions & 0 deletions central/metrics/custom/tracker/aggregator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package tracker

import (
"iter"

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

type aggregationKey string // e.g. IMPORTANT_VULNERABILITY_SEVERITY|true

// aggregatedRecord counts the number of occurrences of a set of label values.
type aggregatedRecord struct {
labels prometheus.Labels
total int
}

// aggregator is a Finding processor, that counts the number of occurences of
// every combination of label values in the findings.
// The processing result is stored in the result field.
// The labelOrder is used to compute the aggregationKey (i.e. pipe separated
// label values).
// MetricsConfiguration provides the list of metrics with their sets of labels.
//
// For example, for a metric M1 with labels L1 and L2, and metric M2 with a
// single label L2, provided the following findings:
//
// [{L1="X", L2="Y"}, {L1="X", L2="Z"}, {L1="X", L2="Z"}],
//
// the aggregator will produce the following result:
//
// {
// M1:
// {"X|Y": {labels: {L1="X", L2="Y"}, total: 1}},
// {"X|Z": {labels: {L1="X", L2="Z"}, total: 2}}
// M2:
// {"Y": {labels: {L2="Y"}, total: 1}},
// {"Z": {labels: {L2="Z"}, total: 2}}
// }
type aggregator[Finding any] struct {
result map[MetricName]map[aggregationKey]*aggregatedRecord
mcfg MetricsConfiguration
labelOrder map[Label]int
getters map[Label]func(Finding) string
}

func makeAggregator[Finding any](mcfg MetricsConfiguration, labelOrder map[Label]int, getters map[Label]func(Finding) string) *aggregator[Finding] {
aggregated := make(map[MetricName]map[aggregationKey]*aggregatedRecord)
for metric := range mcfg {
aggregated[metric] = make(map[aggregationKey]*aggregatedRecord)
}
return &aggregator[Finding]{aggregated, mcfg, labelOrder, getters}
}

// count the finding in the aggregation result.
func (r *aggregator[Finding]) count(finding Finding) {
labelValue := func(label Label) string {
return r.getters[label](finding)
}
for metric, labels := range r.mcfg {
if key, labels := makeAggregationKey(labels, labelValue, r.labelOrder); key != "" {
if rec, ok := r.result[metric][key]; ok {
rec.total++
} else {
r.result[metric][key] = &aggregatedRecord{labels, 1}
}
}
}
}

// makeAggregationKey computes an aggregation key according to the labels from
// the provided expression, and the map of the requested labels to their values.
// The values in the key are sorted according to the provided labelOrder map.
//
// Example:
//
// "Cluster,Deployment" => "pre-prod|backend", {"Cluster": "pre-prod", "Deployment": "backend")}
func makeAggregationKey(labelExpression []Label, getter func(Label) string, labelOrder map[Label]int) (aggregationKey, prometheus.Labels) {
labels := make(prometheus.Labels)
values := make(orderedValues, len(labelExpression))
for label, value := range collectMatchingLabels(labelExpression, getter) {
labels[string(label)] = value
values = append(values, valueOrder{labelOrder[label], value})
}
if len(labels) != len(labelExpression) {
return "", nil
}
return aggregationKey(values.join('|')), labels
}

// collectMatchingLabels returns an iterator over the labels and the values.
func collectMatchingLabels(labels []Label, getter func(Label) string) iter.Seq2[Label, string] {
return func(yield func(Label, string) bool) {
for _, label := range labels {
if !yield(label, getter(label)) {
return
}
}
}
}
141 changes: 141 additions & 0 deletions central/metrics/custom/tracker/aggregator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package tracker

import (
"testing"

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

func Test_aggregator(t *testing.T) {
getters := map[Label]func(testFinding) string{
"Severity": func(tf testFinding) string { return testData[tf]["Severity"] },
"Cluster": func(tf testFinding) string { return testData[tf]["Cluster"] },
"Namespace": func(tf testFinding) string { return testData[tf]["Namespace"] },
}
a := makeAggregator(makeTestMetricConfiguration(t), testLabelOrder, getters)
assert.NotNil(t, a)
assert.Equal(t, map[MetricName]map[aggregationKey]*aggregatedRecord{
"Test_aggregator_metric1": {},
"Test_aggregator_metric2": {},
}, a.result)

a.count(testFinding(0))

assert.Equal(t, map[MetricName]map[aggregationKey]*aggregatedRecord{
"Test_aggregator_metric1": {
"cluster 1|CRITICAL": &aggregatedRecord{
labels: prometheus.Labels{"Cluster": testData[0]["Cluster"], "Severity": testData[0]["Severity"]},
total: 1,
}},
"Test_aggregator_metric2": {
"ns 1": &aggregatedRecord{
labels: prometheus.Labels{"Namespace": testData[0]["Namespace"]},
total: 1,
}},
}, a.result)

a.count(testFinding(0))

assert.Equal(t, map[MetricName]map[aggregationKey]*aggregatedRecord{
"Test_aggregator_metric1": {
"cluster 1|CRITICAL": &aggregatedRecord{
labels: prometheus.Labels{
"Cluster": testData[0]["Cluster"],
"Severity": testData[0]["Severity"]},
total: 2,
}},
"Test_aggregator_metric2": {
"ns 1": &aggregatedRecord{
labels: prometheus.Labels{
"Namespace": testData[0]["Namespace"]},
total: 2,
}},
}, a.result)

a.count(testFinding(1))

assert.Equal(t, map[MetricName]map[aggregationKey]*aggregatedRecord{
"Test_aggregator_metric1": {
"cluster 1|CRITICAL": &aggregatedRecord{
labels: prometheus.Labels{
"Cluster": testData[0]["Cluster"],
"Severity": testData[0]["Severity"]},
total: 2,
},
"cluster 2|HIGH": &aggregatedRecord{
labels: prometheus.Labels{
"Cluster": testData[1]["Cluster"],
"Severity": testData[1]["Severity"]},
total: 1,
},
},
"Test_aggregator_metric2": {
"ns 1": &aggregatedRecord{
labels: prometheus.Labels{"Namespace": testData[0]["Namespace"]},
total: 2,
},
"ns 2": &aggregatedRecord{
labels: prometheus.Labels{"Namespace": testData[1]["Namespace"]},
total: 1,
},
}}, a.result)

a.count(testFinding(1))

assert.Equal(t, map[MetricName]map[aggregationKey]*aggregatedRecord{
"Test_aggregator_metric1": {
"cluster 1|CRITICAL": &aggregatedRecord{
labels: prometheus.Labels{"Cluster": testData[0]["Cluster"], "Severity": testData[0]["Severity"]},
total: 2,
},
"cluster 2|HIGH": &aggregatedRecord{
labels: prometheus.Labels{"Cluster": testData[1]["Cluster"], "Severity": testData[1]["Severity"]},
total: 2,
},
},
"Test_aggregator_metric2": {
"ns 1": &aggregatedRecord{
labels: prometheus.Labels{"Namespace": testData[0]["Namespace"]},
total: 2,
},
"ns 2": &aggregatedRecord{
labels: prometheus.Labels{"Namespace": testData[1]["Namespace"]},
total: 2,
},
},
}, a.result)
}

func Test_makeAggregationKey(t *testing.T) {
testMetric := map[Label]string{
"Cluster": "value",
"IsFixable": "false",
}
getter := func(label Label) string {
return testMetric[label]
}
key, labels := makeAggregationKey(
[]Label{"Cluster", "IsFixable"},
getter,
testLabelOrder)

assert.Equal(t, aggregationKey("value|false"), key)
assert.Equal(t, prometheus.Labels{
"Cluster": "value",
"IsFixable": "false",
}, labels)

}

func Test_collectMatchingLabels(t *testing.T) {
i := 0
for range collectMatchingLabels([]Label{"label1", "label2", "label3"},
func(l Label) string {
i++
return "value"
}) {
break
}
assert.Equal(t, 1, i)
}
31 changes: 31 additions & 0 deletions central/metrics/custom/tracker/ordered_values.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package tracker

import (
"slices"
"strings"
)

// orderedValues is a list of elements knowing their order.
type orderedValues []valueOrder

type valueOrder struct {
int
string
}

func (ov valueOrder) cmp(b valueOrder) int {
return ov.int - b.int
}

// join the elements according to their order.
func (ov orderedValues) join(sep rune) string {
slices.SortFunc(ov, valueOrder.cmp)
sb := strings.Builder{}
for _, value := range ov {
if sb.Len() > 0 {
sb.WriteRune(sep)
}
sb.WriteString(value.string)
}
return sb.String()
}
32 changes: 32 additions & 0 deletions central/metrics/custom/tracker/ordered_values_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package tracker

import (
"testing"

"github.com/stretchr/testify/assert"
)

func Test_orderedValues(t *testing.T) {
ov := orderedValues{
{int: 2, string: "b"},
{int: 1, string: "a"},
{int: 3, string: "c"},
}
assert.Equal(t, "a,b,c", ov.join(','))

// Test with duplicate ints.
ov2 := orderedValues{
{int: 2, string: "b"},
{int: 1, string: "a"},
{int: 2, string: "bb"},
}
joined := ov2.join('-')
assert.True(t, joined == "a-b-bb" || joined == "a-bb-b", joined)

// Test with empty slice.
assert.Empty(t, orderedValues{}.join(','))

// Test with single element
ov4 := orderedValues{{int: 5, string: "s"}}
assert.Equal(t, "s", ov4.join('|'))
}
Loading