From d5883c1b278efcc7bda52b73b98c8e239c561c7a Mon Sep 17 00:00:00 2001 From: Jguer Date: Wed, 16 Mar 2022 15:54:34 +0000 Subject: [PATCH] Service Accounts: Implement basic usage stats (#46619) * Stats: do not count SAs as users * Stats: implement basic service account metrics * Stats: do not count service account tokens as api keys * Stats: fix metric names * Stats: add SA stats test * rename user to sa --- pkg/infra/usagestats/service/usage_stats.go | 2 +- .../serviceaccounts/database/stats.go | 37 +++++++++++++++++ .../serviceaccounts/database/stats_test.go | 41 +++++++++++++++++++ .../serviceaccounts/manager/service.go | 4 ++ .../serviceaccounts/serviceaccounts.go | 1 + pkg/services/serviceaccounts/tests/common.go | 4 ++ pkg/services/sqlstore/stats.go | 27 ++++++++---- 7 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 pkg/services/serviceaccounts/database/stats.go create mode 100644 pkg/services/serviceaccounts/database/stats_test.go diff --git a/pkg/infra/usagestats/service/usage_stats.go b/pkg/infra/usagestats/service/usage_stats.go index 0a28259b48f..b67e1db0aa9 100644 --- a/pkg/infra/usagestats/service/usage_stats.go +++ b/pkg/infra/usagestats/service/usage_stats.go @@ -256,7 +256,7 @@ func (uss *UsageStats) sendUsageStats(ctx context.Context) error { return nil } - uss.log.Debug(fmt.Sprintf("Sending anonymous usage stats to %s", usageStatsURL)) + uss.log.Debug("Sending anonymous usage stats", "url", usageStatsURL) report, err := uss.GetUsageReport(ctx) if err != nil { diff --git a/pkg/services/serviceaccounts/database/stats.go b/pkg/services/serviceaccounts/database/stats.go new file mode 100644 index 00000000000..c24dbfcd855 --- /dev/null +++ b/pkg/services/serviceaccounts/database/stats.go @@ -0,0 +1,37 @@ +package database + +import ( + "context" + + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +func (s *ServiceAccountsStoreImpl) GetUsageMetrics(ctx context.Context) (map[string]interface{}, error) { + stats := map[string]interface{}{"stats.serviceaccounts.enabled.count": int64(1)} + + sb := &sqlstore.SQLBuilder{} + dialect := s.sqlStore.Dialect + sb.Write("SELECT ") + sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("user") + + ` WHERE is_service_account = ` + dialect.BooleanStr(true) + `) AS serviceaccounts,`) + sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("api_key") + + ` WHERE service_account_id IS NOT NULL ) AS serviceaccount_tokens`) + + type saStats struct { + ServiceAccounts int64 `xorm:"serviceaccounts"` + Tokens int64 `xorm:"serviceaccount_tokens"` + } + + var sqlStats saStats + if err := s.sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + _, err := sess.SQL(sb.GetSQLString(), sb.GetParams()...).Get(&sqlStats) + return err + }); err != nil { + return nil, err + } + + stats["stats.serviceaccounts.count"] = sqlStats.ServiceAccounts + stats["stats.serviceaccounts.tokens.count"] = sqlStats.Tokens + + return stats, nil +} diff --git a/pkg/services/serviceaccounts/database/stats_test.go b/pkg/services/serviceaccounts/database/stats_test.go new file mode 100644 index 00000000000..067bf756d9d --- /dev/null +++ b/pkg/services/serviceaccounts/database/stats_test.go @@ -0,0 +1,41 @@ +package database + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/components/apikeygen" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestStore_UsageStats(t *testing.T) { + saToCreate := tests.TestUser{Login: "servicetestwithTeam@admin", IsServiceAccount: true} + db, store := setupTestDatabase(t) + sa := tests.SetupUserServiceAccount(t, db, saToCreate) + + keyName := t.Name() + key, err := apikeygen.New(sa.OrgId, keyName) + require.NoError(t, err) + + cmd := models.AddApiKeyCommand{ + Name: keyName, + Role: "Viewer", + OrgId: sa.OrgId, + Key: key.HashedKey, + SecondsToLive: 0, + Result: &models.ApiKey{}, + } + + err = store.AddServiceAccountToken(context.Background(), sa.Id, &cmd) + require.NoError(t, err) + + stats, err := store.GetUsageMetrics(context.Background()) + require.NoError(t, err) + + assert.Equal(t, int64(1), stats["stats.serviceaccounts.count"].(int64)) + assert.Equal(t, int64(1), stats["stats.serviceaccounts.tokens.count"].(int64)) + assert.Equal(t, int64(1), stats["stats.serviceaccounts.enabled.count"].(int64)) +} diff --git a/pkg/services/serviceaccounts/manager/service.go b/pkg/services/serviceaccounts/manager/service.go index 455e0bd7c0a..9d8967488b5 100644 --- a/pkg/services/serviceaccounts/manager/service.go +++ b/pkg/services/serviceaccounts/manager/service.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/serviceaccounts" @@ -30,6 +31,7 @@ func ProvideServiceAccountsService( store *sqlstore.SQLStore, ac accesscontrol.AccessControl, routeRegister routing.RouteRegister, + usageStats usagestats.Service, ) (*ServiceAccountsService, error) { s := &ServiceAccountsService{ features: features, @@ -41,6 +43,8 @@ func ProvideServiceAccountsService( if err := RegisterRoles(ac); err != nil { s.log.Error("Failed to register roles", "error", err) } + + usageStats.RegisterMetricsFunc(s.store.GetUsageMetrics) } serviceaccountsAPI := api.NewServiceAccountsAPI(cfg, s, ac, routeRegister, s.store) diff --git a/pkg/services/serviceaccounts/serviceaccounts.go b/pkg/services/serviceaccounts/serviceaccounts.go index ac95861905e..5032dbe87d6 100644 --- a/pkg/services/serviceaccounts/serviceaccounts.go +++ b/pkg/services/serviceaccounts/serviceaccounts.go @@ -25,4 +25,5 @@ type Store interface { ListTokens(ctx context.Context, orgID int64, serviceAccount int64) ([]*models.ApiKey, error) DeleteServiceAccountToken(ctx context.Context, orgID, serviceAccountID, tokenID int64) error AddServiceAccountToken(ctx context.Context, serviceAccountID int64, cmd *models.AddApiKeyCommand) error + GetUsageMetrics(ctx context.Context) (map[string]interface{}, error) } diff --git a/pkg/services/serviceaccounts/tests/common.go b/pkg/services/serviceaccounts/tests/common.go index 59f6077d3fd..1de775e5c79 100644 --- a/pkg/services/serviceaccounts/tests/common.go +++ b/pkg/services/serviceaccounts/tests/common.go @@ -138,3 +138,7 @@ func (s *ServiceAccountsStoreMock) AddServiceAccountToken(ctx context.Context, s s.Calls.AddServiceAccountToken = append(s.Calls.AddServiceAccountToken, []interface{}{ctx, cmd}) return nil } + +func (s *ServiceAccountsStoreMock) GetUsageMetrics(ctx context.Context) (map[string]interface{}, error) { + return map[string]interface{}{}, nil +} diff --git a/pkg/services/sqlstore/stats.go b/pkg/services/sqlstore/stats.go index cd1de67f128..43777190fe3 100644 --- a/pkg/services/sqlstore/stats.go +++ b/pkg/services/sqlstore/stats.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/sqlstore/migrator" ) func (ss *SQLStore) addStatsQueryAndCommandHandlers() { @@ -48,11 +49,16 @@ func (ss *SQLStore) GetDataSourceAccessStats(ctx context.Context, query *models. }) } +func notServiceAccount(dialect migrator.Dialect) string { + return `is_service_account = ` + + dialect.BooleanStr(false) +} + func (ss *SQLStore) GetSystemStats(ctx context.Context, query *models.GetSystemStatsQuery) error { return ss.WithDbSession(ctx, func(dbSession *DBSession) error { sb := &SQLBuilder{} sb.Write("SELECT ") - sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("user") + `) AS users,`) + 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,`) sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("star") + `) AS stars,`) @@ -61,13 +67,16 @@ func (ss *SQLStore) GetSystemStats(ctx context.Context, query *models.GetSystemS now := time.Now() activeUserDeadlineDate := now.Add(-activeUserTimeLimit) - sb.Write(`(SELECT COUNT(*) FROM `+dialect.Quote("user")+` WHERE last_seen_at > ?) AS active_users,`, activeUserDeadlineDate) + sb.Write(`(SELECT COUNT(*) FROM `+dialect.Quote("user")+` WHERE `+ + notServiceAccount(dialect)+` AND last_seen_at > ?) AS active_users,`, activeUserDeadlineDate) dailyActiveUserDeadlineDate := now.Add(-dailyActiveUserTimeLimit) - sb.Write(`(SELECT COUNT(*) FROM `+dialect.Quote("user")+` WHERE last_seen_at > ?) AS daily_active_users,`, dailyActiveUserDeadlineDate) + sb.Write(`(SELECT COUNT(*) FROM `+dialect.Quote("user")+` WHERE `+ + notServiceAccount(dialect)+` AND last_seen_at > ?) AS daily_active_users,`, dailyActiveUserDeadlineDate) monthlyActiveUserDeadlineDate := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) - sb.Write(`(SELECT COUNT(*) FROM `+dialect.Quote("user")+` WHERE last_seen_at > ?) AS monthly_active_users,`, monthlyActiveUserDeadlineDate) + sb.Write(`(SELECT COUNT(*) FROM `+dialect.Quote("user")+` WHERE `+ + notServiceAccount(dialect)+` AND last_seen_at > ?) AS monthly_active_users,`, monthlyActiveUserDeadlineDate) sb.Write(`(SELECT COUNT(id) FROM `+dialect.Quote("dashboard")+` WHERE is_folder = ?) AS dashboards,`, dialect.BooleanStr(false)) sb.Write(`(SELECT COUNT(id) FROM `+dialect.Quote("dashboard")+` WHERE is_folder = ?) AS folders,`, dialect.BooleanStr(true)) @@ -100,7 +109,7 @@ func (ss *SQLStore) GetSystemStats(ctx context.Context, query *models.GetSystemS sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("team") + `) AS teams,`) sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("user_auth_token") + `) AS auth_tokens,`) sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("alert_rule") + `) AS alert_rules,`) - sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("api_key") + `) AS api_keys,`) + sb.Write(`(SELECT COUNT(id) FROM ` + dialect.Quote("api_key") + `WHERE service_account_id IS NULL) AS api_keys,`) sb.Write(`(SELECT COUNT(id) FROM `+dialect.Quote("library_element")+` WHERE kind = ?) AS library_panels,`, models.PanelElement) sb.Write(`(SELECT COUNT(id) FROM `+dialect.Quote("library_element")+` WHERE kind = ?) AS library_variables,`, models.VariableElement) @@ -191,19 +200,19 @@ func (ss *SQLStore) GetAdminStats(ctx context.Context, query *models.GetAdminSta ) AS alerts, ( SELECT COUNT(*) - FROM ` + dialect.Quote("user") + ` + FROM ` + dialect.Quote("user") + ` WHERE ` + notServiceAccount(dialect) + ` ) AS users, ( SELECT COUNT(*) - FROM ` + dialect.Quote("user") + ` WHERE last_seen_at > ? + FROM ` + dialect.Quote("user") + ` WHERE ` + notServiceAccount(dialect) + ` AND last_seen_at > ? ) AS active_users, ( SELECT COUNT(*) - FROM ` + dialect.Quote("user") + ` WHERE last_seen_at > ? + FROM ` + dialect.Quote("user") + ` WHERE ` + notServiceAccount(dialect) + ` AND last_seen_at > ? ) AS daily_active_users, ( SELECT COUNT(*) - FROM ` + dialect.Quote("user") + ` WHERE last_seen_at > ? + FROM ` + dialect.Quote("user") + ` WHERE ` + notServiceAccount(dialect) + ` AND last_seen_at > ? ) AS monthly_active_users, ` + ss.roleCounterSQL(ctx) + `, (