Usage Stats Updates (#39235)

* add randomly-generated anonymous id to usage reports
* include uptime in stats
* add last_sent tracking, remove metricsLogger
This commit is contained in:
Dan Cech 2021-09-15 11:47:44 -04:00 committed by GitHub
parent 74beb9a64c
commit f8ae71af5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 113 additions and 28 deletions

View File

@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/grafana/grafana/pkg/bus" "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/infra/log"
"github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
@ -15,8 +16,6 @@ import (
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
var metricsLogger log.Logger = log.New("metrics")
type UsageStats interface { type UsageStats interface {
GetUsageReport(context.Context) (UsageReport, error) GetUsageReport(context.Context) (UsageReport, error)
RegisterMetricsFunc(MetricsFunc) RegisterMetricsFunc(MetricsFunc)
@ -33,6 +32,7 @@ type UsageStatsService struct {
PluginManager plugins.Manager PluginManager plugins.Manager
SocialService social.Service SocialService social.Service
grafanaLive *live.GrafanaLive grafanaLive *live.GrafanaLive
kvStore *kvstore.NamespacedKVStore
log log.Logger log log.Logger
@ -40,6 +40,7 @@ type UsageStatsService struct {
externalMetrics []MetricsFunc externalMetrics []MetricsFunc
concurrentUserStatsCache memoConcurrentUserStats concurrentUserStatsCache memoConcurrentUserStats
liveStats liveUsageStats liveStats liveUsageStats
startTime time.Time
} }
type liveUsageStats struct { type liveUsageStats struct {
@ -54,7 +55,8 @@ type liveUsageStats struct {
func ProvideService(cfg *setting.Cfg, bus bus.Bus, sqlStore *sqlstore.SQLStore, func ProvideService(cfg *setting.Cfg, bus bus.Bus, sqlStore *sqlstore.SQLStore,
alertingStats alerting.UsageStatsQuerier, pluginManager plugins.Manager, 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{ s := &UsageStatsService{
Cfg: cfg, Cfg: cfg,
Bus: bus, Bus: bus,
@ -63,7 +65,9 @@ func ProvideService(cfg *setting.Cfg, bus bus.Bus, sqlStore *sqlstore.SQLStore,
oauthProviders: socialService.GetOAuthProviders(), oauthProviders: socialService.GetOAuthProviders(),
PluginManager: pluginManager, PluginManager: pluginManager,
grafanaLive: grafanaLive, grafanaLive: grafanaLive,
kvStore: kvstore.WithNamespace(kvStore, 0, "infra.usagestats"),
log: log.New("infra.usagestats"), log: log.New("infra.usagestats"),
startTime: time.Now(),
} }
return s 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 { func (uss *UsageStatsService) Run(ctx context.Context) error {
uss.updateTotalStats() 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) updateStatsTicker := time.NewTicker(time.Minute * 30)
defer sendReportTicker.Stop() defer sendReportTicker.Stop()
@ -81,8 +104,19 @@ func (uss *UsageStatsService) Run(ctx context.Context) error {
select { select {
case <-sendReportTicker.C: case <-sendReportTicker.C:
if err := uss.sendUsageStats(ctx); err != nil { 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 // always reset live stats every report tick
uss.resetLiveStats() uss.resetLiveStats()
case <-updateStatsTicker.C: case <-updateStatsTicker.C:

View File

@ -10,6 +10,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/google/uuid"
"github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
) )
@ -24,6 +25,7 @@ type UsageReport struct {
Edition string `json:"edition"` Edition string `json:"edition"`
HasValidLicense bool `json:"hasValidLicense"` HasValidLicense bool `json:"hasValidLicense"`
Packaging string `json:"packaging"` Packaging string `json:"packaging"`
UsageStatsId string `json:"usageStatsId"`
} }
func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport, error) { func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport, error) {
@ -42,11 +44,12 @@ func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport,
Arch: runtime.GOARCH, Arch: runtime.GOARCH,
Edition: edition, Edition: edition,
Packaging: uss.Cfg.Packaging, Packaging: uss.Cfg.Packaging,
UsageStatsId: uss.GetUsageStatsId(ctx),
} }
statsQuery := models.GetSystemStatsQuery{} statsQuery := models.GetSystemStatsQuery{}
if err := uss.Bus.Dispatch(&statsQuery); err != nil { 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 return report, err
} }
@ -132,7 +135,7 @@ func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport,
dsStats := models.GetDataSourceStatsQuery{} dsStats := models.GetDataSourceStatsQuery{}
if err := uss.Bus.Dispatch(&dsStats); err != nil { 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 return report, err
} }
@ -151,7 +154,7 @@ func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport,
esDataSourcesQuery := models.GetDataSourcesByTypeQuery{Type: models.DS_ES} esDataSourcesQuery := models.GetDataSourcesByTypeQuery{Type: models.DS_ES}
if err := uss.Bus.Dispatch(&esDataSourcesQuery); err != nil { 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 return report, err
} }
@ -196,7 +199,7 @@ func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport,
// fetch datasource access stats // fetch datasource access stats
dsAccessStats := models.GetDataSourceAccessStatsQuery{} dsAccessStats := models.GetDataSourceAccessStatsQuery{}
if err := uss.Bus.Dispatch(&dsAccessStats); err != nil { 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 return report, err
} }
@ -225,8 +228,8 @@ func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport,
// get stats about alert notifier usage // get stats about alert notifier usage
anStats := models.GetAlertNotifierUsageStatsQuery{} anStats := models.GetAlertNotifierUsageStatsQuery{}
if err := uss.Bus.Dispatch(&anStats); err != nil { if err := uss.Bus.DispatchCtx(ctx, &anStats); err != nil {
metricsLogger.Error("Failed to get alert notification stats", "error", err) uss.log.Error("Failed to get alert notification stats", "error", err)
return report, err return report, err
} }
@ -256,7 +259,7 @@ func (uss *UsageStatsService) GetUsageReport(ctx context.Context) (UsageReport,
// Get concurrent users stats as histogram // Get concurrent users stats as histogram
concurrentUsersStats, err := uss.GetConcurrentUsersStats(ctx) concurrentUsersStats, err := uss.GetConcurrentUsersStats(ctx)
if err != nil { 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 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_15"] = concurrentUsersStats.BucketLE15
metrics["stats.auth_token_per_user_le_inf"] = concurrentUsersStats.BucketLEInf metrics["stats.auth_token_per_user_le_inf"] = concurrentUsersStats.BucketLEInf
metrics["stats.uptime"] = int64(time.Since(uss.startTime).Seconds())
return report, nil return report, nil
} }
@ -275,7 +280,7 @@ func (uss *UsageStatsService) registerExternalMetrics(metrics map[string]interfa
for _, fn := range uss.externalMetrics { for _, fn := range uss.externalMetrics {
fnMetrics, err := fn() fnMetrics, err := fn()
if err != nil { if err != nil {
metricsLogger.Error("Failed to fetch external metrics", "error", err) uss.log.Error("Failed to fetch external metrics", "error", err)
continue continue
} }
@ -294,7 +299,7 @@ func (uss *UsageStatsService) sendUsageStats(ctx context.Context) error {
return nil 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) report, err := uss.GetUsageReport(ctx)
if err != nil { if err != nil {
@ -307,23 +312,23 @@ func (uss *UsageStatsService) sendUsageStats(ctx context.Context) error {
} }
data := bytes.NewBuffer(out) data := bytes.NewBuffer(out)
sendUsageStats(data) sendUsageStats(uss, data)
return nil return nil
} }
// sendUsageStats sends usage statistics. // sendUsageStats sends usage statistics.
// //
// Stubbable by tests. // Stubbable by tests.
var sendUsageStats = func(data *bytes.Buffer) { var sendUsageStats = func(uss *UsageStatsService, data *bytes.Buffer) {
go func() { go func() {
client := http.Client{Timeout: 5 * time.Second} client := http.Client{Timeout: 5 * time.Second}
resp, err := client.Post(usageStatsURL, "application/json", data) resp, err := client.Post(usageStatsURL, "application/json", data)
if err != nil { if err != nil {
metricsLogger.Error("Failed to send usage stats", "err", err) uss.log.Error("Failed to send usage stats", "err", err)
return return
} }
if err := resp.Body.Close(); err != nil { 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{} statsQuery := models.GetSystemStatsQuery{}
if err := uss.Bus.Dispatch(&statsQuery); err != nil { 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 return
} }
@ -387,7 +392,7 @@ func (uss *UsageStatsService) updateTotalStats() {
dsStats := models.GetDataSourceStatsQuery{} dsStats := models.GetDataSourceStatsQuery{}
if err := uss.Bus.Dispatch(&dsStats); err != nil { 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 return
} }
@ -404,3 +409,31 @@ func (uss *UsageStatsService) ShouldBeReported(dsType string) bool {
return ds.Signature.IsValid() || ds.Signature.IsInternal() 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
}

View File

@ -9,6 +9,8 @@ import (
"time" "time"
"github.com/grafana/grafana/pkg/bus" "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/services/sqlstore"
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -20,6 +22,8 @@ func TestUsageStatsService_GetConcurrentUsersStats(t *testing.T) {
uss := &UsageStatsService{ uss := &UsageStatsService{
Bus: bus.New(), Bus: bus.New(),
SQLStore: sqlStore, SQLStore: sqlStore,
kvStore: kvstore.WithNamespace(kvstore.ProvideService(sqlStore), 0, "infra.usagestats"),
log: log.New("infra.usagestats"),
} }
createConcurrentTokens(t, sqlStore) createConcurrentTokens(t, sqlStore)

View File

@ -14,6 +14,8 @@ import (
"github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson" "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/models"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/manager" "github.com/grafana/grafana/pkg/plugins/manager"
@ -219,7 +221,7 @@ func TestMetrics(t *testing.T) {
sendUsageStats = origSendUsageStats sendUsageStats = origSendUsageStats
}) })
statsSent := false statsSent := false
sendUsageStats = func(*bytes.Buffer) { sendUsageStats = func(uss *UsageStatsService, b *bytes.Buffer) {
statsSent = true statsSent = true
} }
@ -307,6 +309,10 @@ func TestMetrics(t *testing.T) {
assert.Equal(t, runtime.GOOS, j.Get("os").MustString()) assert.Equal(t, runtime.GOOS, j.Get("os").MustString())
assert.Equal(t, runtime.GOARCH, j.Get("arch").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") metrics := j.Get("metrics")
assert.Equal(t, getSystemStatsQuery.Result.Dashboards, metrics.Get("stats.dashboards.count").MustInt64()) assert.Equal(t, getSystemStatsQuery.Result.Dashboards, metrics.Get("stats.dashboards.count").MustInt64())
assert.Equal(t, getSystemStatsQuery.Result.Users, metrics.Get("stats.users.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, 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, 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.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 { func createService(t *testing.T, cfg setting.Cfg) *UsageStatsService {
t.Helper() t.Helper()
sqlStore := sqlstore.InitTestDB(t)
return &UsageStatsService{ return &UsageStatsService{
Bus: bus.New(), Bus: bus.New(),
Cfg: &cfg, Cfg: &cfg,
SQLStore: sqlstore.InitTestDB(t), SQLStore: sqlStore,
AlertingUsageStats: &alertingUsageMock{}, AlertingUsageStats: &alertingUsageMock{},
externalMetrics: make([]MetricsFunc, 0), externalMetrics: make([]MetricsFunc, 0),
PluginManager: &fakePluginManager{}, PluginManager: &fakePluginManager{},
grafanaLive: newTestLive(t), grafanaLive: newTestLive(t),
kvStore: kvstore.WithNamespace(kvstore.ProvideService(sqlStore), 0, "infra.usagestats"),
log: log.New("infra.usagestats"),
startTime: time.Now().Add(-1 * time.Minute),
} }
} }