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
4 changes: 4 additions & 0 deletions central/metrics/aggregator/common/condition.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
arg string
}

func (c *Condition) Equals(b *Condition) bool {
return c == b || c != nil && b != nil && c.op == b.op && c.arg == b.arg

Check warning on line 28 in central/metrics/aggregator/common/condition.go

View check run for this annotation

Codecov / codecov/patch

central/metrics/aggregator/common/condition.go#L27-L28

Added lines #L27 - L28 were not covered by tests
}

// MakeCondition constructs an condition.
func MakeCondition(op, arg string) (*Condition, error) {
cond := &Condition{operator(op), arg}
Expand Down
67 changes: 65 additions & 2 deletions central/metrics/aggregator/common/config_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,39 @@
"maps"
"regexp"
"slices"
"time"

"github.com/pkg/errors"
"github.com/stackrox/rox/central/metrics"
v1 "github.com/stackrox/rox/generated/api/v1"
"github.com/stackrox/rox/generated/storage"
"github.com/stackrox/rox/pkg/errox"
"github.com/stackrox/rox/pkg/search"
)

var (
errInvalidConfiguration = errox.InvalidArgs.New("invalid configuration")

// Source: https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels
metricNamePattern = regexp.MustCompile("^[a-zA-Z_:][a-zA-Z0-9_:]*$")

registryNamePattern = regexp.MustCompile("^[a-zA-Z0-9-_]*$")
)

type metricExposure struct {
registry string
exposure metrics.Exposure
}

type Configuration struct {
metrics MetricsConfiguration
metricRegistry map[MetricName]metricExposure
toAdd []MetricName
toDelete []MetricName
filter *v1.Query
period time.Duration
}

func isKnownLabel(label string, labelOrder map[Label]int) bool {
_, ok := labelOrder[Label(label)]
return ok
Expand All @@ -33,11 +53,17 @@
return nil
}

// ParseMetricLabels converts the storage object to the usable map, validating
// parseMetricLabels converts the storage object to the usable map, validating
// the values.
func ParseMetricLabels(config map[string]*storage.PrometheusMetricsConfig_Labels, labelOrder map[Label]int) (MetricsConfiguration, error) {
func parseMetricLabels(config map[string]*storage.PrometheusMetricsConfig_Labels, labelOrder map[Label]int) (MetricsConfiguration, error) {
result := make(MetricsConfiguration, len(config))
for metric, labels := range config {

if !registryNamePattern.MatchString(labels.GetRegistryName()) {
return nil, errInvalidConfiguration.CausedByf(
`registry name %q for metric %s doesn't match "`+registryNamePattern.String()+`"`, labels.GetRegistryName(), metric)
}

if err := validateMetricName(metric); err != nil {
return nil, errInvalidConfiguration.CausedByf(
"invalid metric name %q: %v", metric, err)
Expand Down Expand Up @@ -68,3 +94,40 @@
}
return result, nil
}

func ParseConfiguration(cfg *storage.PrometheusMetricsConfig_Metrics, currentMetrics MetricsConfiguration, labelOrder map[Label]int) (*Configuration, error) {
metricRegistry := make(map[MetricName]metricExposure, len(cfg.GetMetrics()))

for metric, labels := range cfg.GetMetrics() {
if err := metrics.CheckExposureChange(metric,
labels.GetRegistryName(),
metrics.Exposure(labels.GetExposure())); err != nil {
return nil, errInvalidConfiguration.CausedBy(err)
}

metricRegistry[MetricName(metric)] = metricExposure{
labels.GetRegistryName(),
metrics.Exposure(labels.GetExposure())}
}

mcfg, err := parseMetricLabels(cfg.GetMetrics(), labelOrder)
if err != nil {
return nil, err
}
toAdd, toDelete, changed := currentMetrics.DiffLabels(mcfg)
if len(changed) != 0 {
return nil, errInvalidConfiguration.CausedByf("cannot alter metrics %v", changed)
}
q, err := search.ParseQuery(cfg.GetFilter(), search.MatchAllIfEmpty())
if err != nil {
return nil, errInvalidConfiguration.CausedBy(err)
}

Check warning on line 124 in central/metrics/aggregator/common/config_parser.go

View check run for this annotation

Codecov / codecov/patch

central/metrics/aggregator/common/config_parser.go#L123-L124

Added lines #L123 - L124 were not covered by tests
return &Configuration{
metrics: mcfg,
metricRegistry: metricRegistry,
toAdd: toAdd,
toDelete: toDelete,
filter: q,
period: time.Minute * time.Duration(cfg.GetGatheringPeriodMinutes()),
}, nil
}
159 changes: 153 additions & 6 deletions central/metrics/aggregator/common/config_parser_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
package common

import (
"strings"
"testing"

"github.com/stackrox/rox/central/metrics"
"github.com/stackrox/rox/generated/storage"
"github.com/stretchr/testify/assert"
)

func Test_parseMetricLabels(t *testing.T) {
config := makeTestMetricLabels(t)
labelExpression, err := ParseMetricLabels(config, testLabelOrder)
labelExpression, err := parseMetricLabels(config, testLabelOrder)
assert.NoError(t, err)
assert.Equal(t, makeTestMetricLabelExpression(t), labelExpression)
}
Expand Down Expand Up @@ -41,11 +43,11 @@ func Test_noLabels(t *testing.T) {
"metric2": {},
"metric3": nil,
}
labelExpression, err := ParseMetricLabels(config, testLabelOrder)
labelExpression, err := parseMetricLabels(config, testLabelOrder)
assert.NoError(t, err)
assert.Empty(t, labelExpression)

labelExpression, err = ParseMetricLabels(nil, testLabelOrder)
labelExpression, err = parseMetricLabels(nil, testLabelOrder)
assert.NoError(t, err)
assert.Empty(t, labelExpression)
}
Expand All @@ -58,13 +60,13 @@ func Test_parseErrors(t *testing.T) {
},
},
}
labelExpression, err := ParseMetricLabels(config, testLabelOrder)
labelExpression, err := parseMetricLabels(config, testLabelOrder)
assert.Equal(t, `invalid configuration: label "unknown" for metric "metric1" is not in the list of known labels: [CVE CVSS Cluster IsFixable Namespace Severity test]`, err.Error())
assert.Empty(t, labelExpression)

delete(config, "metric1")
config["met rick"] = nil
labelExpression, err = ParseMetricLabels(config, testLabelOrder)
labelExpression, err = parseMetricLabels(config, testLabelOrder)
assert.Equal(t, `invalid configuration: invalid metric name "met rick": doesn't match "^[a-zA-Z_:][a-zA-Z0-9_:]*$"`, err.Error())
assert.Empty(t, labelExpression)

Expand All @@ -81,7 +83,152 @@ func Test_parseErrors(t *testing.T) {
},
},
}
labelExpression, err = ParseMetricLabels(config, testLabelOrder)
labelExpression, err = parseMetricLabels(config, testLabelOrder)
assert.Equal(t, `invalid configuration: failed to parse a condition for metric "metric1" with label "test": operator in "smoothy" is not one of ["="]`, err.Error())
assert.Empty(t, labelExpression)
}

func TestParseConfiguration(t *testing.T) {
t.Run("bad metric name", func(t *testing.T) {
cfg, err := ParseConfiguration(&storage.PrometheusMetricsConfig_Metrics{
GatheringPeriodMinutes: 121,
Metrics: map[string]*storage.PrometheusMetricsConfig_Labels{
" ": nil,
},
}, nil, testLabelOrder)

assert.ErrorIs(t, err, errInvalidConfiguration)
assert.Equal(t, `invalid configuration: invalid metric name " ": doesn't match "^[a-zA-Z_:][a-zA-Z0-9_:]*$"`, err.Error())
assert.Nil(t, cfg)
})

t.Run("bad registry name", func(t *testing.T) {
cfg, err := ParseConfiguration(&storage.PrometheusMetricsConfig_Metrics{
GatheringPeriodMinutes: 121,
Metrics: map[string]*storage.PrometheusMetricsConfig_Labels{
"m1": {
RegistryName: "bad name",
},
},
}, nil, testLabelOrder)

assert.ErrorIs(t, err, errInvalidConfiguration)
assert.Equal(t, `invalid configuration: registry name "bad name" for metric m1 doesn't match "^[a-zA-Z0-9-_]*$"`, err.Error())
assert.Nil(t, cfg)
})

t.Run("test parse sequence", func(t *testing.T) {
// Good:
cfg0, err := ParseConfiguration(&storage.PrometheusMetricsConfig_Metrics{
GatheringPeriodMinutes: 121,
Metrics: makeTestMetricLabels(t),
Filter: "Cluster:name",
}, nil, testLabelOrder)

assert.NoError(t, err)
assert.Equal(t, "Cluster", cfg0.filter.GetBaseQuery().GetMatchFieldQuery().GetField())
if assert.NotNil(t, cfg0.metrics) {
assert.Equal(t, makeTestMetricLabelExpression(t), cfg0.metrics)
}

// Bad:
cfg1, err := ParseConfiguration(&storage.PrometheusMetricsConfig_Metrics{
GatheringPeriodMinutes: 121,
Metrics: map[string]*storage.PrometheusMetricsConfig_Labels{
"m1": {
Labels: map[string]*storage.PrometheusMetricsConfig_Labels_Expression{
"label1": nil,
},
},
},
}, nil, testLabelOrder)
assert.ErrorIs(t, err, errInvalidConfiguration)
assert.Equal(t, `invalid configuration: label "label1" for metric "m1" is not in the list of known labels: [CVE CVSS Cluster IsFixable Namespace Severity test]`, err.Error())
assert.Nil(t, cfg1)

// Another good:
cfg2, err := ParseConfiguration(&storage.PrometheusMetricsConfig_Metrics{
GatheringPeriodMinutes: 121,
Metrics: map[string]*storage.PrometheusMetricsConfig_Labels{
"m2": {
Labels: map[string]*storage.PrometheusMetricsConfig_Labels_Expression{
"Cluster": nil,
},
},
},
Filter: "Namespace:name",
}, cfg0.metrics, testLabelOrder)

assert.NoError(t, err)
assert.Equal(t, "Namespace", cfg2.filter.GetBaseQuery().GetMatchFieldQuery().GetField())
if assert.NotNil(t, cfg2.metrics) {
assert.NotNil(t, cfg2.metrics["m2"])
assert.Nil(t, cfg2.metrics["m1"])
}
})

t.Run("test bad query", func(t *testing.T) {
cfg, err := ParseConfiguration(&storage.PrometheusMetricsConfig_Metrics{
GatheringPeriodMinutes: 121,
Metrics: makeTestMetricLabels(t),
Filter: "bad query?",
}, nil, testLabelOrder)

assert.NoError(t, err)
if assert.NotNil(t, cfg) {
assert.Empty(t, cfg.filter.GetBaseQuery().GetMatchFieldQuery().GetField())
}
})

t.Run("change exposure", func(t *testing.T) {
storageConfig := &storage.PrometheusMetricsConfig_Metrics{
GatheringPeriodMinutes: 121,
Metrics: makeTestMetricLabels(t),
}
cfg0, err := ParseConfiguration(storageConfig, nil, testLabelOrder)
assert.NoError(t, err)

for _, labels := range storageConfig.Metrics {
if labels.Exposure == storage.PrometheusMetricsConfig_Labels_BOTH {
labels.Exposure = storage.PrometheusMetricsConfig_Labels_INTERNAL
}
}

for _, metric := range cfg0.toAdd {
regCfg := cfg0.metricRegistry[metric]
if err := metrics.RegisterCustomAggregatedMetric(
string(metric),
"test",
cfg0.period,
getMetricLabels(cfg0.metrics[metric], testLabelOrder),
regCfg.registry,
metrics.Exposure(regCfg.exposure)); err != nil {
assert.Failf(t, "Failed to register test metric", "%q: %v", metric, err)
}
}

cfg1, err := ParseConfiguration(storageConfig, cfg0.metrics, testLabelOrder)

assert.ErrorIs(t, err, errInvalidConfiguration, err)
assert.Nil(t, cfg1)
})

t.Run("change labels", func(t *testing.T) {
storageConfig := &storage.PrometheusMetricsConfig_Metrics{
GatheringPeriodMinutes: 121,
Metrics: makeTestMetricLabels(t),
}
cfg0, err := ParseConfiguration(storageConfig, nil, testLabelOrder)
assert.NoError(t, err)
for _, config := range storageConfig.Metrics {
if config.Exposure == storage.PrometheusMetricsConfig_Labels_BOTH {
config.Labels["CVE"] = &storage.PrometheusMetricsConfig_Labels_Expression{}
}
}
cfg1, err := ParseConfiguration(storageConfig, cfg0.metrics, testLabelOrder)

assert.ErrorIs(t, err, errInvalidConfiguration, err)
assert.True(t, strings.Contains(err.Error(), "cannot alter metrics"))
assert.Nil(t, cfg1)
})
}
21 changes: 21 additions & 0 deletions central/metrics/aggregator/common/configuration.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package common

import (
"maps"
"slices"
)

Expand All @@ -26,6 +27,26 @@ func (mcfg MetricsConfiguration) HasAnyLabelOf(labels []Label) bool {
return false
}

func (mcfg MetricsConfiguration) DiffLabels(another MetricsConfiguration) ([]MetricName, []MetricName, []MetricName) {
if mcfg == nil && another == nil {
return nil, nil, nil
}
var toAdd, toDelete, changed []MetricName
for metric, labels := range mcfg {
if anotherLabels, ok := another[metric]; !ok {
toDelete = append(toDelete, metric)
} else if !maps.EqualFunc(labels, anotherLabels, Expression.Equals) {
changed = append(changed, metric)
}
}
for metric := range another {
if _, ok := mcfg[metric]; !ok {
toAdd = append(toAdd, metric)
}
}
return toAdd, toDelete, changed
}

// MakeLabelOrderMap maps labels to their order according to the order of
// the labels in the list of getters.
func MakeLabelOrderMap[Finding Countable](getters []LabelGetter[Finding]) map[Label]int {
Expand Down
Loading
Loading