diff --git a/pkg/api/admin.go b/pkg/api/admin.go index d569e658db2..21603822519 100644 --- a/pkg/api/admin.go +++ b/pkg/api/admin.go @@ -47,7 +47,7 @@ func (hs *HTTPServer) AdminGetSettings(c *models.ReqContext) response.Response { func (hs *HTTPServer) AdminGetStats(c *models.ReqContext) response.Response { statsQuery := models.GetAdminStatsQuery{} - if err := hs.SQLStore.GetAdminStats(c.Req.Context(), &statsQuery); err != nil { + if err := hs.statsService.GetAdminStats(c.Req.Context(), &statsQuery); err != nil { return response.Error(500, "Failed to get admin stats from database", err) } diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index c9450f7267a..3f07d38d494 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -54,6 +54,7 @@ import ( "github.com/grafana/grafana/pkg/services/searchusers/filters" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" + "github.com/grafana/grafana/pkg/services/stats/statsimpl" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/team/teamimpl" @@ -251,6 +252,7 @@ func (s *fakeRenderService) Init() error { func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url string, permissions []accesscontrol.Permission) (*scenarioContext, *HTTPServer) { store := sqlstore.InitTestDB(t) + statsService := statsimpl.ProvideService(store) hs := &HTTPServer{ Cfg: cfg, Live: newTestLive(t, store), @@ -262,6 +264,7 @@ func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url strin searchUsersService: searchusers.ProvideUsersService(filters.ProvideOSSSearchUserFilter(), usertest.NewUserServiceFake()), ldapGroups: ldap.ProvideGroupsService(), accesscontrolService: actest.FakeService{}, + statsService: statsService, } sc := setupScenarioContext(t, url) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 222b6923990..ed99aa51c94 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -20,6 +20,7 @@ import ( "github.com/grafana/grafana/pkg/services/oauthtoken" "github.com/grafana/grafana/pkg/services/querylibrary" "github.com/grafana/grafana/pkg/services/searchV2" + "github.com/grafana/grafana/pkg/services/stats" "github.com/grafana/grafana/pkg/services/store/object/httpobjectstore" "github.com/prometheus/client_golang/prometheus" @@ -207,6 +208,7 @@ type HTTPServer struct { annotationsRepo annotations.Repository tagService tag.Service oauthTokenService oauthtoken.OAuthTokenService + statsService stats.Service } type ServerOptions struct { @@ -249,6 +251,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi accesscontrolService accesscontrol.Service, dashboardThumbsService thumbs.DashboardThumbService, navTreeService navtree.Service, annotationRepo annotations.Repository, tagService tag.Service, searchv2HTTPService searchV2.SearchHTTPService, queryLibraryHTTPService querylibrary.HTTPService, queryLibraryService querylibrary.Service, oauthTokenService oauthtoken.OAuthTokenService, + statsService stats.Service, ) (*HTTPServer, error) { web.Env = cfg.Env m := web.New() @@ -353,6 +356,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi QueryLibraryHTTPService: queryLibraryHTTPService, QueryLibraryService: queryLibraryService, oauthTokenService: oauthTokenService, + statsService: statsService, } if hs.Listener != nil { hs.log.Debug("Using provided listener") diff --git a/pkg/infra/usagestats/statscollector/concurrent_users_test.go b/pkg/infra/usagestats/statscollector/concurrent_users_test.go index 7185c28b6fe..da61dbba372 100644 --- a/pkg/infra/usagestats/statscollector/concurrent_users_test.go +++ b/pkg/infra/usagestats/statscollector/concurrent_users_test.go @@ -12,12 +12,14 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/services/stats/statsimpl" "github.com/grafana/grafana/pkg/util" ) func TestConcurrentUsersMetrics(t *testing.T) { sqlStore, cfg := db.InitTestDBwithCfg(t) - s := createService(t, cfg, sqlStore) + statsService := statsimpl.ProvideService(sqlStore) + s := createService(t, cfg, sqlStore, statsService) createConcurrentTokens(t, sqlStore) @@ -34,7 +36,8 @@ func TestConcurrentUsersMetrics(t *testing.T) { func TestConcurrentUsersStats(t *testing.T) { sqlStore, cfg := db.InitTestDBwithCfg(t) - s := createService(t, cfg, sqlStore) + statsService := statsimpl.ProvideService(sqlStore) + s := createService(t, cfg, sqlStore, statsService) createConcurrentTokens(t, sqlStore) diff --git a/pkg/infra/usagestats/statscollector/prometheus_flavor_test.go b/pkg/infra/usagestats/statscollector/prometheus_flavor_test.go index dbf04514b48..77bb7ec08ca 100644 --- a/pkg/infra/usagestats/statscollector/prometheus_flavor_test.go +++ b/pkg/infra/usagestats/statscollector/prometheus_flavor_test.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" + "github.com/grafana/grafana/pkg/services/stats/statstest" "github.com/grafana/grafana/pkg/setting" ) @@ -33,10 +34,12 @@ func TestDetectPrometheusVariant(t *testing.T) { t.Cleanup(cortex.Close) sqlStore := mockstore.NewSQLStoreMock() + statsService := statstest.NewFakeService() s := createService( t, setting.NewCfg(), sqlStore, + statsService, withDatasources(mockDatasourceService{datasources: []*datasources.DataSource{ { Id: 1, diff --git a/pkg/infra/usagestats/statscollector/service.go b/pkg/infra/usagestats/statscollector/service.go index e173cce9258..1372bd90318 100644 --- a/pkg/infra/usagestats/statscollector/service.go +++ b/pkg/infra/usagestats/statscollector/service.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/stats" "github.com/grafana/grafana/pkg/setting" ) @@ -28,6 +29,7 @@ type Service struct { plugins plugins.Store social social.Service usageStats usagestats.Service + statsService stats.Service features *featuremgmt.FeatureManager datasources datasources.DataSourceService httpClientProvider httpclient.Provider @@ -42,6 +44,7 @@ type Service struct { func ProvideService( us usagestats.Service, + statsService stats.Service, cfg *setting.Cfg, store sqlstore.Store, social social.Service, @@ -56,6 +59,7 @@ func ProvideService( plugins: plugins, social: social, usageStats: us, + statsService: statsService, features: features, datasources: datasourceService, httpClientProvider: httpClientProvider, @@ -106,7 +110,7 @@ func (s *Service) collectSystemStats(ctx context.Context) (map[string]interface{ m := map[string]interface{}{} statsQuery := models.GetSystemStatsQuery{} - if err := s.sqlstore.GetSystemStats(ctx, &statsQuery); err != nil { + if err := s.statsService.GetSystemStats(ctx, &statsQuery); err != nil { s.log.Error("Failed to get system stats", "error", err) return nil, err } @@ -219,7 +223,7 @@ func (s *Service) collectAlertNotifierStats(ctx context.Context) (map[string]int m := map[string]interface{}{} // get stats about alert notifier usage anStats := models.GetAlertNotifierUsageStatsQuery{} - if err := s.sqlstore.GetAlertNotifiersUsageStats(ctx, &anStats); err != nil { + if err := s.statsService.GetAlertNotifiersUsageStats(ctx, &anStats); err != nil { s.log.Error("Failed to get alert notification stats", "error", err) return nil, err } @@ -233,7 +237,7 @@ func (s *Service) collectAlertNotifierStats(ctx context.Context) (map[string]int func (s *Service) collectDatasourceStats(ctx context.Context) (map[string]interface{}, error) { m := map[string]interface{}{} dsStats := models.GetDataSourceStatsQuery{} - if err := s.sqlstore.GetDataSourceStats(ctx, &dsStats); err != nil { + if err := s.statsService.GetDataSourceStats(ctx, &dsStats); err != nil { s.log.Error("Failed to get datasource stats", "error", err) return nil, err } @@ -280,7 +284,7 @@ func (s *Service) collectDatasourceAccess(ctx context.Context) (map[string]inter // fetch datasource access stats dsAccessStats := models.GetDataSourceAccessStatsQuery{} - if err := s.sqlstore.GetDataSourceAccessStats(ctx, &dsAccessStats); err != nil { + if err := s.statsService.GetDataSourceAccessStats(ctx, &dsAccessStats); err != nil { s.log.Error("Failed to get datasource access stats", "error", err) return nil, err } @@ -316,7 +320,7 @@ func (s *Service) updateTotalStats(ctx context.Context) bool { } statsQuery := models.GetSystemStatsQuery{} - if err := s.sqlstore.GetSystemStats(ctx, &statsQuery); err != nil { + if err := s.statsService.GetSystemStats(ctx, &statsQuery); err != nil { s.log.Error("Failed to get system stats", "error", err) return false } @@ -351,7 +355,7 @@ func (s *Service) updateTotalStats(ctx context.Context) bool { metrics.MStatTotalPublicDashboards.Set(float64(statsQuery.Result.PublicDashboards)) dsStats := models.GetDataSourceStatsQuery{} - if err := s.sqlstore.GetDataSourceStats(ctx, &dsStats); err != nil { + if err := s.statsService.GetDataSourceStats(ctx, &dsStats); err != nil { s.log.Error("Failed to get datasource stats", "error", err) return true } diff --git a/pkg/infra/usagestats/statscollector/service_test.go b/pkg/infra/usagestats/statscollector/service_test.go index 1b3aea6d286..5ff592f89eb 100644 --- a/pkg/infra/usagestats/statscollector/service_test.go +++ b/pkg/infra/usagestats/statscollector/service_test.go @@ -24,16 +24,19 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" + "github.com/grafana/grafana/pkg/services/stats" + "github.com/grafana/grafana/pkg/services/stats/statstest" "github.com/grafana/grafana/pkg/setting" ) func TestTotalStatsUpdate(t *testing.T) { sqlStore := mockstore.NewSQLStoreMock() - s := createService(t, setting.NewCfg(), sqlStore) + statsService := statstest.NewFakeService() + s := createService(t, setting.NewCfg(), sqlStore, statsService) s.cfg.MetricsEndpointEnabled = true s.cfg.MetricsEndpointDisableTotalStats = false - sqlStore.ExpectedSystemStats = &models.SystemStats{} + statsService.ExpectedSystemStats = &models.SystemStats{} tests := []struct { MetricsEndpointEnabled bool @@ -94,8 +97,9 @@ func TestUsageStatsProviders(t *testing.T) { provider2 := &dummyUsageStatProvider{stats: map[string]interface{}{"my_stat_x": "valx", "my_stat_z": "valz"}} store := mockstore.NewSQLStoreMock() - mockSystemStats(store) - s := createService(t, setting.NewCfg(), store) + statsService := statstest.NewFakeService() + mockSystemStats(statsService) + s := createService(t, setting.NewCfg(), store, statsService) s.RegisterProviders([]registry.ProvidesUsageStats{provider1, provider2}) m, err := s.collectAdditionalMetrics(context.Background()) @@ -109,8 +113,9 @@ func TestUsageStatsProviders(t *testing.T) { func TestFeatureUsageStats(t *testing.T) { store := mockstore.NewSQLStoreMock() - mockSystemStats(store) - s := createService(t, setting.NewCfg(), store) + statsService := statstest.NewFakeService() + mockSystemStats(statsService) + s := createService(t, setting.NewCfg(), store, statsService) m, err := s.collectSystemStats(context.Background()) require.NoError(t, err, "Expected no error") @@ -121,6 +126,7 @@ func TestFeatureUsageStats(t *testing.T) { func TestCollectingUsageStats(t *testing.T) { sqlStore := mockstore.NewSQLStoreMock() + statsService := statstest.NewFakeService() expectedDataSources := []*datasources.DataSource{ { JsonData: simplejson.NewFromAny(map[string]interface{}{ @@ -148,12 +154,12 @@ func TestCollectingUsageStats(t *testing.T) { AuthProxyEnabled: true, Packaging: "deb", ReportingDistributor: "hosted-grafana", - }, sqlStore, + }, sqlStore, statsService, withDatasources(mockDatasourceService{datasources: expectedDataSources})) s.startTime = time.Now().Add(-1 * time.Minute) - mockSystemStats(sqlStore) + mockSystemStats(statsService) createConcurrentTokens(t, sqlStore) @@ -203,6 +209,7 @@ func TestCollectingUsageStats(t *testing.T) { func TestElasticStats(t *testing.T) { sqlStore := mockstore.NewSQLStoreMock() + statsService := statstest.NewFakeService() expectedDataSources := []*datasources.DataSource{ { @@ -231,7 +238,7 @@ func TestElasticStats(t *testing.T) { AuthProxyEnabled: true, Packaging: "deb", ReportingDistributor: "hosted-grafana", - }, sqlStore, + }, sqlStore, statsService, withDatasources(mockDatasourceService{datasources: expectedDataSources})) metrics, err := s.collectElasticStats(context.Background()) @@ -242,11 +249,12 @@ func TestElasticStats(t *testing.T) { } func TestDatasourceStats(t *testing.T) { sqlStore := mockstore.NewSQLStoreMock() - s := createService(t, &setting.Cfg{}, sqlStore) + statsService := statstest.NewFakeService() + s := createService(t, &setting.Cfg{}, sqlStore, statsService) setupSomeDataSourcePlugins(t, s) - sqlStore.ExpectedDataSourceStats = []*models.DataSourceStats{ + statsService.ExpectedDataSourceStats = []*models.DataSourceStats{ { Type: datasources.DS_ES, Count: 9, @@ -283,7 +291,7 @@ func TestDatasourceStats(t *testing.T) { }, } - sqlStore.ExpectedDataSourcesAccessStats = []*models.DataSourceAccessStats{ + statsService.ExpectedDataSourcesAccessStats = []*models.DataSourceAccessStats{ { Type: datasources.DS_ES, Access: "direct", @@ -349,9 +357,10 @@ func TestDatasourceStats(t *testing.T) { func TestAlertNotifiersStats(t *testing.T) { sqlStore := mockstore.NewSQLStoreMock() - s := createService(t, &setting.Cfg{}, sqlStore) + statsService := statstest.NewFakeService() + s := createService(t, &setting.Cfg{}, sqlStore, statsService) - sqlStore.ExpectedNotifierUsageStats = []*models.NotifierUsageStats{ + statsService.ExpectedNotifierUsageStats = []*models.NotifierUsageStats{ { Type: "slack", Count: 1, @@ -369,8 +378,8 @@ func TestAlertNotifiersStats(t *testing.T) { assert.EqualValues(t, 2, metrics["stats.alert_notifiers.webhook.count"]) } -func mockSystemStats(sqlStore *mockstore.SQLStoreMock) { - sqlStore.ExpectedSystemStats = &models.SystemStats{ +func mockSystemStats(statsService *statstest.FakeService) { + statsService.ExpectedSystemStats = &models.SystemStats{ Dashboards: 1, Datasources: 2, Users: 3, @@ -437,7 +446,7 @@ func setupSomeDataSourcePlugins(t *testing.T, s *Service) { } } -func createService(t testing.TB, cfg *setting.Cfg, store sqlstore.Store, opts ...func(*serviceOptions)) *Service { +func createService(t testing.TB, cfg *setting.Cfg, store sqlstore.Store, statsService stats.Service, opts ...func(*serviceOptions)) *Service { t.Helper() o := &serviceOptions{datasources: mockDatasourceService{}} @@ -448,6 +457,7 @@ func createService(t testing.TB, cfg *setting.Cfg, store sqlstore.Store, opts .. return ProvideService( &usagestats.UsageStatsMock{}, + statsService, cfg, store, &mockSocial{}, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 3bc71a93c21..19c3646755b 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -124,6 +124,7 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/services/star/starimpl" + "github.com/grafana/grafana/pkg/services/stats/statsimpl" "github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/services/store/kind" "github.com/grafana/grafana/pkg/services/store/object/httpobjectstore" @@ -353,6 +354,7 @@ var wireBasicSet = wire.NewSet( publicdashboardsApi.ProvideApi, userimpl.ProvideService, orgimpl.ProvideService, + statsimpl.ProvideService, grpccontext.ProvideContextHandler, grpcserver.ProvideService, grpcserver.ProvideHealthService, diff --git a/pkg/services/sqlstore/stats_integration_test.go b/pkg/services/sqlstore/stats_integration_test.go deleted file mode 100644 index 674c2674e91..00000000000 --- a/pkg/services/sqlstore/stats_integration_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package sqlstore - -import ( - "context" - "testing" - - "github.com/grafana/grafana/pkg/models" - "github.com/stretchr/testify/require" -) - -func TestIntegration_GetAdminStats(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - sqlStore := InitTestDB(t) - - query := models.GetAdminStatsQuery{} - err := sqlStore.GetAdminStats(context.Background(), &query) - require.NoError(t, err) -} diff --git a/pkg/services/sqlstore/store.go b/pkg/services/sqlstore/store.go index 0b903c29290..77dfe576391 100644 --- a/pkg/services/sqlstore/store.go +++ b/pkg/services/sqlstore/store.go @@ -12,13 +12,8 @@ import ( ) type Store interface { - GetAdminStats(ctx context.Context, query *models.GetAdminStatsQuery) error - GetAlertNotifiersUsageStats(ctx context.Context, query *models.GetAlertNotifierUsageStatsQuery) error - GetDataSourceStats(ctx context.Context, query *models.GetDataSourceStatsQuery) error - GetDataSourceAccessStats(ctx context.Context, query *models.GetDataSourceAccessStatsQuery) error GetDialect() migrator.Dialect GetDBType() core.DbType - GetSystemStats(ctx context.Context, query *models.GetSystemStatsQuery) error CreateUser(ctx context.Context, cmd user.CreateUserCommand) (*user.User, error) WithDbSession(ctx context.Context, callback DBTransactionFunc) error WithNewDbSession(ctx context.Context, callback DBTransactionFunc) error diff --git a/pkg/services/stats/stats.go b/pkg/services/stats/stats.go new file mode 100644 index 00000000000..da2b9a49bf2 --- /dev/null +++ b/pkg/services/stats/stats.go @@ -0,0 +1,16 @@ +package stats + +import ( + "context" + + "github.com/grafana/grafana/pkg/models" +) + +type Service interface { + GetAdminStats(ctx context.Context, query *models.GetAdminStatsQuery) error + GetAlertNotifiersUsageStats(ctx context.Context, query *models.GetAlertNotifierUsageStatsQuery) error + GetDataSourceStats(ctx context.Context, query *models.GetDataSourceStatsQuery) error + GetDataSourceAccessStats(ctx context.Context, query *models.GetDataSourceAccessStatsQuery) error + GetSystemStats(ctx context.Context, query *models.GetSystemStatsQuery) error + GetSystemUserCountStats(ctx context.Context, query *models.GetSystemUserCountStatsQuery) error +} diff --git a/pkg/services/sqlstore/stats.go b/pkg/services/stats/statsimpl/stats.go similarity index 77% rename from pkg/services/sqlstore/stats.go rename to pkg/services/stats/statsimpl/stats.go index f0a635e68f7..f5a0f79e91b 100644 --- a/pkg/services/sqlstore/stats.go +++ b/pkg/services/stats/statsimpl/stats.go @@ -1,39 +1,48 @@ -package sqlstore +package statsimpl import ( "context" "strconv" "time" + "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/services/stats" ) const activeUserTimeLimit = time.Hour * 24 * 30 const dailyActiveUserTimeLimit = time.Hour * 24 -func (ss *SQLStore) GetAlertNotifiersUsageStats(ctx context.Context, query *models.GetAlertNotifierUsageStatsQuery) error { - return ss.WithDbSession(ctx, func(dbSession *DBSession) error { - var rawSQL = `SELECT COUNT(*) AS count, type FROM ` + dialect.Quote("alert_notification") + ` GROUP BY type` +func ProvideService(db db.DB) stats.Service { + return &sqlStatsService{db: db} +} + +type sqlStatsService struct{ db db.DB } + +func (ss *sqlStatsService) GetAlertNotifiersUsageStats(ctx context.Context, query *models.GetAlertNotifierUsageStatsQuery) error { + return ss.db.WithDbSession(ctx, func(dbSession *db.Session) error { + var rawSQL = `SELECT COUNT(*) AS count, type FROM ` + ss.db.GetDialect().Quote("alert_notification") + ` GROUP BY type` query.Result = make([]*models.NotifierUsageStats, 0) err := dbSession.SQL(rawSQL).Find(&query.Result) return err }) } -func (ss *SQLStore) GetDataSourceStats(ctx context.Context, query *models.GetDataSourceStatsQuery) error { - return ss.WithDbSession(ctx, func(dbSession *DBSession) error { - var rawSQL = `SELECT COUNT(*) AS count, type FROM ` + dialect.Quote("data_source") + ` GROUP BY type` +func (ss *sqlStatsService) GetDataSourceStats(ctx context.Context, query *models.GetDataSourceStatsQuery) error { + return ss.db.WithDbSession(ctx, func(dbSession *db.Session) error { + var rawSQL = `SELECT COUNT(*) AS count, type FROM ` + ss.db.GetDialect().Quote("data_source") + ` GROUP BY type` query.Result = make([]*models.DataSourceStats, 0) err := dbSession.SQL(rawSQL).Find(&query.Result) return err }) } -func (ss *SQLStore) GetDataSourceAccessStats(ctx context.Context, query *models.GetDataSourceAccessStatsQuery) error { - return ss.WithDbSession(ctx, func(dbSession *DBSession) error { - var rawSQL = `SELECT COUNT(*) AS count, type, access FROM ` + dialect.Quote("data_source") + ` GROUP BY type, access` +func (ss *sqlStatsService) GetDataSourceAccessStats(ctx context.Context, query *models.GetDataSourceAccessStatsQuery) error { + return ss.db.WithDbSession(ctx, func(dbSession *db.Session) error { + var rawSQL = `SELECT COUNT(*) AS count, type, access FROM ` + ss.db.GetDialect().Quote("data_source") + ` GROUP BY type, access` query.Result = make([]*models.DataSourceAccessStats, 0) err := dbSession.SQL(rawSQL).Find(&query.Result) return err @@ -45,10 +54,11 @@ func notServiceAccount(dialect migrator.Dialect) string { dialect.BooleanStr(false) } -func (ss *SQLStore) GetSystemStats(ctx context.Context, query *models.GetSystemStatsQuery) error { - return ss.WithDbSession(ctx, func(dbSession *DBSession) error { - sb := &SQLBuilder{} +func (ss *sqlStatsService) GetSystemStats(ctx context.Context, query *models.GetSystemStatsQuery) error { + return ss.db.WithDbSession(ctx, func(dbSession *db.Session) error { + sb := &sqlstore.SQLBuilder{} sb.Write("SELECT ") + dialect := ss.db.GetDialect() sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("user") + ` WHERE ` + notServiceAccount(dialect) + `) AS users,`) sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("org") + `) AS orgs,`) sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("data_source") + `) AS datasources,`) @@ -88,10 +98,10 @@ func (ss *SQLStore) GetSystemStats(ctx context.Context, query *models.GetSystemS WHERE d.is_folder = ? ) AS folder_permissions,`, dialect.BooleanStr(true)) - sb.Write(viewersPermissionsCounterSQL("dashboards_viewers_can_edit", false, models.PERMISSION_EDIT)) - sb.Write(viewersPermissionsCounterSQL("dashboards_viewers_can_admin", false, models.PERMISSION_ADMIN)) - sb.Write(viewersPermissionsCounterSQL("folders_viewers_can_edit", true, models.PERMISSION_EDIT)) - sb.Write(viewersPermissionsCounterSQL("folders_viewers_can_admin", true, models.PERMISSION_ADMIN)) + sb.Write(viewersPermissionsCounterSQL(ss.db, "dashboards_viewers_can_edit", false, models.PERMISSION_EDIT)) + sb.Write(viewersPermissionsCounterSQL(ss.db, "dashboards_viewers_can_admin", false, models.PERMISSION_ADMIN)) + sb.Write(viewersPermissionsCounterSQL(ss.db, "folders_viewers_can_edit", true, models.PERMISSION_EDIT)) + sb.Write(viewersPermissionsCounterSQL(ss.db, "folders_viewers_can_admin", true, models.PERMISSION_ADMIN)) sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("dashboard_provisioning") + `) AS provisioned_dashboards,`) sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("dashboard_snapshot") + `) AS snapshots,`) @@ -112,7 +122,7 @@ func (ss *SQLStore) GetSystemStats(ctx context.Context, query *models.GetSystemS sb.Write(ss.roleCounterSQL(ctx)) var stats models.SystemStats - _, err := dbSession.SQL(sb.GetSQLString(), sb.params...).Get(&stats) + _, err := dbSession.SQL(sb.GetSQLString(), sb.GetParams()...).Get(&stats) if err != nil { return err } @@ -123,7 +133,7 @@ func (ss *SQLStore) GetSystemStats(ctx context.Context, query *models.GetSystemS }) } -func (ss *SQLStore) roleCounterSQL(ctx context.Context) string { +func (ss *sqlStatsService) roleCounterSQL(ctx context.Context) string { const roleCounterTimeout = 20 * time.Second ctx, cancel := context.WithTimeout(ctx, roleCounterTimeout) defer cancel() @@ -142,7 +152,8 @@ func (ss *SQLStore) roleCounterSQL(ctx context.Context) string { return sqlQuery } -func viewersPermissionsCounterSQL(statName string, isFolder bool, permission models.PermissionType) string { +func viewersPermissionsCounterSQL(db db.DB, statName string, isFolder bool, permission models.PermissionType) string { + dialect := db.GetDialect() return `( SELECT COUNT(*) FROM ` + dialect.Quote("dashboard_acl") + ` AS acl @@ -154,8 +165,9 @@ func viewersPermissionsCounterSQL(statName string, isFolder bool, permission mod ) AS ` + statName + `, ` } -func (ss *SQLStore) GetAdminStats(ctx context.Context, query *models.GetAdminStatsQuery) error { - return ss.WithDbSession(ctx, func(dbSession *DBSession) error { +func (ss *sqlStatsService) GetAdminStats(ctx context.Context, query *models.GetAdminStatsQuery) error { + return ss.db.WithDbSession(ctx, func(dbSession *db.Session) error { + dialect := ss.db.GetDialect() now := time.Now() activeEndDate := now.Add(-activeUserTimeLimit) dailyActiveEndDate := now.Add(-dailyActiveUserTimeLimit) @@ -231,9 +243,9 @@ func (ss *SQLStore) GetAdminStats(ctx context.Context, query *models.GetAdminSta }) } -func (ss *SQLStore) GetSystemUserCountStats(ctx context.Context, query *models.GetSystemUserCountStatsQuery) error { - return ss.WithDbSession(ctx, func(sess *DBSession) error { - var rawSQL = `SELECT COUNT(id) AS Count FROM ` + dialect.Quote("user") +func (ss *sqlStatsService) GetSystemUserCountStats(ctx context.Context, query *models.GetSystemUserCountStatsQuery) error { + return ss.db.WithDbSession(ctx, func(sess *db.Session) error { + var rawSQL = `SELECT COUNT(id) AS Count FROM ` + ss.db.GetDialect().Quote("user") var stats models.SystemUserCountStats _, err := sess.SQL(rawSQL).Get(&stats) if err != nil { @@ -246,7 +258,7 @@ func (ss *SQLStore) GetSystemUserCountStats(ctx context.Context, query *models.G }) } -func (ss *SQLStore) updateUserRoleCountsIfNecessary(ctx context.Context, forced bool) error { +func (ss *sqlStatsService) updateUserRoleCountsIfNecessary(ctx context.Context, forced bool) error { memoizationPeriod := time.Now().Add(-userStatsCacheLimetime) if forced || userStatsCache.memoized.Before(memoizationPeriod) { err := ss.updateUserRoleCounts(ctx) @@ -270,8 +282,8 @@ var ( userStatsCacheLimetime = 5 * time.Minute ) -func (ss *SQLStore) updateUserRoleCounts(ctx context.Context) error { - return ss.WithDbSession(ctx, func(dbSession *DBSession) error { +func (ss *sqlStatsService) updateUserRoleCounts(ctx context.Context) error { + return ss.db.WithDbSession(ctx, func(dbSession *db.Session) error { query := ` SELECT role AS bitrole, active, COUNT(role) AS count FROM (SELECT last_seen_at>? AS active, last_seen_at>? AS daily_active, SUM(role) AS role @@ -283,7 +295,7 @@ SELECT role AS bitrole, active, COUNT(role) AS count FROM ELSE 1 END AS role, u.last_seen_at - FROM ` + dialect.Quote("user") + ` AS u INNER JOIN org_user ON org_user.user_id = u.id + FROM ` + ss.db.GetDialect().Quote("user") + ` AS u INNER JOIN org_user ON org_user.user_id = u.id GROUP BY u.id, u.last_seen_at, org_user.role) AS t2 GROUP BY id, last_seen_at) AS t1 GROUP BY active, daily_active, role;` diff --git a/pkg/services/sqlstore/stats_test.go b/pkg/services/stats/statsimpl/stats_test.go similarity index 60% rename from pkg/services/sqlstore/stats_test.go rename to pkg/services/stats/statsimpl/stats_test.go index 1b0eca543e9..1017f870b35 100644 --- a/pkg/services/sqlstore/stats_test.go +++ b/pkg/services/stats/statsimpl/stats_test.go @@ -1,28 +1,31 @@ -package sqlstore +package statsimpl import ( "context" "fmt" "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/org/orgimpl" + "github.com/grafana/grafana/pkg/services/quota/quotatest" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/user" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestIntegrationStatsDataAccess(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } - sqlStore := InitTestDB(t) - populateDB(t, sqlStore) + db := sqlstore.InitTestDB(t) + statsService := &sqlStatsService{db: db} + populateDB(t, db) t.Run("Get system stats should not results in error", func(t *testing.T) { query := models.GetSystemStatsQuery{} - err := sqlStore.GetSystemStats(context.Background(), &query) + err := statsService.GetSystemStats(context.Background(), &query) require.NoError(t, err) assert.Equal(t, int64(3), query.Result.Users) assert.Equal(t, int64(0), query.Result.Editors) @@ -35,38 +38,40 @@ func TestIntegrationStatsDataAccess(t *testing.T) { t.Run("Get system user count stats should not results in error", func(t *testing.T) { query := models.GetSystemUserCountStatsQuery{} - err := sqlStore.GetSystemUserCountStats(context.Background(), &query) + err := statsService.GetSystemUserCountStats(context.Background(), &query) assert.NoError(t, err) }) t.Run("Get datasource stats should not results in error", func(t *testing.T) { query := models.GetDataSourceStatsQuery{} - err := sqlStore.GetDataSourceStats(context.Background(), &query) + err := statsService.GetDataSourceStats(context.Background(), &query) assert.NoError(t, err) }) t.Run("Get datasource access stats should not results in error", func(t *testing.T) { query := models.GetDataSourceAccessStatsQuery{} - err := sqlStore.GetDataSourceAccessStats(context.Background(), &query) + err := statsService.GetDataSourceAccessStats(context.Background(), &query) assert.NoError(t, err) }) t.Run("Get alert notifier stats should not results in error", func(t *testing.T) { query := models.GetAlertNotifierUsageStatsQuery{} - err := sqlStore.GetAlertNotifiersUsageStats(context.Background(), &query) + err := statsService.GetAlertNotifiersUsageStats(context.Background(), &query) assert.NoError(t, err) }) t.Run("Get admin stats should not result in error", func(t *testing.T) { query := models.GetAdminStatsQuery{} - err := sqlStore.GetAdminStats(context.Background(), &query) + err := statsService.GetAdminStats(context.Background(), &query) assert.NoError(t, err) }) } -func populateDB(t *testing.T, sqlStore *SQLStore) { +func populateDB(t *testing.T, sqlStore *sqlstore.SQLStore) { t.Helper() + orgService, _ := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotatest.New(false, nil)) + users := make([]user.User, 3) for i := range users { cmd := user.CreateUserCommand{ @@ -81,33 +86,41 @@ func populateDB(t *testing.T, sqlStore *SQLStore) { } // add 2nd user as editor - cmd := &models.AddOrgUserCommand{ - OrgId: users[0].OrgID, - UserId: users[1].ID, + cmd := &org.AddOrgUserCommand{ + OrgID: users[0].OrgID, + UserID: users[1].ID, Role: org.RoleEditor, } - err := sqlStore.addOrgUser(context.Background(), cmd) + err := orgService.AddOrgUser(context.Background(), cmd) require.NoError(t, err) // add 3rd user as viewer - cmd = &models.AddOrgUserCommand{ - OrgId: users[0].OrgID, - UserId: users[2].ID, + cmd = &org.AddOrgUserCommand{ + OrgID: users[0].OrgID, + UserID: users[2].ID, Role: org.RoleViewer, } - err = sqlStore.addOrgUser(context.Background(), cmd) + err = orgService.AddOrgUser(context.Background(), cmd) require.NoError(t, err) // add 1st user as admin - cmd = &models.AddOrgUserCommand{ - OrgId: users[1].OrgID, - UserId: users[0].ID, + cmd = &org.AddOrgUserCommand{ + OrgID: users[1].OrgID, + UserID: users[0].ID, Role: org.RoleAdmin, } - err = sqlStore.addOrgUser(context.Background(), cmd) + err = orgService.AddOrgUser(context.Background(), cmd) require.NoError(t, err) +} + +func TestIntegration_GetAdminStats(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + db := sqlstore.InitTestDB(t) + statsService := ProvideService(db) - // force renewal of user stats - err = sqlStore.updateUserRoleCountsIfNecessary(context.Background(), true) + query := models.GetAdminStatsQuery{} + err := statsService.GetAdminStats(context.Background(), &query) require.NoError(t, err) } diff --git a/pkg/services/stats/statstest/stats.go b/pkg/services/stats/statstest/stats.go new file mode 100644 index 00000000000..77822bbfffb --- /dev/null +++ b/pkg/services/stats/statstest/stats.go @@ -0,0 +1,48 @@ +package statstest + +import ( + "context" + + "github.com/grafana/grafana/pkg/models" +) + +type FakeService struct { + ExpectedSystemStats *models.SystemStats + ExpectedDataSourceStats []*models.DataSourceStats + ExpectedDataSourcesAccessStats []*models.DataSourceAccessStats + ExpectedNotifierUsageStats []*models.NotifierUsageStats + + ExpectedError error +} + +func NewFakeService() *FakeService { + return &FakeService{} +} + +func (s *FakeService) GetAdminStats(ctx context.Context, query *models.GetAdminStatsQuery) error { + return s.ExpectedError +} + +func (s *FakeService) GetAlertNotifiersUsageStats(ctx context.Context, query *models.GetAlertNotifierUsageStatsQuery) error { + query.Result = s.ExpectedNotifierUsageStats + return s.ExpectedError +} + +func (s *FakeService) GetDataSourceStats(ctx context.Context, query *models.GetDataSourceStatsQuery) error { + query.Result = s.ExpectedDataSourceStats + return s.ExpectedError +} + +func (s *FakeService) GetDataSourceAccessStats(ctx context.Context, query *models.GetDataSourceAccessStatsQuery) error { + query.Result = s.ExpectedDataSourcesAccessStats + return s.ExpectedError +} + +func (s *FakeService) GetSystemStats(ctx context.Context, query *models.GetSystemStatsQuery) error { + query.Result = s.ExpectedSystemStats + return s.ExpectedError +} + +func (s *FakeService) GetSystemUserCountStats(ctx context.Context, query *models.GetSystemUserCountStatsQuery) error { + return s.ExpectedError +}