Skip to content
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
4 changes: 2 additions & 2 deletions central/metrics/custom/image_vulnerabilities/tracker.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ import (
"github.com/stackrox/rox/pkg/search"
)

func New(registry metrics.CustomRegistry, ds deploymentDS.DataStore) *tracker.TrackerBase[*finding] {
func New(registryFactory func(string) metrics.CustomRegistry, ds deploymentDS.DataStore) *tracker.TrackerBase[*finding] {
return tracker.MakeTrackerBase(
"vulnerabilities",
"CVEs",
lazyLabels,
func(ctx context.Context, md tracker.MetricDescriptors) iter.Seq[*finding] {
return trackVulnerabilityMetrics(ctx, md, ds)
},
registry)
registryFactory)
}

func trackVulnerabilityMetrics(ctx context.Context, _ tracker.MetricDescriptors, ds deploymentDS.DataStore) iter.Seq[*finding] {
Expand Down
3 changes: 2 additions & 1 deletion central/metrics/custom/image_vulnerabilities/tracker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/prometheus/client_golang/prometheus"
deploymentMockDS "github.com/stackrox/rox/central/deployment/datastore/mocks"
"github.com/stackrox/rox/central/metrics"
"github.com/stackrox/rox/central/metrics/mocks"
v1 "github.com/stackrox/rox/generated/api/v1"
"github.com/stackrox/rox/generated/storage"
Expand Down Expand Up @@ -119,7 +120,7 @@ func TestQueryDeploymentsAndImages(t *testing.T) {
var actual = make(map[string][]*labelsTotal)

mr := mocks.NewMockCustomRegistry(ctrl)
tracker := New(mr, ds)
tracker := New(func(string) metrics.CustomRegistry { return mr }, ds)
mr.EXPECT().RegisterMetric(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Times(3)
mr.EXPECT().SetTotal(gomock.Any(), gomock.Any(), gomock.Any()).
Expand Down
4 changes: 2 additions & 2 deletions central/metrics/custom/policy_violations/tracker.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ import (
"github.com/stackrox/rox/pkg/search"
)

func New(registry metrics.CustomRegistry, ds alertDS.DataStore) *tracker.TrackerBase[*finding] {
func New(registryFactory func(string) metrics.CustomRegistry, ds alertDS.DataStore) *tracker.TrackerBase[*finding] {
return tracker.MakeTrackerBase(
"alerts",
"policy violations",
lazyLabels,
func(ctx context.Context, _ tracker.MetricDescriptors) iter.Seq[*finding] {
return trackViolations(ctx, ds)
},
registry)
registryFactory)
}

func trackViolations(ctx context.Context, ds alertDS.DataStore) iter.Seq[*finding] {
Expand Down
17 changes: 9 additions & 8 deletions central/metrics/custom/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
)

type aggregatorRunner struct {
registry metrics.CustomRegistry
image_vulnerabilities custom.Tracker
policy_violations custom.Tracker
}
Expand All @@ -32,11 +31,10 @@ type RunnerConfiguration struct {
policy_violations *custom.Configuration
}

func makeRunner(registry metrics.CustomRegistry, dds deploymentDS.DataStore, ads alertDS.DataStore) *aggregatorRunner {
func makeRunner(registryFactory func(string) metrics.CustomRegistry, dds deploymentDS.DataStore, ads alertDS.DataStore) *aggregatorRunner {
return &aggregatorRunner{
registry: registry,
image_vulnerabilities: image_vulnerabilities.New(registry, dds),
policy_violations: policy_violations.New(registry, ads),
image_vulnerabilities: image_vulnerabilities.New(registryFactory, dds),
policy_violations: policy_violations.New(registryFactory, ads),
}
}

Expand Down Expand Up @@ -91,13 +89,16 @@ func (ar *aggregatorRunner) ServeHTTP(w http.ResponseWriter, req *http.Request)
if ar == nil {
return
}
var userID string
if id := authn.IdentityFromContextOrNil(req.Context()); id != nil {
userID = id.UID()
// The request context is cancelled when the client's connection closes.
ctx := authn.CopyContextIdentity(context.Background(), req.Context())
go ar.image_vulnerabilities.Gather(ctx)
go ar.policy_violations.Gather(ctx)
}
ar.registry.Lock()
defer ar.registry.Unlock()
ar.registry.ServeHTTP(w, req)
registry := metrics.GetCustomRegistry(userID)
registry.Lock()
defer registry.Unlock()
registry.ServeHTTP(w, req)
}
8 changes: 4 additions & 4 deletions central/metrics/custom/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestRunner_makeRunner(t *testing.T) {
Metrics: nil,
},
nil)
runner := makeRunner(metrics.MakeCustomRegistry(), nil, nil)
runner := makeRunner(metrics.GetCustomRegistry, nil, nil)
runner.initialize(cds)
assert.NotNil(t, runner)

Expand All @@ -54,7 +54,7 @@ func TestRunner_makeRunner(t *testing.T) {
cds.EXPECT().GetPrivateConfig(gomock.Any()).Times(1).Return(
nil,
errors.New("DB error"))
runner := makeRunner(metrics.MakeCustomRegistry(), nil, nil)
runner := makeRunner(metrics.GetCustomRegistry, nil, nil)
assert.NotNil(t, runner)
runner.initialize(cds)

Expand Down Expand Up @@ -142,7 +142,7 @@ func TestRunner_ServeHTTP(t *testing.T) {
}).
Return(nil)

runner := makeRunner(metrics.MakeCustomRegistry(), dds, ads)
runner := makeRunner(metrics.GetCustomRegistry, dds, ads)
runner.initialize(cds)
runner.image_vulnerabilities.Gather(makeAdminContext(t))
runner.policy_violations.Gather(makeAdminContext(t))
Expand All @@ -155,7 +155,7 @@ func TestRunner_ServeHTTP(t *testing.T) {

t.Run("body", func(t *testing.T) {
rec := httptest.NewRecorder()
req := httptest.NewRequestWithContext(context.Background(),
req := httptest.NewRequestWithContext(makeAdminContext(t),
"GET", "/metrics", nil)
runner.ServeHTTP(rec, req)

Expand Down
2 changes: 1 addition & 1 deletion central/metrics/custom/singleton.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type Runner interface {
// initialization. nil runner is safe, but no-op.
func Singleton() Runner {
onceRunner.Do(func() {
runner = makeRunner(metrics.MakeCustomRegistry(), deploymentDS.Singleton(), alertDS.Singleton())
runner = makeRunner(metrics.GetCustomRegistry, deploymentDS.Singleton(), alertDS.Singleton())
go runner.initialize(configDS.Singleton())
})
return runner
Expand Down
68 changes: 51 additions & 17 deletions central/metrics/custom/tracker/tracker_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/pkg/errors"
"github.com/stackrox/rox/central/metrics"
"github.com/stackrox/rox/generated/storage"
"github.com/stackrox/rox/pkg/grpc/authn"
"github.com/stackrox/rox/pkg/logging"
"github.com/stackrox/rox/pkg/sync"
)
Expand Down Expand Up @@ -80,7 +81,9 @@ type TrackerBase[Finding WithError] struct {
config *Configuration
metricsConfigMux sync.RWMutex

gatherer *gatherer
gatherers sync.Map // map[user ID]tokenGatherer

registryFactory func(userID string) metrics.CustomRegistry // for mocking in tests.
}

// makeGettersMap transforms a list of label names with their getters to a map.
Expand All @@ -96,15 +99,15 @@ func makeGettersMap[Finding WithError](getters []LazyLabel[Finding]) map[Label]f
// configuration. Call Reconfigure to configure the period and the metrics.
func MakeTrackerBase[Finding WithError](category, description string,
getters []LazyLabel[Finding], generator FindingGenerator[Finding],
registry metrics.CustomRegistry,
registryFactory func(string) metrics.CustomRegistry,
) *TrackerBase[Finding] {
return &TrackerBase[Finding]{
category: category,
description: description,
labelOrder: MakeLabelOrderMap(getters),
getters: makeGettersMap(getters),
generator: generator,
gatherer: &gatherer{registry: registry},
category: category,
description: description,
labelOrder: MakeLabelOrderMap(getters),
getters: makeGettersMap(getters),
generator: generator,
registryFactory: registryFactory,
}
}

Expand Down Expand Up @@ -159,17 +162,21 @@ func labelsAsStrings(labels []Label) []string {
}

func (tracker *TrackerBase[Finding]) unregisterMetrics(metrics []MetricName) {
for _, metric := range metrics {
if tracker.gatherer.registry.UnregisterMetric(string(metric)) {
log.Debugf("Unregistered %s Prometheus metric %q", tracker.category, metric)
tracker.gatherers.Range(func(userID, g any) bool {
for _, metric := range metrics {
g.(*gatherer).registry.UnregisterMetric(string(metric))
}
}
return true
})
}

func (tracker *TrackerBase[Finding]) registerMetrics(cfg *Configuration, metrics []MetricName) {
for _, metric := range metrics {
tracker.registerMetric(tracker.gatherer, cfg, metric)
}
tracker.gatherers.Range(func(userID, g any) bool {
for _, metric := range metrics {
tracker.registerMetric(g.(*gatherer), cfg, metric)
}
return true
})
}

func (tracker *TrackerBase[Finding]) registerMetric(gatherer *gatherer, cfg *Configuration, metric MetricName) {
Expand Down Expand Up @@ -224,20 +231,47 @@ func (tracker *TrackerBase[Finding]) track(ctx context.Context, registry metrics

// Gather the data not more often then maxAge.
func (tracker *TrackerBase[Finding]) Gather(ctx context.Context) {
id, err := authn.IdentityFromContext(ctx)
if err != nil {
return
}
cfg := tracker.GetConfiguration()
gatherer := tracker.gatherer
if cfg == nil {
return
}
gatherer := tracker.getGatherer(id.UID(), cfg)

// Return if is still running.
if !gatherer.running.CompareAndSwap(false, true) {
return
}
defer gatherer.running.Store(false)

if cfg == nil || cfg.period == 0 || time.Since(gatherer.lastGather) < cfg.period {
if cfg.period == 0 || time.Since(gatherer.lastGather) < cfg.period {
return
}
if err := tracker.track(ctx, gatherer.registry, cfg.metrics); err != nil {
log.Errorf("Failed to gather %s metrics: %v", tracker.category, err)
}
gatherer.lastGather = time.Now()
}

// getGatherer returns the existing or a new gatherer for the given userID.
// When creating a new gatherer, it also registers all known metrics on the
// gatherer registry.
func (tracker *TrackerBase[Finding]) getGatherer(userID string, cfg *Configuration) *gatherer {
// TODO(PR #16176): limit the number of different tokens accessing the metrics?
var gr *gatherer
if g, ok := tracker.gatherers.Load(userID); !ok {
gr = &gatherer{
registry: tracker.registryFactory(userID),
}
tracker.gatherers.Store(userID, gr)
for metricName := range cfg.metrics {
tracker.registerMetric(gr, cfg, metricName)
}
} else {
gr = g.(*gatherer)
}
return gr
}
29 changes: 19 additions & 10 deletions central/metrics/custom/tracker/tracker_base_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import (
"time"

"github.com/prometheus/client_golang/prometheus"
"github.com/stackrox/rox/central/metrics"
"github.com/stackrox/rox/central/metrics/mocks"
"github.com/stackrox/rox/pkg/auth/authproviders"
"github.com/stackrox/rox/pkg/errox"
"github.com/stackrox/rox/pkg/grpc/authn"
"github.com/stackrox/rox/pkg/grpc/authn/basic"
"github.com/stackrox/rox/pkg/uuid"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -49,7 +51,8 @@ func makeTestGatherFunc(data []map[Label]string) FindingGenerator[testFinding] {
}

func TestMakeTrackerBase(t *testing.T) {
tracker := MakeTrackerBase("test", "test", testLabelGetters, nilGatherFunc, nil)
tracker := MakeTrackerBase("test", "test", testLabelGetters, nilGatherFunc,
func(string) metrics.CustomRegistry { return nil })
assert.NotNil(t, tracker)
assert.Nil(t, tracker.GetConfiguration())
}
Expand All @@ -58,7 +61,9 @@ func TestTrackerBase_Reconfigure(t *testing.T) {
ctrl := gomock.NewController(t)
t.Run("nil configuration", func(t *testing.T) {
rf := mocks.NewMockCustomRegistry(ctrl)
tracker := MakeTrackerBase("test", "test", testLabelGetters, nilGatherFunc, rf)

tracker := MakeTrackerBase("test", "test", testLabelGetters, nilGatherFunc,
func(string) metrics.CustomRegistry { return rf })

tracker.Reconfigure(nil)
config := tracker.GetConfiguration()
Expand All @@ -70,8 +75,8 @@ func TestTrackerBase_Reconfigure(t *testing.T) {

t.Run("test 0 period", func(t *testing.T) {
rf := mocks.NewMockCustomRegistry(ctrl)
tracker := MakeTrackerBase("test", "test", testLabelGetters, nilGatherFunc, rf)

tracker := MakeTrackerBase("test", "test", testLabelGetters, nilGatherFunc,
func(string) metrics.CustomRegistry { return rf })
cfg0 := &Configuration{}

tracker.Reconfigure(cfg0)
Expand All @@ -87,7 +92,8 @@ func TestTrackerBase_Reconfigure(t *testing.T) {

rf := mocks.NewMockCustomRegistry(ctrl)
tracker := MakeTrackerBase("test", "test", testLabelGetters,
makeTestGatherFunc(testData), rf)
makeTestGatherFunc(testData),
func(string) metrics.CustomRegistry { return rf })

var registered, unregistered []MetricName
rf.EXPECT().RegisterMetric(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Expand Down Expand Up @@ -149,8 +155,11 @@ func TestTrackerBase_Reconfigure(t *testing.T) {
tracker.Gather(ctx)
assert.Empty(t, trackedMetricNames)

// Reset lastGather
tracker.gatherer.lastGather = time.Time{}
{ // Reset lastGather
identity, _ := authn.IdentityFromContext(ctx)
g, _ := tracker.gatherers.Load(identity.UID())
g.(*gatherer).lastGather = time.Time{}
}
tracker.Gather(ctx)

assert.ElementsMatch(t, slices.Compact(trackedMetricNames), metricNames[1:])
Expand All @@ -174,7 +183,7 @@ func TestTrackerBase_Track(t *testing.T) {
tracker := MakeTrackerBase("test", "test",
testLabelGetters,
makeTestGatherFunc(testData),
rf)
func(string) metrics.CustomRegistry { return rf })

result := make(map[string][]*aggregatedRecord)
rf.EXPECT().SetTotal(gomock.Any(), gomock.Any(), gomock.Any()).
Expand Down Expand Up @@ -244,7 +253,7 @@ func TestTrackerBase_error(t *testing.T) {
}
}
},
rf)
func(string) metrics.CustomRegistry { return rf })

tracker.config = &Configuration{
metrics: makeTestMetricDescriptors(t),
Expand All @@ -259,7 +268,7 @@ func TestTrackerBase_Gather(t *testing.T) {
tracker := MakeTrackerBase("test", "test",
testLabelGetters,
makeTestGatherFunc(testData),
rf)
func(string) metrics.CustomRegistry { return rf })

result := make(map[string][]*aggregatedRecord)
{ // Capture result with a mock registry.
Expand Down
24 changes: 18 additions & 6 deletions central/metrics/custom_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,25 @@ type customRegistry struct {
gauges sync.Map // map[metricName string]*prometheus.GaugeVec
}

// MakeCustomRegistry is a CustomRegistry factory.
func MakeCustomRegistry() CustomRegistry {
registry := prometheus.NewRegistry()
return &customRegistry{
Registry: registry,
Handler: promhttp.HandlerFor(registry, promhttp.HandlerOpts{}),
var (
userRegistries map[string]*customRegistry = make(map[string]*customRegistry)
registriesMux sync.Mutex
)

// GetCustomRegistry is a CustomRegistry factory that returns the existing or
// a new registry for the user.
func GetCustomRegistry(userID string) CustomRegistry {
registriesMux.Lock()
defer registriesMux.Unlock()
registry, ok := userRegistries[userID]
if !ok {
registry = &customRegistry{
Registry: prometheus.NewRegistry(),
}
registry.Handler = promhttp.HandlerFor(registry, promhttp.HandlerOpts{})
userRegistries[userID] = registry
}
return registry
}

var _ CustomRegistry = (*customRegistry)(nil)
Expand Down
Loading
Loading