From f8ae71af5b693aa7c68f9f8e6a8775899c65cefe Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Wed, 15 Sep 2021 11:47:44 -0400 Subject: [PATCH] Usage Stats Updates (#39235) * add randomly-generated anonymous id to usage reports * include uptime in stats * add last_sent tracking, remove metricsLogger --- pkg/infra/usagestats/service.go | 44 +++++++++-- pkg/infra/usagestats/usage_stats.go | 75 +++++++++++++------ .../usagestats/usage_stats_service_test.go | 4 + pkg/infra/usagestats/usage_stats_test.go | 18 ++++- 4 files changed, 113 insertions(+), 28 deletions(-) diff --git a/pkg/infra/usagestats/service.go b/pkg/infra/usagestats/service.go index dad282b9702..2e07741e437 100644 --- a/pkg/infra/usagestats/service.go +++ b/pkg/infra/usagestats/service.go @@ -6,6 +6,7 @@ import ( "time" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/plugins" @@ -15,8 +16,6 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -var metricsLogger log.Logger = log.New("metrics") - type UsageStats interface { GetUsageReport(context.Context) (UsageReport, error) RegisterMetricsFunc(MetricsFunc) @@ -33,6 +32,7 @@ type UsageStatsService struct { PluginManager plugins.Manager SocialService social.Service grafanaLive *live.GrafanaLive + kvStore *kvstore.NamespacedKVStore log log.Logger @@ -40,6 +40,7 @@ type UsageStatsService struct { externalMetrics []MetricsFunc concurrentUserStatsCache memoConcurrentUserStats liveStats liveUsageStats + startTime time.Time } type liveUsageStats struct { @@ -54,7 +55,8 @@ type liveUsageStats struct { func ProvideService(cfg *setting.Cfg, bus bus.Bus, sqlStore *sqlstore.SQLStore, alertingStats alerting.UsageStatsQuerier, pluginManager plugins.Manager, - socialService social.Service, grafanaLive *live.GrafanaLive) *UsageStatsService { + socialService social.Service, grafanaLive *live.GrafanaLive, + kvStore kvstore.KVStore) *UsageStatsService { s := &UsageStatsService{ Cfg: cfg, Bus: bus, @@ -63,7 +65,9 @@ func ProvideService(cfg *setting.Cfg, bus bus.Bus, sqlStore *sqlstore.SQLStore, oauthProviders: socialService.GetOAuthProviders(), PluginManager: pluginManager, grafanaLive: grafanaLive, + kvStore: kvstore.WithNamespace(kvStore, 0, "infra.usagestats"), log: log.New("infra.usagestats"), + startTime: time.Now(), } return s } @@ -71,7 +75,26 @@ func ProvideService(cfg *setting.Cfg, bus bus.Bus, sqlStore *sqlstore.SQLStore, func (uss *UsageStatsService) Run(ctx context.Context) error { uss.updateTotalStats() - sendReportTicker := time.NewTicker(time.Hour * 24) + // try to load last sent time from kv store + lastSent := time.Now() + if val, ok, err := uss.kvStore.Get(ctx, "last_sent"); err != nil { + uss.log.Error("Failed to get last sent time", "error", err) + } else if ok { + if parsed, err := time.Parse(time.RFC3339, val); err != nil { + uss.log.Error("Failed to parse last sent time", "error", err) + } else { + lastSent = parsed + } + } + + // calculate initial send delay + sendInterval := time.Hour * 24 + nextSendInterval := time.Until(lastSent.Add(sendInterval)) + if nextSendInterval < time.Minute { + nextSendInterval = time.Minute + } + + sendReportTicker := time.NewTicker(nextSendInterval) updateStatsTicker := time.NewTicker(time.Minute * 30) defer sendReportTicker.Stop() @@ -81,8 +104,19 @@ func (uss *UsageStatsService) Run(ctx context.Context) error { select { case <-sendReportTicker.C: if err := uss.sendUsageStats(ctx); err != nil { - metricsLogger.Warn("Failed to send usage stats", "err", err) + uss.log.Warn("Failed to send usage stats", "error", err) } + + lastSent = time.Now() + if err := uss.kvStore.Set(ctx, "last_sent", lastSent.Format(time.RFC3339)); err != nil { + uss.log.Warn("Failed to update last sent time", "error", err) + } + + if nextSendInterval != sendInterval { + nextSendInterval = sendInterval + sendReportTicker.Reset(nextSendInterval) + } + // always reset live stats every report tick uss.resetLiveStats() case <-updateStatsTicker.C: diff --git a/pkg/infra/usagestats/usage_stats.go b/pkg/infra/usagestats/usage_stats.go index 266e4b20a23..87115fc352f 100644 --- a/pkg/infra/usagestats/usage_stats.go +++ b/pkg/infra/usagestats/usage_stats.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "github.com/google/uuid" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/models" ) @@ -24,6 +25,7 @@ type UsageReport struct { Edition string `json:"edition"` HasValidLicense bool `json:"hasValidLicense"` Packaging string `json:"packaging"` + UsageStatsId string `json:"usageStatsId"` } func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport, error) { @@ -36,17 +38,18 @@ func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport, edition = "enterprise" } report := UsageReport{ - Version: version, - Metrics: metrics, - Os: runtime.GOOS, - Arch: runtime.GOARCH, - Edition: edition, - Packaging: uss.Cfg.Packaging, + Version: version, + Metrics: metrics, + Os: runtime.GOOS, + Arch: runtime.GOARCH, + Edition: edition, + Packaging: uss.Cfg.Packaging, + UsageStatsId: uss.GetUsageStatsId(ctx), } statsQuery := models.GetSystemStatsQuery{} if err := uss.Bus.Dispatch(&statsQuery); err != nil { - metricsLogger.Error("Failed to get system stats", "error", err) + uss.log.Error("Failed to get system stats", "error", err) return report, err } @@ -132,7 +135,7 @@ func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport, dsStats := models.GetDataSourceStatsQuery{} if err := uss.Bus.Dispatch(&dsStats); err != nil { - metricsLogger.Error("Failed to get datasource stats", "error", err) + uss.log.Error("Failed to get datasource stats", "error", err) return report, err } @@ -151,7 +154,7 @@ func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport, esDataSourcesQuery := models.GetDataSourcesByTypeQuery{Type: models.DS_ES} if err := uss.Bus.Dispatch(&esDataSourcesQuery); err != nil { - metricsLogger.Error("Failed to get elasticsearch json data", "error", err) + uss.log.Error("Failed to get elasticsearch json data", "error", err) return report, err } @@ -196,7 +199,7 @@ func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport, // fetch datasource access stats dsAccessStats := models.GetDataSourceAccessStatsQuery{} if err := uss.Bus.Dispatch(&dsAccessStats); err != nil { - metricsLogger.Error("Failed to get datasource access stats", "error", err) + uss.log.Error("Failed to get datasource access stats", "error", err) return report, err } @@ -225,8 +228,8 @@ func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport, // get stats about alert notifier usage anStats := models.GetAlertNotifierUsageStatsQuery{} - if err := uss.Bus.Dispatch(&anStats); err != nil { - metricsLogger.Error("Failed to get alert notification stats", "error", err) + if err := uss.Bus.DispatchCtx(ctx, &anStats); err != nil { + uss.log.Error("Failed to get alert notification stats", "error", err) return report, err } @@ -256,7 +259,7 @@ func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport, // Get concurrent users stats as histogram concurrentUsersStats, err := uss.GetConcurrentUsersStats(ctx) if err != nil { - metricsLogger.Error("Failed to get concurrent users stats", "error", err) + uss.log.Error("Failed to get concurrent users stats", "error", err) return report, err } @@ -268,6 +271,8 @@ func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport, metrics["stats.auth_token_per_user_le_15"] = concurrentUsersStats.BucketLE15 metrics["stats.auth_token_per_user_le_inf"] = concurrentUsersStats.BucketLEInf + metrics["stats.uptime"] = int64(time.Since(uss.startTime).Seconds()) + return report, nil } @@ -275,7 +280,7 @@ func (uss *UsageStatsService) registerExternalMetrics(metrics map[string]interfa for _, fn := range uss.externalMetrics { fnMetrics, err := fn() if err != nil { - metricsLogger.Error("Failed to fetch external metrics", "error", err) + uss.log.Error("Failed to fetch external metrics", "error", err) continue } @@ -294,7 +299,7 @@ func (uss *UsageStatsService) sendUsageStats(ctx context.Context) error { return nil } - metricsLogger.Debug(fmt.Sprintf("Sending anonymous usage stats to %s", usageStatsURL)) + uss.log.Debug(fmt.Sprintf("Sending anonymous usage stats to %s", usageStatsURL)) report, err := uss.GetUsageReport(ctx) if err != nil { @@ -307,23 +312,23 @@ func (uss *UsageStatsService) sendUsageStats(ctx context.Context) error { } data := bytes.NewBuffer(out) - sendUsageStats(data) + sendUsageStats(uss, data) return nil } // sendUsageStats sends usage statistics. // // Stubbable by tests. -var sendUsageStats = func(data *bytes.Buffer) { +var sendUsageStats = func(uss *UsageStatsService, data *bytes.Buffer) { go func() { client := http.Client{Timeout: 5 * time.Second} resp, err := client.Post(usageStatsURL, "application/json", data) if err != nil { - metricsLogger.Error("Failed to send usage stats", "err", err) + uss.log.Error("Failed to send usage stats", "err", err) return } if err := resp.Body.Close(); err != nil { - metricsLogger.Warn("Failed to close response body", "err", err) + uss.log.Warn("Failed to close response body", "err", err) } }() } @@ -363,7 +368,7 @@ func (uss *UsageStatsService) updateTotalStats() { statsQuery := models.GetSystemStatsQuery{} if err := uss.Bus.Dispatch(&statsQuery); err != nil { - metricsLogger.Error("Failed to get system stats", "error", err) + uss.log.Error("Failed to get system stats", "error", err) return } @@ -387,7 +392,7 @@ func (uss *UsageStatsService) updateTotalStats() { dsStats := models.GetDataSourceStatsQuery{} if err := uss.Bus.Dispatch(&dsStats); err != nil { - metricsLogger.Error("Failed to get datasource stats", "error", err) + uss.log.Error("Failed to get datasource stats", "error", err) return } @@ -404,3 +409,31 @@ func (uss *UsageStatsService) ShouldBeReported(dsType string) bool { return ds.Signature.IsValid() || ds.Signature.IsInternal() } + +func (uss *UsageStatsService) GetUsageStatsId(ctx context.Context) string { + anonId, ok, err := uss.kvStore.Get(ctx, "anonymous_id") + if err != nil { + uss.log.Error("Failed to get usage stats id", "error", err) + return "" + } + + if ok { + return anonId + } + + newId, err := uuid.NewRandom() + if err != nil { + uss.log.Error("Failed to generate usage stats id", "error", err) + return "" + } + + anonId = newId.String() + + err = uss.kvStore.Set(ctx, "anonymous_id", anonId) + if err != nil { + uss.log.Error("Failed to store usage stats id", "error", err) + return "" + } + + return anonId +} diff --git a/pkg/infra/usagestats/usage_stats_service_test.go b/pkg/infra/usagestats/usage_stats_service_test.go index 932ac27ade9..ba226d9c064 100644 --- a/pkg/infra/usagestats/usage_stats_service_test.go +++ b/pkg/infra/usagestats/usage_stats_service_test.go @@ -9,6 +9,8 @@ import ( "time" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/infra/kvstore" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/util" "github.com/stretchr/testify/assert" @@ -20,6 +22,8 @@ func TestUsageStatsService_GetConcurrentUsersStats(t *testing.T) { uss := &UsageStatsService{ Bus: bus.New(), SQLStore: sqlStore, + kvStore: kvstore.WithNamespace(kvstore.ProvideService(sqlStore), 0, "infra.usagestats"), + log: log.New("infra.usagestats"), } createConcurrentTokens(t, sqlStore) diff --git a/pkg/infra/usagestats/usage_stats_test.go b/pkg/infra/usagestats/usage_stats_test.go index d8a950b9bea..7b6ca4a72f1 100644 --- a/pkg/infra/usagestats/usage_stats_test.go +++ b/pkg/infra/usagestats/usage_stats_test.go @@ -14,6 +14,8 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/infra/kvstore" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/manager" @@ -219,7 +221,7 @@ func TestMetrics(t *testing.T) { sendUsageStats = origSendUsageStats }) statsSent := false - sendUsageStats = func(*bytes.Buffer) { + sendUsageStats = func(uss *UsageStatsService, b *bytes.Buffer) { statsSent = true } @@ -307,6 +309,10 @@ func TestMetrics(t *testing.T) { assert.Equal(t, runtime.GOOS, j.Get("os").MustString()) assert.Equal(t, runtime.GOARCH, j.Get("arch").MustString()) + usageId := uss.GetUsageStatsId(context.Background()) + assert.NotEmpty(t, usageId) + assert.Equal(t, usageId, j.Get("usageStatsId").MustString()) + metrics := j.Get("metrics") assert.Equal(t, getSystemStatsQuery.Result.Dashboards, metrics.Get("stats.dashboards.count").MustInt64()) assert.Equal(t, getSystemStatsQuery.Result.Users, metrics.Get("stats.users.count").MustInt64()) @@ -393,6 +399,9 @@ func TestMetrics(t *testing.T) { assert.Equal(t, 4, metrics.Get("stats.auth_token_per_user_le_12").MustInt()) assert.Equal(t, 5, metrics.Get("stats.auth_token_per_user_le_15").MustInt()) assert.Equal(t, 6, metrics.Get("stats.auth_token_per_user_le_inf").MustInt()) + + assert.LessOrEqual(t, 60, metrics.Get("stats.uptime").MustInt()) + assert.Greater(t, 70, metrics.Get("stats.uptime").MustInt()) }) }) @@ -629,14 +638,19 @@ type httpResp struct { func createService(t *testing.T, cfg setting.Cfg) *UsageStatsService { t.Helper() + sqlStore := sqlstore.InitTestDB(t) + return &UsageStatsService{ Bus: bus.New(), Cfg: &cfg, - SQLStore: sqlstore.InitTestDB(t), + SQLStore: sqlStore, AlertingUsageStats: &alertingUsageMock{}, externalMetrics: make([]MetricsFunc, 0), PluginManager: &fakePluginManager{}, grafanaLive: newTestLive(t), + kvStore: kvstore.WithNamespace(kvstore.ProvideService(sqlStore), 0, "infra.usagestats"), + log: log.New("infra.usagestats"), + startTime: time.Now().Add(-1 * time.Minute), } }