Chore: Add support bundle for folders (#83360)

* add support bundle for folders

* fix ProvideService in tests

* add a test for collector
This commit is contained in:
Serge Zaitsev
2024-02-26 11:27:22 +01:00
committed by GitHub
parent ae00b4fb53
commit d0679f0993
14 changed files with 163 additions and 18 deletions
+69 -1
View File
@@ -2,6 +2,7 @@ package folderimpl
import (
"context"
"encoding/json"
"errors"
"fmt"
"runtime"
@@ -27,6 +28,8 @@ import (
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/store/entity"
"github.com/grafana/grafana/pkg/services/supportbundles"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@@ -40,7 +43,6 @@ type Service struct {
dashboardFolderStore folder.FolderStore
features featuremgmt.FeatureToggles
accessControl accesscontrol.AccessControl
// bus is currently used to publish event in case of title change
bus bus.Bus
@@ -57,6 +59,7 @@ func ProvideService(
folderStore folder.FolderStore,
db db.DB, // DB for the (new) nested folder store
features featuremgmt.FeatureToggles,
supportBundles supportbundles.Service,
r prometheus.Registerer,
) folder.Service {
store := ProvideStore(db, cfg)
@@ -75,6 +78,8 @@ func ProvideService(
}
srv.DBMigration(db)
supportBundles.RegisterSupportItemCollector(srv.supportBundleCollector())
ac.RegisterScopeAttributeResolver(dashboards.NewFolderNameScopeResolver(folderStore, srv))
ac.RegisterScopeAttributeResolver(dashboards.NewFolderIDScopeResolver(folderStore, srv))
ac.RegisterScopeAttributeResolver(dashboards.NewFolderUIDScopeResolver(srv))
@@ -1181,3 +1186,66 @@ func (s *Service) RegisterService(r folder.RegistryService) error {
return nil
}
func (s *Service) supportBundleCollector() supportbundles.Collector {
collector := supportbundles.Collector{
UID: "folder-stats",
DisplayName: "Folder information",
Description: "Folder information for the Grafana instance",
IncludedByDefault: false,
Default: true,
Fn: func(ctx context.Context) (*supportbundles.SupportItem, error) {
s.log.Info("Generating folder support bundle")
folders, err := s.GetFolders(ctx, folder.GetFoldersQuery{
OrgID: 0,
SignedInUser: &user.SignedInUser{
Login: "sa-supportbundle",
OrgRole: "Admin",
IsGrafanaAdmin: true,
IsServiceAccount: true,
Permissions: map[int64]map[string][]string{accesscontrol.GlobalOrgID: {dashboards.ActionFoldersRead: {dashboards.ScopeFoldersAll}}},
},
})
if err != nil {
return nil, err
}
return s.supportItemFromFolders(folders)
},
}
return collector
}
func (s *Service) supportItemFromFolders(folders []*folder.Folder) (*supportbundles.SupportItem, error) {
stats := struct {
Total int `json:"total"` // how many folders?
Depths map[int]int `json:"depths"` // how deep they are?
Children map[int]int `json:"children"` // how many child folders they have?
Folders []*folder.Folder `json:"folders"` // what are they?
}{Total: len(folders), Folders: folders, Children: map[int]int{}, Depths: map[int]int{}}
// Build parent-child mapping
parents := map[string]string{}
children := map[string][]string{}
for _, f := range folders {
parents[f.UID] = f.ParentUID
children[f.ParentUID] = append(children[f.ParentUID], f.UID)
}
// Find depths of each folder
for _, f := range folders {
depth := 0
for uid := f.UID; uid != ""; uid = parents[uid] {
depth++
}
stats.Depths[depth] += 1
stats.Children[len(children[f.UID])] += 1
}
b, err := json.MarshalIndent(stats, "", " ")
if err != nil {
return nil, err
}
return &supportbundles.SupportItem{
Filename: "folders.json",
FileBytes: b,
}, nil
}
+67 -1
View File
@@ -2,6 +2,7 @@ package folderimpl
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/rand"
@@ -40,6 +41,7 @@ import (
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/store/entity"
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
@@ -57,7 +59,7 @@ func TestIntegrationProvideFolderService(t *testing.T) {
cfg := setting.NewCfg()
ac := acmock.New()
db := sqlstore.InitTestDB(t)
ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, nil, nil, db, &featuremgmt.FeatureManager{}, nil)
ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, nil, nil, db, &featuremgmt.FeatureManager{}, supportbundlestest.NewFakeBundleService(), nil)
require.Len(t, ac.Calls.RegisterAttributeScopeResolver, 3)
})
@@ -1798,6 +1800,70 @@ func TestFolderServiceGetFolders(t *testing.T) {
})
}
func TestSupportBundle(t *testing.T) {
f := func(uid, parent string) *folder.Folder { return &folder.Folder{UID: uid, ParentUID: parent} }
for _, tc := range []struct {
Folders []*folder.Folder
ExpectedTotal int
ExpectedDepths map[int]int
ExpectedChildren map[int]int
}{
// Empty folder list
{
Folders: []*folder.Folder{},
ExpectedTotal: 0,
ExpectedDepths: map[int]int{},
ExpectedChildren: map[int]int{},
},
// Single folder
{
Folders: []*folder.Folder{f("a", "")},
ExpectedTotal: 1,
ExpectedDepths: map[int]int{1: 1},
ExpectedChildren: map[int]int{0: 1},
},
// Flat folders
{
Folders: []*folder.Folder{f("a", ""), f("b", ""), f("c", "")},
ExpectedTotal: 3,
ExpectedDepths: map[int]int{1: 3},
ExpectedChildren: map[int]int{0: 3},
},
// Nested folders
{
Folders: []*folder.Folder{f("a", ""), f("ab", "a"), f("ac", "a"), f("x", ""), f("xy", "x"), f("xyz", "xy")},
ExpectedTotal: 6,
ExpectedDepths: map[int]int{1: 2, 2: 3, 3: 1},
ExpectedChildren: map[int]int{0: 3, 1: 2, 2: 1},
},
} {
svc := &Service{}
supportItem, err := svc.supportItemFromFolders(tc.Folders)
if err != nil {
t.Fatal(err)
}
stats := struct {
Total int `json:"total"`
Depths map[int]int `json:"depths"`
Children map[int]int `json:"children"`
}{}
if err := json.Unmarshal(supportItem.FileBytes, &stats); err != nil {
t.Fatal(err)
}
if stats.Total != tc.ExpectedTotal {
t.Error("Total mismatch", stats, tc)
}
if fmt.Sprint(stats.Depths) != fmt.Sprint(tc.ExpectedDepths) {
t.Error("Depths mismatch", stats, tc.ExpectedDepths)
}
if fmt.Sprint(stats.Children) != fmt.Sprint(tc.ExpectedChildren) {
t.Error("Depths mismatch", stats, tc.ExpectedChildren)
}
}
}
func CreateSubtreeInStore(t *testing.T, store store, service *Service, depth int, prefix string, cmd folder.CreateFolderCommand) []*folder.Folder {
t.Helper()
+5 -2
View File
@@ -464,8 +464,11 @@ func (ss *sqlStore) GetFolders(ctx context.Context, q getFoldersQuery) ([]*folde
s.WriteString(getFullpathJoinsSQL())
}
// covered by UQE_folder_org_id_uid
s.WriteString(` WHERE f0.org_id=?`)
args := []any{q.OrgID}
args := []any{}
if q.OrgID > 0 {
s.WriteString(` WHERE f0.org_id=?`)
args = []any{q.OrgID}
}
if len(partialUIDs) > 0 {
s.WriteString(` AND f0.uid IN (?` + strings.Repeat(", ?", len(partialUIDs)-1) + `)`)
for _, uid := range partialUIDs {