grafana/pkg/infra/usagestats/statscollector/service_test.go
Jo 108202f96c
UsageStats: Separate context and threads for usage stats (#83963)
* separate context and threads for usage stats

* use constants

* ignore original context

* fix runMetricsFunc

* fix collector registration

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

* change background ctx

* fix test randomness

* Add traces to support bundle collector

* Remove unecessay span

* Add trace to usagestats api

* Close spans

* Mv trace to bundle

* Change span name

* use parent context

* fix runtime declare of stats

* Fix pointer dereference problem on usage stat func

Co-authored-by: Karl Persson <kalle.persson@grafana.com>
Co-authored-by: jguer <joao.guerreiro@grafana.com>

* fix broken support bundle tests by tracer

---------

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
Co-authored-by: gamab <gabriel.mabille@grafana.com>
Co-authored-by: Karl Persson <kalle.persson@grafana.com>
2024-03-11 15:18:42 +01:00

411 lines
12 KiB
Go

package statscollector
import (
"context"
"fmt"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/db/dbtest"
"github.com/grafana/grafana/pkg/infra/httpclient"
"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/pluginsintegration/pluginstore"
"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 := dbtest.NewFakeDB()
statsService := statstest.NewFakeService()
s := createService(t, setting.NewCfg(), sqlStore, statsService)
s.cfg.MetricsEndpointEnabled = true
s.cfg.MetricsEndpointDisableTotalStats = false
statsService.ExpectedSystemStats = &stats.SystemStats{}
tests := []struct {
MetricsEndpointEnabled bool
MetricsEndpointDisableTotalStats bool
ExpectedUpdate bool
}{
{
MetricsEndpointEnabled: false,
MetricsEndpointDisableTotalStats: false,
ExpectedUpdate: false,
},
{
MetricsEndpointEnabled: false,
MetricsEndpointDisableTotalStats: true,
ExpectedUpdate: false,
},
{
MetricsEndpointEnabled: true,
MetricsEndpointDisableTotalStats: true,
ExpectedUpdate: false,
},
{
MetricsEndpointEnabled: true,
MetricsEndpointDisableTotalStats: false,
ExpectedUpdate: true,
},
}
for _, tc := range tests {
tc := tc
t.Run(fmt.Sprintf(
"metricsEnabled(%v) * totalStatsDisabled(%v) = %v",
tc.MetricsEndpointEnabled,
tc.MetricsEndpointDisableTotalStats,
tc.ExpectedUpdate,
), func(t *testing.T) {
s.cfg.MetricsEndpointEnabled = tc.MetricsEndpointEnabled
s.cfg.MetricsEndpointDisableTotalStats = tc.MetricsEndpointDisableTotalStats
assert.Equal(t, tc.ExpectedUpdate, s.updateTotalStats(context.Background()))
})
}
}
var _ registry.ProvidesUsageStats = (*dummyUsageStatProvider)(nil)
type dummyUsageStatProvider struct {
stats map[string]any
}
func (d dummyUsageStatProvider) GetUsageStats(ctx context.Context) map[string]any {
return d.stats
}
func TestUsageStatsProviders(t *testing.T) {
provider := &dummyUsageStatProvider{stats: map[string]any{"my_stat_x": "valx", "my_stat_z": "valz"}}
store := dbtest.NewFakeDB()
statsService := statstest.NewFakeService()
mockSystemStats(statsService)
s := createService(t, setting.NewCfg(), store, statsService)
s.RegisterProviders([]registry.ProvidesUsageStats{provider})
report, err := s.usageStats.GetUsageReport(context.Background())
require.NoError(t, err, "Expected no error")
assert.Equal(t, "valx", report.Metrics["my_stat_x"])
assert.Equal(t, "valz", report.Metrics["my_stat_z"])
}
func TestFeatureUsageStats(t *testing.T) {
store := dbtest.NewFakeDB()
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")
assert.Equal(t, 1, m["stats.features.feature_1.count"])
assert.Equal(t, 1, m["stats.features.feature_2.count"])
}
func TestCollectingUsageStats(t *testing.T) {
sqlStore := dbtest.NewFakeDB()
statsService := statstest.NewFakeService()
expectedDataSources := []*datasources.DataSource{
{
JsonData: simplejson.NewFromAny(map[string]any{}),
},
{
JsonData: simplejson.NewFromAny(map[string]any{}),
},
{
JsonData: simplejson.NewFromAny(map[string]any{}),
},
}
s := createService(t, &setting.Cfg{
ReportingEnabled: true,
BuildVersion: "5.0.0",
AnonymousEnabled: true,
BasicAuthEnabled: true,
LDAPAuthEnabled: true,
AuthProxy: setting.AuthProxySettings{Enabled: true},
Packaging: "deb",
ReportingDistributor: "hosted-grafana",
RemoteCacheOptions: &setting.RemoteCacheOptions{
Name: "database",
},
}, sqlStore, statsService,
withDatasources(mockDatasourceService{datasources: expectedDataSources}))
s.startTime = time.Now().Add(-1 * time.Minute)
mockSystemStats(statsService)
createConcurrentTokens(t, sqlStore)
metrics, err := s.collectSystemStats(context.Background())
require.NoError(t, err)
assert.EqualValues(t, 15, metrics["stats.total_auth_token.count"])
assert.EqualValues(t, 2, metrics["stats.api_keys.count"])
assert.EqualValues(t, 5, metrics["stats.avg_auth_token_per_user.count"])
assert.EqualValues(t, 16, metrics["stats.dashboard_versions.count"])
assert.EqualValues(t, 17, metrics["stats.annotations.count"])
assert.EqualValues(t, 18, metrics["stats.alert_rules.count"])
assert.EqualValues(t, 19, metrics["stats.library_panels.count"])
assert.EqualValues(t, 20, metrics["stats.library_variables.count"])
assert.EqualValues(t, 1, metrics["stats.packaging.deb.count"])
assert.EqualValues(t, 1, metrics["stats.distributor.hosted-grafana.count"])
assert.EqualValues(t, 11, metrics["stats.data_keys.count"])
assert.EqualValues(t, 3, metrics["stats.active_data_keys.count"])
assert.EqualValues(t, 5, metrics["stats.public_dashboards.count"])
assert.EqualValues(t, 3, metrics["stats.correlations.count"])
assert.InDelta(t, int64(65), metrics["stats.uptime"], 6)
}
func TestDatasourceStats(t *testing.T) {
sqlStore := dbtest.NewFakeDB()
statsService := statstest.NewFakeService()
s := createService(t, &setting.Cfg{}, sqlStore, statsService)
setupSomeDataSourcePlugins(t, s)
statsService.ExpectedDataSourceStats = []*stats.DataSourceStats{
{
Type: datasources.DS_ES,
Count: 9,
},
{
Type: datasources.DS_PROMETHEUS,
Count: 10,
},
{
Type: "unknown_ds",
Count: 11,
},
{
Type: "unknown_ds2",
Count: 12,
},
}
statsService.ExpectedDataSourcesAccessStats = []*stats.DataSourceAccessStats{
{
Type: datasources.DS_ES,
Access: "direct",
Count: 1,
},
{
Type: datasources.DS_ES,
Access: "proxy",
Count: 2,
},
{
Type: datasources.DS_PROMETHEUS,
Access: "proxy",
Count: 3,
},
{
Type: "unknown_ds",
Access: "proxy",
Count: 4,
},
{
Type: "unknown_ds2",
Access: "",
Count: 5,
},
{
Type: "unknown_ds3",
Access: "direct",
Count: 6,
},
{
Type: "unknown_ds4",
Access: "direct",
Count: 7,
},
{
Type: "unknown_ds5",
Access: "proxy",
Count: 8,
},
}
{
db, err := s.collectDatasourceStats(context.Background())
require.NoError(t, err)
assert.EqualValues(t, 9, db["stats.ds."+datasources.DS_ES+".count"])
assert.EqualValues(t, 10, db["stats.ds."+datasources.DS_PROMETHEUS+".count"])
assert.EqualValues(t, 11+12, db["stats.ds.other.count"])
}
{
dba, err := s.collectDatasourceAccess(context.Background())
require.NoError(t, err)
assert.EqualValues(t, 1, dba["stats.ds_access."+datasources.DS_ES+".direct.count"])
assert.EqualValues(t, 2, dba["stats.ds_access."+datasources.DS_ES+".proxy.count"])
assert.EqualValues(t, 3, dba["stats.ds_access."+datasources.DS_PROMETHEUS+".proxy.count"])
assert.EqualValues(t, 6+7, dba["stats.ds_access.other.direct.count"])
assert.EqualValues(t, 4+8, dba["stats.ds_access.other.proxy.count"])
}
}
func TestAlertNotifiersStats(t *testing.T) {
sqlStore := dbtest.NewFakeDB()
statsService := statstest.NewFakeService()
s := createService(t, &setting.Cfg{}, sqlStore, statsService)
statsService.ExpectedNotifierUsageStats = []*stats.NotifierUsageStats{
{
Type: "slack",
Count: 1,
},
{
Type: "webhook",
Count: 2,
},
}
metrics, err := s.collectAlertNotifierStats(context.Background())
require.NoError(t, err)
assert.EqualValues(t, 1, metrics["stats.alert_notifiers.slack.count"])
assert.EqualValues(t, 2, metrics["stats.alert_notifiers.webhook.count"])
}
func mockSystemStats(statsService *statstest.FakeService) {
statsService.ExpectedSystemStats = &stats.SystemStats{
Dashboards: 1,
Datasources: 2,
Users: 3,
Admins: 31,
Editors: 32,
Viewers: 33,
ActiveUsers: 4,
ActiveAdmins: 21,
ActiveEditors: 22,
ActiveViewers: 23,
ActiveSessions: 24,
DailyActiveUsers: 25,
DailyActiveAdmins: 26,
DailyActiveEditors: 27,
DailyActiveViewers: 28,
DailyActiveSessions: 29,
Orgs: 5,
Playlists: 6,
Alerts: 7,
Stars: 8,
Folders: 9,
DashboardPermissions: 10,
FolderPermissions: 11,
ProvisionedDashboards: 12,
Snapshots: 13,
Teams: 14,
AuthTokens: 15,
DashboardVersions: 16,
Annotations: 17,
AlertRules: 18,
LibraryPanels: 19,
LibraryVariables: 20,
DashboardsViewersCanAdmin: 3,
DashboardsViewersCanEdit: 2,
FoldersViewersCanAdmin: 1,
FoldersViewersCanEdit: 5,
APIKeys: 2,
DataKeys: 11,
ActiveDataKeys: 3,
PublicDashboards: 5,
Correlations: 3,
}
}
type mockSocial struct {
social.Service
OAuthProviders map[string]bool
}
func (m *mockSocial) GetOAuthProviders() map[string]bool {
return m.OAuthProviders
}
func setupSomeDataSourcePlugins(t *testing.T, s *Service) {
t.Helper()
s.plugins = &pluginstore.FakePluginStore{
PluginList: []pluginstore.Plugin{
{JSONData: plugins.JSONData{ID: datasources.DS_ES}, Signature: "internal"},
{JSONData: plugins.JSONData{ID: datasources.DS_PROMETHEUS}, Signature: "internal"},
{JSONData: plugins.JSONData{ID: datasources.DS_GRAPHITE}, Signature: "internal"},
{JSONData: plugins.JSONData{ID: datasources.DS_MYSQL}, Signature: "internal"},
},
}
}
func createService(t testing.TB, cfg *setting.Cfg, store db.DB, statsService stats.Service, opts ...func(*serviceOptions)) *Service {
t.Helper()
o := &serviceOptions{datasources: mockDatasourceService{}}
for _, opt := range opts {
opt(o)
}
return ProvideService(
&usagestats.UsageStatsMock{},
&validator.FakeUsageStatsValidator{},
statsService,
cfg,
store,
&mockSocial{},
&pluginstore.FakePluginStore{},
featuremgmt.WithManager("feature1", "feature2"),
o.datasources,
httpclient.NewProvider(sdkhttpclient.ProviderOptions{Middlewares: []sdkhttpclient.Middleware{}}),
)
}
type serviceOptions struct {
datasources datasources.DataSourceService
}
func withDatasources(ds datasources.DataSourceService) func(*serviceOptions) {
return func(options *serviceOptions) {
options.datasources = ds
}
}
type mockDatasourceService struct {
datasources.DataSourceService
datasources []*datasources.DataSource
}
func (s mockDatasourceService) GetDataSourcesByType(ctx context.Context, query *datasources.GetDataSourcesByTypeQuery) ([]*datasources.DataSource, error) {
return s.datasources, nil
}
func (s mockDatasourceService) GetHTTPTransport(ctx context.Context, ds *datasources.DataSource, provider httpclient.Provider, customMiddlewares ...sdkhttpclient.Middleware) (http.RoundTripper, error) {
return provider.GetTransport()
}