mirror of
https://github.com/grafana/grafana.git
synced 2024-11-30 12:44:10 -06:00
366 lines
13 KiB
Go
366 lines
13 KiB
Go
package statscollector
|
|
|
|
import (
|
|
"context"
|
|
"math/rand"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
|
|
"github.com/grafana/grafana/pkg/infra/db"
|
|
"github.com/grafana/grafana/pkg/infra/httpclient"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/infra/metrics"
|
|
"github.com/grafana/grafana/pkg/infra/usagestats"
|
|
"github.com/grafana/grafana/pkg/infra/usagestats/validator"
|
|
"github.com/grafana/grafana/pkg/login/social"
|
|
"github.com/grafana/grafana/pkg/plugins"
|
|
"github.com/grafana/grafana/pkg/registry"
|
|
"github.com/grafana/grafana/pkg/services/datasources"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/stats"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
)
|
|
|
|
const (
|
|
MIN_DELAY = 30
|
|
MAX_DELAY = 120
|
|
)
|
|
|
|
type Service struct {
|
|
cfg *setting.Cfg
|
|
sqlstore db.DB
|
|
plugins plugins.Store
|
|
usageStats usagestats.Service
|
|
validator validator.Service
|
|
statsService stats.Service
|
|
features *featuremgmt.FeatureManager
|
|
datasources datasources.DataSourceService
|
|
httpClientProvider httpclient.Provider
|
|
|
|
log log.Logger
|
|
|
|
startTime time.Time
|
|
concurrentUserStatsCache memoConcurrentUserStats
|
|
promFlavorCache memoPrometheusFlavor
|
|
usageStatProviders []registry.ProvidesUsageStats
|
|
}
|
|
|
|
func ProvideService(
|
|
us usagestats.Service,
|
|
validator validator.Service,
|
|
statsService stats.Service,
|
|
cfg *setting.Cfg,
|
|
store db.DB,
|
|
social social.Service,
|
|
plugins plugins.Store,
|
|
features *featuremgmt.FeatureManager,
|
|
datasourceService datasources.DataSourceService,
|
|
httpClientProvider httpclient.Provider,
|
|
) *Service {
|
|
s := &Service{
|
|
cfg: cfg,
|
|
sqlstore: store,
|
|
plugins: plugins,
|
|
usageStats: us,
|
|
validator: validator,
|
|
statsService: statsService,
|
|
features: features,
|
|
datasources: datasourceService,
|
|
httpClientProvider: httpClientProvider,
|
|
|
|
startTime: time.Now(),
|
|
log: log.New("infra.usagestats.collector"),
|
|
}
|
|
|
|
collectors := []usagestats.MetricsFunc{
|
|
s.collectSystemStats,
|
|
s.collectConcurrentUsers,
|
|
s.collectDatasourceStats,
|
|
s.collectDatasourceAccess,
|
|
s.collectAlertNotifierStats,
|
|
s.collectPrometheusFlavors,
|
|
s.collectAdditionalMetrics,
|
|
}
|
|
for _, c := range collectors {
|
|
us.RegisterMetricsFunc(c)
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// RegisterProviders is called only once - during Grafana start up
|
|
func (s *Service) RegisterProviders(usageStatProviders []registry.ProvidesUsageStats) {
|
|
s.log.Info("registering usage stat providers", "usageStatsProvidersLen", len(usageStatProviders))
|
|
s.usageStatProviders = usageStatProviders
|
|
}
|
|
|
|
func (s *Service) Run(ctx context.Context) error {
|
|
sendInterval := time.Second * time.Duration(s.cfg.MetricsTotalStatsIntervalSeconds)
|
|
nextSendInterval := time.Duration(rand.Intn(MAX_DELAY-MIN_DELAY)+MIN_DELAY) * time.Second
|
|
s.log.Debug("usage stats collector started", "sendInterval", sendInterval, "nextSendInterval", nextSendInterval)
|
|
updateStatsTicker := time.NewTicker(nextSendInterval)
|
|
defer updateStatsTicker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-updateStatsTicker.C:
|
|
s.updateTotalStats(ctx)
|
|
|
|
if nextSendInterval != sendInterval {
|
|
nextSendInterval = sendInterval
|
|
updateStatsTicker.Reset(nextSendInterval)
|
|
}
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Service) collectSystemStats(ctx context.Context) (map[string]any, error) {
|
|
m := map[string]any{}
|
|
|
|
statsResult, err := s.statsService.GetSystemStats(ctx, &stats.GetSystemStatsQuery{})
|
|
if err != nil {
|
|
s.log.Error("Failed to get system stats", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
m["stats.dashboards.count"] = statsResult.Dashboards
|
|
m["stats.dashboard_bytes.total"] = statsResult.DashboardBytesTotal
|
|
m["stats.dashboard_bytes.max"] = statsResult.DashboardBytesMax
|
|
m["stats.users.count"] = statsResult.Users
|
|
m["stats.admins.count"] = statsResult.Admins
|
|
m["stats.editors.count"] = statsResult.Editors
|
|
m["stats.viewers.count"] = statsResult.Viewers
|
|
m["stats.orgs.count"] = statsResult.Orgs
|
|
m["stats.playlist.count"] = statsResult.Playlists
|
|
m["stats.plugins.apps.count"] = s.appCount(ctx)
|
|
m["stats.plugins.panels.count"] = s.panelCount(ctx)
|
|
m["stats.plugins.datasources.count"] = s.dataSourceCount(ctx)
|
|
m["stats.alerts.count"] = statsResult.Alerts
|
|
m["stats.active_users.count"] = statsResult.ActiveUsers
|
|
m["stats.active_admins.count"] = statsResult.ActiveAdmins
|
|
m["stats.active_editors.count"] = statsResult.ActiveEditors
|
|
m["stats.active_viewers.count"] = statsResult.ActiveViewers
|
|
m["stats.active_sessions.count"] = statsResult.ActiveSessions
|
|
m["stats.monthly_active_users.count"] = statsResult.MonthlyActiveUsers
|
|
m["stats.daily_active_users.count"] = statsResult.DailyActiveUsers
|
|
m["stats.daily_active_admins.count"] = statsResult.DailyActiveAdmins
|
|
m["stats.daily_active_editors.count"] = statsResult.DailyActiveEditors
|
|
m["stats.daily_active_viewers.count"] = statsResult.DailyActiveViewers
|
|
m["stats.daily_active_sessions.count"] = statsResult.DailyActiveSessions
|
|
m["stats.datasources.count"] = statsResult.Datasources
|
|
m["stats.stars.count"] = statsResult.Stars
|
|
m["stats.folders.count"] = statsResult.Folders
|
|
m["stats.dashboard_permissions.count"] = statsResult.DashboardPermissions
|
|
m["stats.folder_permissions.count"] = statsResult.FolderPermissions
|
|
m["stats.provisioned_dashboards.count"] = statsResult.ProvisionedDashboards
|
|
m["stats.snapshots.count"] = statsResult.Snapshots
|
|
m["stats.teams.count"] = statsResult.Teams
|
|
m["stats.total_auth_token.count"] = statsResult.AuthTokens
|
|
m["stats.dashboard_versions.count"] = statsResult.DashboardVersions
|
|
m["stats.annotations.count"] = statsResult.Annotations
|
|
m["stats.alert_rules.count"] = statsResult.AlertRules
|
|
m["stats.library_panels.count"] = statsResult.LibraryPanels
|
|
m["stats.library_variables.count"] = statsResult.LibraryVariables
|
|
m["stats.dashboards_viewers_can_edit.count"] = statsResult.DashboardsViewersCanEdit
|
|
m["stats.dashboards_viewers_can_admin.count"] = statsResult.DashboardsViewersCanAdmin
|
|
m["stats.folders_viewers_can_edit.count"] = statsResult.FoldersViewersCanEdit
|
|
m["stats.folders_viewers_can_admin.count"] = statsResult.FoldersViewersCanAdmin
|
|
m["stats.api_keys.count"] = statsResult.APIKeys
|
|
m["stats.data_keys.count"] = statsResult.DataKeys
|
|
m["stats.active_data_keys.count"] = statsResult.ActiveDataKeys
|
|
m["stats.public_dashboards.count"] = statsResult.PublicDashboards
|
|
m["stats.correlations.count"] = statsResult.Correlations
|
|
if statsResult.DatabaseCreatedTime != nil {
|
|
m["stats.database.created.time"] = statsResult.DatabaseCreatedTime.Unix()
|
|
}
|
|
if statsResult.DatabaseDriver != "" {
|
|
m["stats.database.driver"] = statsResult.DatabaseDriver
|
|
}
|
|
|
|
ossEditionCount := 1
|
|
enterpriseEditionCount := 0
|
|
if s.cfg.IsEnterprise {
|
|
enterpriseEditionCount = 1
|
|
ossEditionCount = 0
|
|
}
|
|
m["stats.edition.oss.count"] = ossEditionCount
|
|
m["stats.edition.enterprise.count"] = enterpriseEditionCount
|
|
|
|
userCount := statsResult.Users
|
|
avgAuthTokensPerUser := statsResult.AuthTokens
|
|
if userCount != 0 {
|
|
avgAuthTokensPerUser /= userCount
|
|
}
|
|
|
|
m["stats.avg_auth_token_per_user.count"] = avgAuthTokensPerUser
|
|
m["stats.packaging."+s.cfg.Packaging+".count"] = 1
|
|
m["stats.distributor."+s.cfg.ReportingDistributor+".count"] = 1
|
|
|
|
m["stats.uptime"] = int64(time.Since(s.startTime).Seconds())
|
|
|
|
featureUsageStats := s.features.GetUsageStats(ctx)
|
|
for k, v := range featureUsageStats {
|
|
m[k] = v
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
func (s *Service) collectAdditionalMetrics(ctx context.Context) (map[string]any, error) {
|
|
m := map[string]any{}
|
|
for _, usageStatProvider := range s.usageStatProviders {
|
|
stats := usageStatProvider.GetUsageStats(ctx)
|
|
for k, v := range stats {
|
|
m[k] = v
|
|
}
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (s *Service) collectAlertNotifierStats(ctx context.Context) (map[string]any, error) {
|
|
m := map[string]any{}
|
|
// get stats about alert notifier usage
|
|
anResult, err := s.statsService.GetAlertNotifiersUsageStats(ctx, &stats.GetAlertNotifierUsageStatsQuery{})
|
|
if err != nil {
|
|
s.log.Error("Failed to get alert notification stats", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
for _, stats := range anResult {
|
|
m["stats.alert_notifiers."+stats.Type+".count"] = stats.Count
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (s *Service) collectDatasourceStats(ctx context.Context) (map[string]any, error) {
|
|
m := map[string]any{}
|
|
dsResult, err := s.statsService.GetDataSourceStats(ctx, &stats.GetDataSourceStatsQuery{})
|
|
if err != nil {
|
|
s.log.Error("Failed to get datasource stats", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
// send counters for each data source
|
|
// but ignore any custom data sources
|
|
// as sending that name could be sensitive information
|
|
dsOtherCount := 0
|
|
for _, dsStat := range dsResult {
|
|
if s.validator.ShouldBeReported(ctx, dsStat.Type) {
|
|
m["stats.ds."+dsStat.Type+".count"] = dsStat.Count
|
|
} else {
|
|
dsOtherCount += dsStat.Count
|
|
}
|
|
}
|
|
m["stats.ds.other.count"] = dsOtherCount
|
|
|
|
return m, nil
|
|
}
|
|
|
|
func (s *Service) collectDatasourceAccess(ctx context.Context) (map[string]any, error) {
|
|
m := map[string]any{}
|
|
|
|
// fetch datasource access stats
|
|
dsAccessResult, err := s.statsService.GetDataSourceAccessStats(ctx, &stats.GetDataSourceAccessStatsQuery{})
|
|
if err != nil {
|
|
s.log.Error("Failed to get datasource access stats", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
// send access counters for each data source
|
|
// but ignore any custom data sources
|
|
// as sending that name could be sensitive information
|
|
dsAccessOtherCount := make(map[string]int64)
|
|
for _, dsAccessStat := range dsAccessResult {
|
|
if dsAccessStat.Access == "" {
|
|
continue
|
|
}
|
|
|
|
access := strings.ToLower(dsAccessStat.Access)
|
|
|
|
if s.validator.ShouldBeReported(ctx, dsAccessStat.Type) {
|
|
m["stats.ds_access."+dsAccessStat.Type+"."+access+".count"] = dsAccessStat.Count
|
|
} else {
|
|
old := dsAccessOtherCount[access]
|
|
dsAccessOtherCount[access] = old + dsAccessStat.Count
|
|
}
|
|
}
|
|
|
|
for access, count := range dsAccessOtherCount {
|
|
m["stats.ds_access.other."+access+".count"] = count
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (s *Service) updateTotalStats(ctx context.Context) bool {
|
|
if !s.cfg.MetricsEndpointEnabled || s.cfg.MetricsEndpointDisableTotalStats {
|
|
return false
|
|
}
|
|
|
|
statsResult, err := s.statsService.GetSystemStats(ctx, &stats.GetSystemStatsQuery{})
|
|
if err != nil {
|
|
s.log.Error("Failed to get system stats", "error", err)
|
|
return false
|
|
}
|
|
|
|
if statsResult == nil {
|
|
s.log.Error("Cannot retrieve system stats")
|
|
return false
|
|
}
|
|
|
|
metrics.MStatTotalDashboards.Set(float64(statsResult.Dashboards))
|
|
metrics.MStatTotalFolders.Set(float64(statsResult.Folders))
|
|
metrics.MStatTotalUsers.Set(float64(statsResult.Users))
|
|
metrics.MStatTotalTeams.Set(float64(statsResult.Teams))
|
|
metrics.MStatActiveUsers.Set(float64(statsResult.ActiveUsers))
|
|
metrics.MStatTotalPlaylists.Set(float64(statsResult.Playlists))
|
|
metrics.MStatTotalOrgs.Set(float64(statsResult.Orgs))
|
|
metrics.StatsTotalViewers.Set(float64(statsResult.Viewers))
|
|
metrics.StatsTotalActiveViewers.Set(float64(statsResult.ActiveViewers))
|
|
metrics.StatsTotalEditors.Set(float64(statsResult.Editors))
|
|
metrics.StatsTotalActiveEditors.Set(float64(statsResult.ActiveEditors))
|
|
metrics.StatsTotalAdmins.Set(float64(statsResult.Admins))
|
|
metrics.StatsTotalActiveAdmins.Set(float64(statsResult.ActiveAdmins))
|
|
metrics.StatsTotalDashboardVersions.Set(float64(statsResult.DashboardVersions))
|
|
metrics.StatsTotalAnnotations.Set(float64(statsResult.Annotations))
|
|
metrics.StatsTotalAlertRules.Set(float64(statsResult.AlertRules))
|
|
metrics.StatsTotalLibraryPanels.Set(float64(statsResult.LibraryPanels))
|
|
metrics.StatsTotalLibraryVariables.Set(float64(statsResult.LibraryVariables))
|
|
|
|
metrics.StatsTotalDataKeys.With(prometheus.Labels{"active": "true"}).Set(float64(statsResult.ActiveDataKeys))
|
|
inactiveDataKeys := statsResult.DataKeys - statsResult.ActiveDataKeys
|
|
metrics.StatsTotalDataKeys.With(prometheus.Labels{"active": "false"}).Set(float64(inactiveDataKeys))
|
|
|
|
metrics.MStatTotalPublicDashboards.Set(float64(statsResult.PublicDashboards))
|
|
|
|
metrics.MStatTotalCorrelations.Set(float64(statsResult.Correlations))
|
|
|
|
s.usageStats.SetReadyToReport(ctx)
|
|
|
|
dsResult, err := s.statsService.GetDataSourceStats(ctx, &stats.GetDataSourceStatsQuery{})
|
|
if err != nil {
|
|
s.log.Error("Failed to get datasource stats", "error", err)
|
|
return true
|
|
}
|
|
|
|
for _, dsStat := range dsResult {
|
|
metrics.StatsTotalDataSources.WithLabelValues(dsStat.Type).Set(float64(dsStat.Count))
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (s *Service) appCount(ctx context.Context) int {
|
|
return len(s.plugins.Plugins(ctx, plugins.TypeApp))
|
|
}
|
|
|
|
func (s *Service) panelCount(ctx context.Context) int {
|
|
return len(s.plugins.Plugins(ctx, plugins.TypePanel))
|
|
}
|
|
|
|
func (s *Service) dataSourceCount(ctx context.Context) int {
|
|
return len(s.plugins.Plugins(ctx, plugins.TypeDataSource))
|
|
}
|