Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
49da319
track on timer
parametalol Apr 22, 2025
ad1bee3
refactoring
parametalol May 16, 2025
6881879
refactoring wip
parametalol May 16, 2025
1423912
wip
parametalol May 16, 2025
408bf18
wip
parametalol May 16, 2025
25e1362
wip
parametalol May 16, 2025
5c91ad0
common aggregator
parametalol May 18, 2025
ffc5ac3
refactor reconfiguration
parametalol May 18, 2025
6fef275
fix test
parametalol May 18, 2025
a43b4f2
refactor tracker config
parametalol May 18, 2025
cafb650
naming, nits and other refactorings
parametalol May 18, 2025
920daac
rename labelsGetter to finding
parametalol May 18, 2025
78243ba
another refactoring
parametalol May 18, 2025
3917293
naming
parametalol May 19, 2025
5547008
proto.lock
parametalol May 19, 2025
d47b94f
style
parametalol May 19, 2025
33c2f08
fix runner
parametalol May 19, 2025
efdd356
runner tests
parametalol May 19, 2025
7111505
avoid panic on metric reregistration
parametalol May 19, 2025
cac9b4b
test register metrics twice
parametalol May 19, 2025
8833143
fix iterator interruption, use pkg/errors
parametalol May 19, 2025
ed13535
another refactoring
parametalol May 19, 2025
c0c59ab
optimize vulnerabilities tracker a little
parametalol May 20, 2025
f67710a
refactor getters
parametalol May 21, 2025
a5d0135
labels getter refactoring
parametalol May 21, 2025
9414701
nit
parametalol May 21, 2025
d780930
sync metrics map
parametalol May 21, 2025
a4ef364
style
parametalol May 21, 2025
6416ca7
fix tests
parametalol May 21, 2025
dfe4d77
rename message in proto
parametalol May 22, 2025
5c930c1
rename in the code
parametalol May 22, 2025
5c143ac
don't track empty config, image query optimization
parametalol May 26, 2025
80b01a6
fix after empty config optimization
parametalol May 27, 2025
47ce9b1
finding Count()
parametalol May 27, 2025
f00ab88
query config parameter
parametalol May 27, 2025
c2b28c0
walk by image cves
parametalol May 27, 2025
c961f2b
trest walk by image cves
parametalol May 27, 2025
5e412e7
style
parametalol May 28, 2025
03dbdb6
unregister disabled metrics
parametalol May 28, 2025
5af869b
fix metric registration
parametalol May 28, 2025
dd1a161
style
parametalol May 28, 2025
b0cb6da
proto.lock
parametalol May 29, 2025
666c0f2
platform matcher
parametalol May 29, 2025
a445c71
lazy platform matcher initialization
parametalol May 29, 2025
c092786
better errors
parametalol May 31, 2025
313a778
renamed expression to condition
parametalol May 31, 2025
230fb46
refactor expression
parametalol May 31, 2025
5cb7a1d
oops ui
parametalol May 31, 2025
1ef205f
proto.lock
parametalol May 31, 2025
84a91b7
rename label_expressions to labels
parametalol Jun 2, 2025
e283db9
proto.lock
parametalol Jun 2, 2025
a7401c3
proto: renamed query to filter, hours to minutes
parametalol Jun 3, 2025
eb92621
review feedback, no query selection
parametalol Jun 3, 2025
af53fb0
revert cve store.go
parametalol Jun 3, 2025
c3ba955
OneOrMore countable
parametalol Jun 3, 2025
7397978
addressing code review comments
parametalol Jun 3, 2025
be8a8cf
proto.lock
parametalol Jun 3, 2025
7a8c1a5
fix test
parametalol Jun 4, 2025
d21a272
fix generated
parametalol Jun 4, 2025
7eaaf3e
ticker refactoring
parametalol Jun 10, 2025
67f6772
style, test fix
parametalol Jun 12, 2025
b3ec6a8
refactor singleton
parametalol Jun 12, 2025
66c1662
refactor aggregator
parametalol Jun 12, 2025
af17142
nits
parametalol Jun 12, 2025
8405b8b
comment and test forEachImageVuln
parametalol Jun 12, 2025
ee180cf
rename Vulnerabilities to Metrics in config.proto
parametalol Jun 12, 2025
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
13 changes: 10 additions & 3 deletions central/config/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/stackrox/rox/central/config/datastore"
"github.com/stackrox/rox/central/convert/storagetov1"
"github.com/stackrox/rox/central/convert/v1tostorage"
"github.com/stackrox/rox/central/metrics/aggregator"
"github.com/stackrox/rox/central/platform/matcher"
"github.com/stackrox/rox/central/platform/reprocessor"
"github.com/stackrox/rox/central/telemetry/centralclient"
Expand Down Expand Up @@ -66,16 +67,18 @@ type Service interface {
}

// New returns a new Service instance using the given DataStore.
func New(datastore datastore.DataStore) Service {
func New(datastore datastore.DataStore, ar aggregator.Runner) Service {
return &serviceImpl{
datastore: datastore,
datastore: datastore,
aggregator: ar,
}
}

type serviceImpl struct {
v1.UnimplementedConfigServiceServer

datastore datastore.DataStore
datastore datastore.DataStore
aggregator aggregator.Runner
}

// RegisterServiceServer registers this service with the given gRPC Server.
Expand Down Expand Up @@ -170,6 +173,10 @@ func (s *serviceImpl) PutConfig(ctx context.Context, req *v1.PutConfigRequest) (
}
matcher.Singleton().SetRegexes(regexes)
go reprocessor.Singleton().RunReprocessor()

if err := s.aggregator.Reconfigure(req.GetConfig().GetPrivateConfig().GetPrometheusMetricsConfig()); err != nil {
return nil, err
}
return req.GetConfig(), nil
}

Expand Down
3 changes: 2 additions & 1 deletion central/config/service/singleton.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package service

import (
"github.com/stackrox/rox/central/config/datastore"
"github.com/stackrox/rox/central/metrics/aggregator"
"github.com/stackrox/rox/pkg/sync"
)

Expand All @@ -12,7 +13,7 @@ var (
)

func initialize() {
as = New(datastore.Singleton())
as = New(datastore.Singleton(), aggregator.Singleton())
}

// Singleton provides the instance of the Service interface to register.
Expand Down
14 changes: 13 additions & 1 deletion central/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ import (
"github.com/stackrox/rox/central/jwt"
logimbueHandler "github.com/stackrox/rox/central/logimbue/handler"
metadataService "github.com/stackrox/rox/central/metadata/service"
customMetrics "github.com/stackrox/rox/central/metrics/aggregator"
"github.com/stackrox/rox/central/metrics/telemetry"
mitreService "github.com/stackrox/rox/central/mitre/service"
namespaceService "github.com/stackrox/rox/central/namespace/service"
Expand Down Expand Up @@ -381,7 +382,7 @@ func startServices() {
administrationUsageInjector.Singleton().Start()
gcp.Singleton().Start()
administrationEventHandler.Singleton().Start()

customMetrics.Singleton().Start()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Something does not fit here right. The aggregator is a part of the config service, so I would expect that the config service would be started here, not the aggregator. Why cannot we keep the hierarchy here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The aggregator is not a part of the config service. The latter allows for reconfiguring the former in runtime.

if features.PlatformComponents.Enabled() {
platformReprocessor.Singleton().Start()
}
Expand Down Expand Up @@ -866,6 +867,16 @@ func customRoutes() (customRoutes []routes.CustomRoute) {
ServerHandler: certHandler.BackupCerts(listener.Singleton()),
Compression: true,
},
{
// User configured Prometheus metrics will be exposed on this path.
// The access is behind authorization because the metric label
// values may include sensitive data, such as deployment names and
// CVEs.
Route: "/metrics",
Authorizer: user.With(permissions.View(resources.Administration)),
ServerHandler: customMetrics.Singleton(),
Compression: true,
},
}
scannerDefinitionsRoute := "/api/extensions/scannerdefinitions"
// Only grant compression to well-known content types. It should capture files
Expand Down Expand Up @@ -962,6 +973,7 @@ func waitForTerminationSignal() {
{gcp.Singleton(), "GCP cloud credentials manager"},
{cloudSourcesManager.Singleton(), "cloud sources manager"},
{administrationEventHandler.Singleton(), "administration events handler"},
{customMetrics.Singleton(), "custom Prometheus metrics gatherer"},
}

stoppables = append(stoppables,
Expand Down
43 changes: 43 additions & 0 deletions central/metrics/aggregator/common/aggregator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package common

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

// aggregatedRecord is a single gauge metric record.
type aggregatedRecord struct {
labels prometheus.Labels
total int
}
Comment on lines +7 to +11
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cannot we reuse the type that is built-in prometheus library?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Please suggest a type.


// aggregator computes the aggregation result.
type aggregator[Finding Countable] struct {
result map[MetricName]map[aggregationKey]*aggregatedRecord
mcfg MetricsConfiguration
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is type MetricsConfiguration map[MetricName]map[Label]Expression and it has its own type-alias, but the other does not have it. Why cannot we use the map[MetricName]map[Label]Expression as type here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Because MetricsConfiguration has methods and is used in other places as such.

labelOrder map[Label]int
getters map[Label]func(Finding) string
}

func makeAggregator[Finding Countable](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 += finding.Count()
} else {
r.result[metric][key] = &aggregatedRecord{labels, finding.Count()}
}
}
}
}
107 changes: 107 additions & 0 deletions central/metrics/aggregator/common/aggregator_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package common

import (
"testing"

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

type testFinding struct {
OneOrMore
index int
}

func Test_aggregator(t *testing.T) {
getters := map[Label]func(testFinding) string{
"Severity": func(tf testFinding) string { return testData[tf.index]["Severity"] },
"Cluster": func(tf testFinding) string { return testData[tf.index]["Cluster"] },
"Namespace": func(tf testFinding) string { return testData[tf.index]["Namespace"] },
}
a := makeAggregator(makeTestMetricLabelExpression(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{index: 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{index: 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{index: 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{OneOrMore: 5, index: 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: 6,
},
},
"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: 6,
},
},
}, a.result)
}
77 changes: 77 additions & 0 deletions central/metrics/aggregator/common/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package common

import (
"context"
"iter"
"regexp"
"slices"

"github.com/pkg/errors"
v1 "github.com/stackrox/rox/generated/api/v1"
"github.com/stackrox/rox/pkg/logging"
)

var (
log = logging.CreateLogger(logging.ModuleForName("central_metrics"), 1)

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

type Label string // Prometheus label.
type MetricName string // Prometheus metric name.
type Countable interface{ Count() int }
type FindingGenerator[Finding Countable] func(context.Context, *v1.Query, MetricsConfiguration) iter.Seq[Finding]
type LabelGetter[Finding Countable] struct {
Label Label
Getter func(Finding) string
}

// MetricsConfiguration is the parsed aggregation configuration.
type MetricsConfiguration map[MetricName]map[Label]Expression

func (mcfg MetricsConfiguration) HasAnyLabelOf(labels []Label) bool {
for _, labelExpr := range mcfg {
for label := range labelExpr {
if slices.Contains(labels, label) {
return true
}
}
}
return false
}
Comment on lines +34 to +42
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This smells like we could use some optimization from https://pkg.go.dev/maps. Could we somehow smartly apply the maps.Values here? (optional)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

github.com/parametalol/curry could help.


// OneOrMore is a helper implementation of Countable interface, that counts 1
// by default. Can be used as a base for other implementations.
type OneOrMore int

func (o OneOrMore) Count() int {
if o > 0 {
return int(o)
}
return 1
}

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

var ErrStopIterator = errors.New("stopped")

// validateMetricName ensures the name is alnum_.
func validateMetricName(name string) error {
if len(name) == 0 {
return errors.New("empty")
}
if !metricNamePattern.MatchString(name) {
return errors.New(`doesn't match "` + metricNamePattern.String() + `"`)
}
return nil
}

// Bind4th binds the fourth function argument:
//
// f(a, b, c, d) == Bind4th(f, d)(a, b, c)
func Bind4th[A1 any, A2 any, A3 any, A4 any, RV any](f func(A1, A2, A3, A4) RV, bound A4) func(A1, A2, A3) RV {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

make it a lib :)
it will be super popular with C++ devs who happen to write in Go 😉

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

return func(a1 A1, a2 A2, a3 A3) RV {
return f(a1, a2, a3, bound)
}
}
Loading
Loading