grafana/pkg/infra/usagestats/service.go
Vardan Torosyan 7ff37bc6bb
Usage Stats: Calculate concurrent users as a histogram (#30006)
Usage Stats: Calculate concurrent users as a histogram

Metric will help to understand if users are sharing their credentials or using one account for concurrent sessions. This will help to make more informed decisions when enforcing any license limitations on credentials usage.
2021-01-06 16:57:31 +01:00

115 lines
3.3 KiB
Go

package usagestats
import (
"context"
"fmt"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/setting"
)
var metricsLogger log.Logger = log.New("metrics")
func init() {
registry.RegisterService(&UsageStatsService{})
}
type UsageStats interface {
GetUsageReport(ctx context.Context) (UsageReport, error)
RegisterMetric(name string, fn MetricFunc)
}
type MetricFunc func() (interface{}, error)
type UsageStatsService struct {
Cfg *setting.Cfg `inject:""`
Bus bus.Bus `inject:""`
SQLStore *sqlstore.SQLStore `inject:""`
AlertingUsageStats alerting.UsageStatsQuerier `inject:""`
License models.Licensing `inject:""`
log log.Logger
oauthProviders map[string]bool
externalMetrics map[string]MetricFunc
concurrentUserStatsCache memoConcurrentUserStats
}
func (uss *UsageStatsService) Init() error {
uss.log = log.New("infra.usagestats")
uss.oauthProviders = social.GetOAuthProviders(uss.Cfg)
uss.externalMetrics = make(map[string]MetricFunc)
return nil
}
func (uss *UsageStatsService) Run(ctx context.Context) error {
uss.updateTotalStats()
onceEveryDayTick := time.NewTicker(time.Hour * 24)
everyMinuteTicker := time.NewTicker(time.Minute)
defer onceEveryDayTick.Stop()
defer everyMinuteTicker.Stop()
for {
select {
case <-onceEveryDayTick.C:
if err := uss.sendUsageStats(ctx); err != nil {
metricsLogger.Warn("Failed to send usage stats", "err", err)
}
case <-everyMinuteTicker.C:
uss.updateTotalStats()
case <-ctx.Done():
return ctx.Err()
}
}
}
type memoConcurrentUserStats struct {
stats *concurrentUsersStats
memoized time.Time
}
const concurrentUserStatsCacheLifetime = time.Hour
func (uss *UsageStatsService) GetConcurrentUsersStats(ctx context.Context) (*concurrentUsersStats, error) {
memoizationPeriod := time.Now().Add(-concurrentUserStatsCacheLifetime)
if !uss.concurrentUserStatsCache.memoized.Before(memoizationPeriod) {
return uss.concurrentUserStatsCache.stats, nil
}
uss.concurrentUserStatsCache.stats = &concurrentUsersStats{}
err := uss.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
// Retrieves concurrent users stats as a histogram. Buckets are accumulative and upper bound is inclusive.
rawSQL := `
SELECT
COUNT(CASE WHEN tokens <= 3 THEN 1 END) AS bucket_le_3,
COUNT(CASE WHEN tokens <= 6 THEN 1 END) AS bucket_le_6,
COUNT(CASE WHEN tokens <= 9 THEN 1 END) AS bucket_le_9,
COUNT(CASE WHEN tokens <= 12 THEN 1 END) AS bucket_le_12,
COUNT(CASE WHEN tokens <= 15 THEN 1 END) AS bucket_le_15,
COUNT(1) AS bucket_le_inf
FROM (select count(1) as tokens from user_auth_token group by user_id) uat;`
_, err := sess.SQL(rawSQL).Get(uss.concurrentUserStatsCache.stats)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to get concurrent users stats from database: %w", err)
}
uss.concurrentUserStatsCache.memoized = time.Now()
return uss.concurrentUserStatsCache.stats, nil
}