mirror of
https://github.com/grafana/grafana.git
synced 2024-11-30 12:44:10 -06:00
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
This commit is contained in:
parent
943a8508a6
commit
d5883c1b27
@ -256,7 +256,7 @@ func (uss *UsageStats) sendUsageStats(ctx context.Context) error {
|
|||||||
return nil
|
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)
|
report, err := uss.GetUsageReport(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
37
pkg/services/serviceaccounts/database/stats.go
Normal file
37
pkg/services/serviceaccounts/database/stats.go
Normal file
@ -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
|
||||||
|
}
|
41
pkg/services/serviceaccounts/database/stats_test.go
Normal file
41
pkg/services/serviceaccounts/database/stats_test.go
Normal file
@ -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))
|
||||||
|
}
|
@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/routing"
|
"github.com/grafana/grafana/pkg/api/routing"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"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/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
@ -30,6 +31,7 @@ func ProvideServiceAccountsService(
|
|||||||
store *sqlstore.SQLStore,
|
store *sqlstore.SQLStore,
|
||||||
ac accesscontrol.AccessControl,
|
ac accesscontrol.AccessControl,
|
||||||
routeRegister routing.RouteRegister,
|
routeRegister routing.RouteRegister,
|
||||||
|
usageStats usagestats.Service,
|
||||||
) (*ServiceAccountsService, error) {
|
) (*ServiceAccountsService, error) {
|
||||||
s := &ServiceAccountsService{
|
s := &ServiceAccountsService{
|
||||||
features: features,
|
features: features,
|
||||||
@ -41,6 +43,8 @@ func ProvideServiceAccountsService(
|
|||||||
if err := RegisterRoles(ac); err != nil {
|
if err := RegisterRoles(ac); err != nil {
|
||||||
s.log.Error("Failed to register roles", "error", err)
|
s.log.Error("Failed to register roles", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
usageStats.RegisterMetricsFunc(s.store.GetUsageMetrics)
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceaccountsAPI := api.NewServiceAccountsAPI(cfg, s, ac, routeRegister, s.store)
|
serviceaccountsAPI := api.NewServiceAccountsAPI(cfg, s, ac, routeRegister, s.store)
|
||||||
|
@ -25,4 +25,5 @@ type Store interface {
|
|||||||
ListTokens(ctx context.Context, orgID int64, serviceAccount int64) ([]*models.ApiKey, error)
|
ListTokens(ctx context.Context, orgID int64, serviceAccount int64) ([]*models.ApiKey, error)
|
||||||
DeleteServiceAccountToken(ctx context.Context, orgID, serviceAccountID, tokenID int64) error
|
DeleteServiceAccountToken(ctx context.Context, orgID, serviceAccountID, tokenID int64) error
|
||||||
AddServiceAccountToken(ctx context.Context, serviceAccountID int64, cmd *models.AddApiKeyCommand) error
|
AddServiceAccountToken(ctx context.Context, serviceAccountID int64, cmd *models.AddApiKeyCommand) error
|
||||||
|
GetUsageMetrics(ctx context.Context) (map[string]interface{}, error)
|
||||||
}
|
}
|
||||||
|
@ -138,3 +138,7 @@ func (s *ServiceAccountsStoreMock) AddServiceAccountToken(ctx context.Context, s
|
|||||||
s.Calls.AddServiceAccountToken = append(s.Calls.AddServiceAccountToken, []interface{}{ctx, cmd})
|
s.Calls.AddServiceAccountToken = append(s.Calls.AddServiceAccountToken, []interface{}{ctx, cmd})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ServiceAccountsStoreMock) GetUsageMetrics(ctx context.Context) (map[string]interface{}, error) {
|
||||||
|
return map[string]interface{}{}, nil
|
||||||
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ss *SQLStore) addStatsQueryAndCommandHandlers() {
|
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 {
|
func (ss *SQLStore) GetSystemStats(ctx context.Context, query *models.GetSystemStatsQuery) error {
|
||||||
return ss.WithDbSession(ctx, func(dbSession *DBSession) error {
|
return ss.WithDbSession(ctx, func(dbSession *DBSession) error {
|
||||||
sb := &SQLBuilder{}
|
sb := &SQLBuilder{}
|
||||||
sb.Write("SELECT ")
|
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("org") + `) AS orgs,`)
|
||||||
sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("data_source") + `) AS datasources,`)
|
sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("data_source") + `) AS datasources,`)
|
||||||
sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("star") + `) AS stars,`)
|
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()
|
now := time.Now()
|
||||||
activeUserDeadlineDate := now.Add(-activeUserTimeLimit)
|
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)
|
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())
|
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 dashboards,`, dialect.BooleanStr(false))
|
||||||
sb.Write(`(SELECT COUNT(id) FROM `+dialect.Quote("dashboard")+` WHERE is_folder = ?) AS folders,`, dialect.BooleanStr(true))
|
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("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("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("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_panels,`, models.PanelElement)
|
||||||
sb.Write(`(SELECT COUNT(id) FROM `+dialect.Quote("library_element")+` WHERE kind = ?) AS library_variables,`, models.VariableElement)
|
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,
|
) AS alerts,
|
||||||
(
|
(
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM ` + dialect.Quote("user") + `
|
FROM ` + dialect.Quote("user") + ` WHERE ` + notServiceAccount(dialect) + `
|
||||||
) AS users,
|
) AS users,
|
||||||
(
|
(
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM ` + dialect.Quote("user") + ` WHERE last_seen_at > ?
|
FROM ` + dialect.Quote("user") + ` WHERE ` + notServiceAccount(dialect) + ` AND last_seen_at > ?
|
||||||
) AS active_users,
|
) AS active_users,
|
||||||
(
|
(
|
||||||
SELECT COUNT(*)
|
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,
|
) AS daily_active_users,
|
||||||
(
|
(
|
||||||
SELECT COUNT(*)
|
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,
|
) AS monthly_active_users,
|
||||||
` + ss.roleCounterSQL(ctx) + `,
|
` + ss.roleCounterSQL(ctx) + `,
|
||||||
(
|
(
|
||||||
|
Loading…
Reference in New Issue
Block a user