Usage stats: Fix concurrency issue (#84993)

* Usage stats: Fix concurrency issue
This commit is contained in:
Sofia Papagiannaki 2024-03-22 15:52:59 +02:00 committed by GitHub
parent 93ca49b785
commit 369cb5b6eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 47 additions and 19 deletions

View File

@ -29,16 +29,16 @@ var usageStatsURL = "https://stats.grafana.org/grafana-usage-report"
func (uss *UsageStats) GetUsageReport(ctx context.Context) (usagestats.Report, error) { func (uss *UsageStats) GetUsageReport(ctx context.Context) (usagestats.Report, error) {
version := strings.ReplaceAll(uss.Cfg.BuildVersion, ".", "_") version := strings.ReplaceAll(uss.Cfg.BuildVersion, ".", "_")
metrics := map[string]any{} metrics := sync.Map{}
start := time.Now() start := time.Now()
edition := "oss" edition := "oss"
if uss.Cfg.IsEnterprise { if uss.Cfg.IsEnterprise {
edition = "enterprise" edition = "enterprise"
} }
report := usagestats.Report{ report := usagestats.Report{
Version: version, Version: version,
Metrics: metrics,
Os: runtime.GOOS, Os: runtime.GOOS,
Arch: runtime.GOARCH, Arch: runtime.GOARCH,
Edition: edition, Edition: edition,
@ -46,20 +46,28 @@ func (uss *UsageStats) GetUsageReport(ctx context.Context) (usagestats.Report, e
UsageStatsId: uss.GetUsageStatsId(ctx), UsageStatsId: uss.GetUsageStatsId(ctx),
} }
uss.gatherMetrics(ctx, metrics) uss.gatherMetrics(ctx, &metrics)
// must run after registration of external metrics // must run after registration of external metrics
if v, ok := metrics["stats.valid_license.count"]; ok { if v, ok := metrics.Load("stats.valid_license.count"); ok {
report.HasValidLicense = v == 1 report.HasValidLicense = v == 1
} else { } else {
metrics["stats.valid_license.count"] = 0 metrics.Store("stats.valid_license.count", 0)
} }
uss.log.FromContext(ctx).Debug("Collected usage stats", "metricCount", len(metrics), "version", report.Version, "os", report.Os, "arch", report.Arch, "edition", report.Edition, "duration", time.Since(start)) report.Metrics = make(map[string]any)
metricCount := 0
metrics.Range(func(key, value any) bool {
report.Metrics[key.(string)] = value
metricCount++
return true
})
uss.log.FromContext(ctx).Debug("Collected usage stats", "metricCount", metricCount, "version", report.Version, "os", report.Os, "arch", report.Arch, "edition", report.Edition, "duration", time.Since(start))
return report, nil return report, nil
} }
func (uss *UsageStats) gatherMetrics(ctx context.Context, metrics map[string]any) { func (uss *UsageStats) gatherMetrics(ctx context.Context, metrics *sync.Map) {
ctxTracer, span := uss.tracer.Start(ctx, "UsageStats.GatherLoop") ctxTracer, span := uss.tracer.Start(ctx, "UsageStats.GatherLoop")
defer span.End() defer span.End()
totC, errC := 0, 0 totC, errC := 0, 0
@ -86,14 +94,14 @@ func (uss *UsageStats) gatherMetrics(ctx context.Context, metrics map[string]any
} }
for name, value := range fnMetrics { for name, value := range fnMetrics {
metrics[name] = value metrics.Store(name, value)
} }
}(fn) }(fn)
} }
wg.Wait() wg.Wait()
metrics["stats.usagestats.debug.collect.total.count"] = totC metrics.Store("stats.usagestats.debug.collect.total.count", totC)
metrics["stats.usagestats.debug.collect.error.count"] = errC metrics.Store("stats.usagestats.debug.collect.error.count", errC)
} }
func (uss *UsageStats) runMetricsFunc(ctx context.Context, fn usagestats.MetricsFunc) (map[string]any, error) { func (uss *UsageStats) runMetricsFunc(ctx context.Context, fn usagestats.MetricsFunc) (map[string]any, error) {

View File

@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"runtime" "runtime"
"sync"
"testing" "testing"
"time" "time"
@ -175,7 +176,9 @@ func TestRegisterMetrics(t *testing.T) {
sqlStore := dbtest.NewFakeDB() sqlStore := dbtest.NewFakeDB()
uss := createService(t, sqlStore, false) uss := createService(t, sqlStore, false)
metrics := map[string]any{"stats.test_metric.count": 1, "stats.test_metric_second.count": 2} metrics := sync.Map{}
metrics.Store("stats.test_metric.count", 1)
metrics.Store("stats.test_metric_second.count", 2)
uss.RegisterMetricsFunc(func(context.Context) (map[string]any, error) { uss.RegisterMetricsFunc(func(context.Context) (map[string]any, error) {
return map[string]any{goodMetricName: 1}, nil return map[string]any{goodMetricName: 1}, nil
@ -187,9 +190,15 @@ func TestRegisterMetrics(t *testing.T) {
assert.Equal(t, map[string]any{goodMetricName: 1}, extMetrics) assert.Equal(t, map[string]any{goodMetricName: 1}, extMetrics)
} }
uss.gatherMetrics(context.Background(), metrics) uss.gatherMetrics(context.Background(), &metrics)
assert.Equal(t, 1, metrics[goodMetricName]) v, ok := metrics.Load(goodMetricName)
metricsCount := len(metrics) assert.True(t, ok)
assert.Equal(t, 1, v)
metricsCountBefore := 0
metrics.Range(func(_, _ any) bool {
metricsCountBefore++
return true
})
t.Run("do not add metrics that return an error when fetched", func(t *testing.T) { t.Run("do not add metrics that return an error when fetched", func(t *testing.T) {
const badMetricName = "stats.test_external_metric_error.count" const badMetricName = "stats.test_external_metric_error.count"
@ -197,15 +206,26 @@ func TestRegisterMetrics(t *testing.T) {
uss.RegisterMetricsFunc(func(context.Context) (map[string]any, error) { uss.RegisterMetricsFunc(func(context.Context) (map[string]any, error) {
return map[string]any{badMetricName: 1}, errors.New("some error") return map[string]any{badMetricName: 1}, errors.New("some error")
}) })
uss.gatherMetrics(context.Background(), metrics) uss.gatherMetrics(context.Background(), &metrics)
extErrorMetric := metrics[badMetricName] extErrorMetric, ok := metrics.Load(badMetricName)
extMetric := metrics[goodMetricName] assert.False(t, ok)
extMetric, ok := metrics.Load(goodMetricName)
assert.True(t, ok)
require.Nil(t, extErrorMetric, "Invalid metric should not be added") require.Nil(t, extErrorMetric, "Invalid metric should not be added")
assert.Equal(t, 1, extMetric) assert.Equal(t, 1, extMetric)
assert.Len(t, metrics, metricsCount, "Expected same number of metrics before and after collecting bad metric")
assert.EqualValues(t, 1, metrics["stats.usagestats.debug.collect.error.count"]) metricsCountAfter := 0
metrics.Range(func(_, _ any) bool {
metricsCountAfter++
return true
})
assert.Equal(t, metricsCountAfter, metricsCountBefore, "Expected same number of metrics before and after collecting bad metric")
errCount, ok := metrics.Load("stats.usagestats.debug.collect.error.count")
assert.True(t, ok)
assert.EqualValues(t, 1, errCount)
}) })
} }