From 89c70fcdcf725184ff0bec015301dbd2bd866de8 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Thu, 10 Apr 2025 10:51:00 +0200 Subject: [PATCH] Chore: Advisor stats (#103711) --- .../pkg/app/checks/datasourcecheck/check.go | 12 +- .../pkg/app/checks/plugincheck/check.go | 12 +- .../usagestats/statscollector/service.go | 18 +- .../usagestats/statscollector/service_test.go | 9 + .../pluginsintegration/advisor/advisor.go | 106 ++++++++++++ .../advisor/advisor_test.go | 163 ++++++++++++++++++ .../pluginsintegration/pluginsintegration.go | 3 + 7 files changed, 314 insertions(+), 9 deletions(-) create mode 100644 pkg/services/pluginsintegration/advisor/advisor.go create mode 100644 pkg/services/pluginsintegration/advisor/advisor_test.go diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/check.go b/apps/advisor/pkg/app/checks/datasourcecheck/check.go index 0fc42d1bfbd..edb37410299 100644 --- a/apps/advisor/pkg/app/checks/datasourcecheck/check.go +++ b/apps/advisor/pkg/app/checks/datasourcecheck/check.go @@ -16,6 +16,12 @@ import ( "github.com/grafana/grafana/pkg/util" ) +const ( + CheckID = "datasource" + HealthCheckStepID = "health-check" + UIDValidationStepID = "uid-validation" +) + type check struct { DatasourceSvc datasources.DataSourceService PluginStore pluginstore.Store @@ -52,7 +58,7 @@ func (c *check) Items(ctx context.Context) ([]any, error) { } func (c *check) ID() string { - return "datasource" + return CheckID } func (c *check) Steps() []checks.Step { @@ -69,7 +75,7 @@ func (c *check) Steps() []checks.Step { type uidValidationStep struct{} func (s *uidValidationStep) ID() string { - return "uid-validation" + return UIDValidationStepID } func (s *uidValidationStep) Title() string { @@ -122,7 +128,7 @@ func (s *healthCheckStep) Resolution() string { } func (s *healthCheckStep) ID() string { - return "health-check" + return HealthCheckStepID } func (s *healthCheckStep) Run(ctx context.Context, obj *advisor.CheckSpec, i any) (*advisor.CheckReportFailure, error) { diff --git a/apps/advisor/pkg/app/checks/plugincheck/check.go b/apps/advisor/pkg/app/checks/plugincheck/check.go index 3b08e0c5f6f..7bb2b958ee0 100644 --- a/apps/advisor/pkg/app/checks/plugincheck/check.go +++ b/apps/advisor/pkg/app/checks/plugincheck/check.go @@ -18,6 +18,12 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/provisionedplugins" ) +const ( + CheckID = "plugin" + DeprecationStepID = "deprecation" + UpdateStepID = "update" +) + func New( pluginStore pluginstore.Store, pluginRepo repo.Service, @@ -43,7 +49,7 @@ type check struct { } func (c *check) ID() string { - return "plugin" + return CheckID } func (c *check) Items(ctx context.Context) ([]any, error) { @@ -88,7 +94,7 @@ func (s *deprecationStep) Resolution() string { } func (s *deprecationStep) ID() string { - return "deprecation" + return DeprecationStepID } func (s *deprecationStep) Run(ctx context.Context, _ *advisor.CheckSpec, it any) (*advisor.CheckReportFailure, error) { @@ -146,7 +152,7 @@ func (s *updateStep) Resolution() string { } func (s *updateStep) ID() string { - return "update" + return UpdateStepID } func (s *updateStep) Run(ctx context.Context, _ *advisor.CheckSpec, i any) (*advisor.CheckReportFailure, error) { diff --git a/pkg/infra/usagestats/statscollector/service.go b/pkg/infra/usagestats/statscollector/service.go index 13ed55b238f..54dfc0af9e8 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/registry" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/pluginsintegration/advisor" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/services/pluginsintegration/sandbox" "github.com/grafana/grafana/pkg/services/stats" @@ -41,6 +42,7 @@ type Service struct { datasources datasources.DataSourceService httpClientProvider httpclient.Provider sandbox sandbox.Sandbox + advisor advisor.AdvisorStats log log.Logger @@ -61,6 +63,7 @@ func ProvideService( datasourceService datasources.DataSourceService, httpClientProvider httpclient.Provider, sandbox sandbox.Sandbox, + advisor advisor.AdvisorStats, ) *Service { s := &Service{ cfg: cfg, @@ -73,9 +76,9 @@ func ProvideService( datasources: datasourceService, httpClientProvider: httpClientProvider, sandbox: sandbox, - - startTime: time.Now(), - log: log.New("infra.usagestats.collector"), + advisor: advisor, + startTime: time.Now(), + log: log.New("infra.usagestats.collector"), } collectors := []usagestats.MetricsFunc{ @@ -215,6 +218,15 @@ func (s *Service) collectSystemStats(ctx context.Context) (map[string]any, error m["stats.uptime"] = int64(time.Since(s.startTime).Seconds()) + report, err := s.advisor.ReportSummary(ctx) + if err != nil { + s.log.Error("Failed to get advisor usage stats", "error", err) + } else { + m["stats.plugins.advisor.outdated_plugins"] = report.PluginsOutdated + m["stats.plugins.advisor.deprecated_plugins"] = report.PluginsDeprecated + m["stats.plugins.advisor.unhealthy_datasources"] = report.DatasourcesUnhealthy + } + featureUsageStats := s.features.GetUsageStats(ctx) for k, v := range featureUsageStats { m[k] = v diff --git a/pkg/infra/usagestats/statscollector/service_test.go b/pkg/infra/usagestats/statscollector/service_test.go index f8c4f2a5f7f..52fc30a576b 100644 --- a/pkg/infra/usagestats/statscollector/service_test.go +++ b/pkg/infra/usagestats/statscollector/service_test.go @@ -23,6 +23,7 @@ import ( "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/advisor" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/services/pluginsintegration/sandbox" "github.com/grafana/grafana/pkg/services/stats" @@ -353,6 +354,13 @@ func (m *mockSocial) GetOAuthProviders() map[string]bool { return m.OAuthProviders } +type mockAdvisor struct { +} + +func (m *mockAdvisor) ReportSummary(ctx context.Context) (*advisor.ReportInfo, error) { + return &advisor.ReportInfo{}, nil +} + func setupSomeDataSourcePlugins(t *testing.T, s *Service) { t.Helper() @@ -387,6 +395,7 @@ func createService(t testing.TB, cfg *setting.Cfg, store db.DB, statsService sta o.datasources, httpclient.NewProvider(sdkhttpclient.ProviderOptions{Middlewares: []sdkhttpclient.Middleware{}}), sandbox.ProvideService(cfg), + &mockAdvisor{}, ) } diff --git a/pkg/services/pluginsintegration/advisor/advisor.go b/pkg/services/pluginsintegration/advisor/advisor.go new file mode 100644 index 00000000000..aceabd325ce --- /dev/null +++ b/pkg/services/pluginsintegration/advisor/advisor.go @@ -0,0 +1,106 @@ +package advisor + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/k8s" + "github.com/grafana/grafana-app-sdk/resource" + advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks/datasourcecheck" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks/plugincheck" + "github.com/grafana/grafana/pkg/services/apiserver" + apiserverrequest "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/setting" +) + +type AdvisorStats interface { + ReportSummary(ctx context.Context) (*ReportInfo, error) +} + +type Service struct { + cfg *setting.Cfg + namespace string + clientGenerator func(ctx context.Context) (resource.Client, error) +} + +func ProvideService( + cfg *setting.Cfg, + restConfigProvider apiserver.RestConfigProvider, +) (*Service, error) { + namespace := "default" + if cfg.StackID != "" { + namespace = apiserverrequest.GetNamespaceMapper(cfg)(1) + } + + return &Service{ + cfg: cfg, + namespace: namespace, + clientGenerator: func(ctx context.Context) (resource.Client, error) { + kubeConfig, err := restConfigProvider.GetRestConfig(ctx) + if err != nil { + return nil, err + } + clientGenerator := k8s.NewClientRegistry(*kubeConfig, k8s.ClientConfig{}) + return clientGenerator.ClientFor(advisorv0alpha1.CheckKind()) + }, + }, nil +} + +type ReportInfo struct { + PluginsOutdated int + PluginsDeprecated int + DatasourcesUnhealthy int +} + +func isMoreRecent(check1 resource.Object, check2 resource.Object) bool { + return check1.GetCommonMetadata().CreationTimestamp.After(check2.GetCommonMetadata().CreationTimestamp) +} + +// findLatestCheck returns the most recent check of the specified type from the list +func findLatestCheck(checkList []resource.Object, checkType string) *advisorv0alpha1.Check { + var latestCheck *advisorv0alpha1.Check + for _, check := range checkList { + currentCheckType := check.GetLabels()[checks.TypeLabel] + if currentCheckType != checkType { + continue + } + if latestCheck == nil || isMoreRecent(check, latestCheck) { + latestCheck = check.(*advisorv0alpha1.Check) + } + } + return latestCheck +} + +func (s *Service) ReportSummary(ctx context.Context) (*ReportInfo, error) { + client, err := s.clientGenerator(ctx) + if err != nil { + return nil, err + } + checkList, err := client.List(ctx, s.namespace, resource.ListOptions{}) + if err != nil { + return nil, err + } + + latestPluginCheck := findLatestCheck(checkList.GetItems(), plugincheck.CheckID) + latestDatasourceCheck := findLatestCheck(checkList.GetItems(), datasourcecheck.CheckID) + reportInfo := &ReportInfo{} + if latestPluginCheck != nil { + for _, failure := range latestPluginCheck.CheckStatus.Report.Failures { + if failure.StepID == plugincheck.UpdateStepID { + reportInfo.PluginsOutdated++ + } else if failure.StepID == plugincheck.DeprecationStepID { + reportInfo.PluginsDeprecated++ + } + } + } + if latestDatasourceCheck != nil { + for _, failure := range latestDatasourceCheck.CheckStatus.Report.Failures { + if failure.StepID == datasourcecheck.HealthCheckStepID { + reportInfo.DatasourcesUnhealthy++ + } + } + } + + return reportInfo, nil +} diff --git a/pkg/services/pluginsintegration/advisor/advisor_test.go b/pkg/services/pluginsintegration/advisor/advisor_test.go new file mode 100644 index 00000000000..c9c9b8d1609 --- /dev/null +++ b/pkg/services/pluginsintegration/advisor/advisor_test.go @@ -0,0 +1,163 @@ +package advisor + +import ( + "context" + "testing" + "time" + + "github.com/grafana/grafana-app-sdk/resource" + advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks/datasourcecheck" + "github.com/grafana/grafana/apps/advisor/pkg/app/checks/plugincheck" + "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestService_ReportSummary(t *testing.T) { + now := time.Now() + earlier := now.Add(-1 * time.Hour) + + tests := []struct { + name string + config *setting.Cfg + restConfigErr error + listItems []resource.Object + listErr error + expectedReport *ReportInfo + expectedErr error + }{ + { + name: "should return correct report with multiple checks", + config: &setting.Cfg{ + StackID: "test-stack", + }, + listItems: []resource.Object{ + &advisorv0alpha1.Check{ + ObjectMeta: metav1.ObjectMeta{ + CreationTimestamp: metav1.Time{Time: earlier}, + Labels: map[string]string{ + checks.TypeLabel: plugincheck.CheckID, + }, + }, + CheckStatus: advisorv0alpha1.CheckStatus{ + Report: advisorv0alpha1.CheckV0alpha1StatusReport{ + Failures: []advisorv0alpha1.CheckReportFailure{ + {StepID: plugincheck.UpdateStepID}, + }, + }, + }, + }, + &advisorv0alpha1.Check{ + ObjectMeta: metav1.ObjectMeta{ + CreationTimestamp: metav1.Time{Time: now}, + Labels: map[string]string{ + checks.TypeLabel: plugincheck.CheckID, + }, + }, + CheckStatus: advisorv0alpha1.CheckStatus{ + Report: advisorv0alpha1.CheckV0alpha1StatusReport{ + Failures: []advisorv0alpha1.CheckReportFailure{ + {StepID: plugincheck.UpdateStepID}, + {StepID: plugincheck.DeprecationStepID}, + }, + }, + }, + }, + &advisorv0alpha1.Check{ + ObjectMeta: metav1.ObjectMeta{ + CreationTimestamp: metav1.Time{Time: now}, + Labels: map[string]string{ + checks.TypeLabel: datasourcecheck.CheckID, + }, + }, + CheckStatus: advisorv0alpha1.CheckStatus{ + Report: advisorv0alpha1.CheckV0alpha1StatusReport{ + Failures: []advisorv0alpha1.CheckReportFailure{ + {StepID: datasourcecheck.HealthCheckStepID}, + {StepID: datasourcecheck.HealthCheckStepID}, + }, + }, + }, + }, + }, + expectedReport: &ReportInfo{ + PluginsOutdated: 1, + PluginsDeprecated: 1, + DatasourcesUnhealthy: 2, + }, + }, + { + name: "should handle empty check list", + config: &setting.Cfg{ + StackID: "test-stack", + }, + listItems: []resource.Object{}, + expectedReport: &ReportInfo{ + PluginsOutdated: 0, + PluginsDeprecated: 0, + DatasourcesUnhealthy: 0, + }, + }, + { + name: "should handle list error", + config: &setting.Cfg{ + StackID: "test-stack", + }, + listErr: assert.AnError, + expectedErr: assert.AnError, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup + client := &mockClient{ + listItems: tt.listItems, + listErr: tt.listErr, + } + + service := &Service{ + cfg: tt.config, + namespace: "stacks-0", + clientGenerator: func(ctx context.Context) (resource.Client, error) { return client, nil }, + } + + // Execute + report, err := service.ReportSummary(context.Background()) + + // Verify + if tt.expectedErr != nil { + assert.Error(t, err) + assert.Equal(t, tt.expectedErr, err) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.expectedReport, report) + }) + } +} + +type mockClient struct { + resource.Client + listItems []resource.Object + listErr error +} + +func (m *mockClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (resource.ListObject, error) { + if m.listErr != nil { + return nil, m.listErr + } + return &mockListObject{items: m.listItems}, nil +} + +type mockListObject struct { + resource.ListObject + items []resource.Object +} + +func (m *mockListObject) GetItems() []resource.Object { + return m.items +} diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index afe0ae6c5b4..8920e51551a 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -33,6 +33,7 @@ import ( "github.com/grafana/grafana/pkg/services/caching" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/oauthtoken" + "github.com/grafana/grafana/pkg/services/pluginsintegration/advisor" "github.com/grafana/grafana/pkg/services/pluginsintegration/angulardetectorsprovider" "github.com/grafana/grafana/pkg/services/pluginsintegration/angularinspector" "github.com/grafana/grafana/pkg/services/pluginsintegration/angularpatternsstore" @@ -129,6 +130,8 @@ var WireSet = wire.NewSet( pluginassets.ProvideService, plugininstaller.ProvidePreinstall, wire.Bind(new(plugininstaller.Preinstall), new(*plugininstaller.PreinstallImpl)), + advisor.ProvideService, + wire.Bind(new(advisor.AdvisorStats), new(*advisor.Service)), ) // WireExtensionSet provides a wire.ProviderSet of plugin providers that can be