-
Notifications
You must be signed in to change notification settings - Fork 29
observability-lib: pin Grafana panel IDs via PanelOptions.StableID #2241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
cawthorne
wants to merge
11
commits into
main
Choose a base branch
from
feat/grafana-stable-panel-ids
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
65ffba5
Add stable Grafana panel ID support to observability-lib builder.
cawthorne 5cda859
Assign Grafana panel IDs from PanelOptions.StableID at build time.
cawthorne 4ec92a3
Drop title-based panel ID pinning from observability-lib builder.
cawthorne b632a50
Simplify StableID assignment; drop within-dashboard duplicate tracking.
cawthorne 1632e6e
Skip auto-increment panel IDs already reserved by StableID.
cawthorne db7a6a4
Fix observability-lib lint and clarify stable panel ID behavior.
cawthorne fd737d6
Fix goimports formatting in PanelOptions struct.
cawthorne d485dbf
ci: re-trigger CodeQL and benchmark workflows
cawthorne 612fa8c
Reject duplicate panel IDs during dashboard Build().
cawthorne 240c419
Merge branch 'main' into feat/grafana-stable-panel-ids
cawthorne 9559c49
fix ci checks
cawthorne File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| package grafana | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
|
|
||
| "github.com/grafana/grafana-foundation-sdk/go/cog" | ||
| "github.com/grafana/grafana-foundation-sdk/go/dashboard" | ||
| ) | ||
|
|
||
| // ValidateStablePanelIDs checks that pinned titles are non-empty and panel IDs are unique. | ||
| func ValidateStablePanelIDs(byTitle map[string]uint32) error { | ||
| if len(byTitle) == 0 { | ||
| return nil | ||
| } | ||
| seen := make(map[uint32]string, len(byTitle)) | ||
| for title, id := range byTitle { | ||
| if title == "" { | ||
| return errors.New("stable panel ID map has empty title") | ||
| } | ||
| if id == 0 { | ||
| return fmt.Errorf("stable panel ID for %q must be non-zero", title) | ||
| } | ||
| if prev, ok := seen[id]; ok { | ||
| return fmt.Errorf("duplicate stable panel ID %d for titles %q and %q", id, prev, title) | ||
| } | ||
| seen[id] = title | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // ApplyStablePanelIDs assigns fixed Grafana panel IDs to panels matched by title. | ||
| // Walks top-level panels and panels nested inside row panels. | ||
| func ApplyStablePanelIDs(db *dashboard.Dashboard, byTitle map[string]uint32) error { | ||
| if db == nil || len(byTitle) == 0 { | ||
| return nil | ||
| } | ||
| if err := ValidateStablePanelIDs(byTitle); err != nil { | ||
| return err | ||
| } | ||
| for i := range db.Panels { | ||
| applyStablePanelIDToOrRow(&db.Panels[i], byTitle) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func applyStablePanelIDToOrRow(item *dashboard.PanelOrRowPanel, byTitle map[string]uint32) { | ||
| if item.Panel != nil { | ||
| patchStablePanelID(item.Panel, byTitle) | ||
| } | ||
| if item.RowPanel != nil { | ||
| for j := range item.RowPanel.Panels { | ||
| patchStablePanelID(&item.RowPanel.Panels[j], byTitle) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func patchStablePanelID(panel *dashboard.Panel, byTitle map[string]uint32) { | ||
| if panel == nil || panel.Title == nil { | ||
| return | ||
| } | ||
| if id, ok := byTitle[*panel.Title]; ok { | ||
| panel.Id = cog.ToPtr(id) | ||
| } | ||
| } | ||
|
|
||
| // PanelIDByTitle returns the panel ID for a dashboard panel title, including row panels. | ||
| func PanelIDByTitle(db *dashboard.Dashboard, title string) (uint32, bool) { | ||
| if db == nil || title == "" { | ||
| return 0, false | ||
| } | ||
| var found uint32 | ||
| ok := false | ||
| foreachPanel(db, func(panel *dashboard.Panel) bool { | ||
| if panel.Title != nil && *panel.Title == title && panel.Id != nil { | ||
| found = *panel.Id | ||
| ok = true | ||
| return false | ||
| } | ||
| return true | ||
| }) | ||
| return found, ok | ||
| } | ||
|
|
||
| func foreachPanel(db *dashboard.Dashboard, fn func(panel *dashboard.Panel) bool) { | ||
| for i := range db.Panels { | ||
| if !applyToOrRowPanel(&db.Panels[i], fn) { | ||
| return | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func applyToOrRowPanel(item *dashboard.PanelOrRowPanel, fn func(panel *dashboard.Panel) bool) bool { | ||
| if item.Panel != nil { | ||
| if !fn(item.Panel) { | ||
| return false | ||
| } | ||
| } | ||
| if item.RowPanel != nil { | ||
| for j := range item.RowPanel.Panels { | ||
| if !fn(&item.RowPanel.Panels[j]) { | ||
| return false | ||
| } | ||
| } | ||
| } | ||
| return true | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| package grafana_test | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/smartcontractkit/chainlink-common/observability-lib/grafana" | ||
| ) | ||
|
|
||
| func TestValidateStablePanelIDs(t *testing.T) { | ||
| t.Run("accepts valid map", func(t *testing.T) { | ||
| err := grafana.ValidateStablePanelIDs(map[string]uint32{ | ||
| "Panel A": 20101, | ||
| "Panel B": 20102, | ||
| }) | ||
| require.NoError(t, err) | ||
| }) | ||
|
|
||
| t.Run("rejects duplicate IDs", func(t *testing.T) { | ||
| err := grafana.ValidateStablePanelIDs(map[string]uint32{ | ||
| "Panel A": 20101, | ||
| "Panel B": 20101, | ||
| }) | ||
| require.Error(t, err) | ||
| require.Contains(t, err.Error(), "duplicate stable panel ID") | ||
| }) | ||
|
|
||
| t.Run("rejects zero ID", func(t *testing.T) { | ||
| err := grafana.ValidateStablePanelIDs(map[string]uint32{ | ||
| "Panel A": 0, | ||
| }) | ||
| require.Error(t, err) | ||
| require.Contains(t, err.Error(), "must be non-zero") | ||
| }) | ||
| } | ||
|
|
||
| func TestApplyStablePanelIDs(t *testing.T) { | ||
| builder := grafana.NewBuilder(&grafana.BuilderOptions{Name: "Stable IDs"}) | ||
| builder.AddRow("Metrics") | ||
| builder.AddPanelToRow("Metrics", | ||
| grafana.NewStatPanel(&grafana.StatPanelOptions{ | ||
| PanelOptions: &grafana.PanelOptions{Title: grafana.Pointer("Pinned Panel")}, | ||
| }), | ||
| grafana.NewStatPanel(&grafana.StatPanelOptions{ | ||
| PanelOptions: &grafana.PanelOptions{Title: grafana.Pointer("Auto Panel")}, | ||
| }), | ||
| ) | ||
| builder.AddPanel(grafana.NewStatPanel(&grafana.StatPanelOptions{ | ||
| PanelOptions: &grafana.PanelOptions{Title: grafana.Pointer("Top Level Pinned")}, | ||
| })) | ||
|
|
||
| o, err := builder.Build() | ||
| require.NoError(t, err) | ||
|
|
||
| err = grafana.ApplyStablePanelIDs(o.Dashboard, map[string]uint32{ | ||
| "Pinned Panel": 20105, | ||
| "Top Level Pinned": 20110, | ||
| }) | ||
| require.NoError(t, err) | ||
|
|
||
| id, ok := grafana.PanelIDByTitle(o.Dashboard, "Pinned Panel") | ||
| require.True(t, ok) | ||
| require.Equal(t, uint32(20105), id) | ||
|
|
||
| id, ok = grafana.PanelIDByTitle(o.Dashboard, "Top Level Pinned") | ||
| require.True(t, ok) | ||
| require.Equal(t, uint32(20110), id) | ||
|
|
||
| id, ok = grafana.PanelIDByTitle(o.Dashboard, "Auto Panel") | ||
| require.True(t, ok) | ||
| require.NotEqual(t, uint32(20105), id) | ||
| require.NotEqual(t, uint32(20110), id) | ||
| } | ||
|
|
||
| func TestBuilderPanelOptionsStableID(t *testing.T) { | ||
| builder := grafana.NewBuilder(&grafana.BuilderOptions{Name: "PanelOptions StableID"}) | ||
| builder.AddRow("Row") | ||
| builder.AddPanelToRow("Row", grafana.NewStatPanel(&grafana.StatPanelOptions{ | ||
| PanelOptions: &grafana.PanelOptions{ | ||
| Title: grafana.Pointer("Inside Row"), | ||
| StableID: 20127, | ||
| }, | ||
| })) | ||
| builder.AddPanel(grafana.NewStatPanel(&grafana.StatPanelOptions{ | ||
| PanelOptions: &grafana.PanelOptions{Title: grafana.Pointer("Auto Panel")}, | ||
| })) | ||
|
|
||
| o, err := builder.Build() | ||
| require.NoError(t, err) | ||
|
|
||
| id, ok := grafana.PanelIDByTitle(o.Dashboard, "Inside Row") | ||
| require.True(t, ok) | ||
| require.Equal(t, uint32(20127), id) | ||
|
|
||
| id, ok = grafana.PanelIDByTitle(o.Dashboard, "Auto Panel") | ||
| require.True(t, ok) | ||
| require.NotEqual(t, uint32(20127), id) | ||
| } | ||
|
|
||
| func TestBuilderPanelOptionsStableIDDuplicate(t *testing.T) { | ||
| builder := grafana.NewBuilder(&grafana.BuilderOptions{Name: "Duplicate StableID"}) | ||
| builder.AddPanel( | ||
| grafana.NewStatPanel(&grafana.StatPanelOptions{ | ||
| PanelOptions: &grafana.PanelOptions{Title: grafana.Pointer("A"), StableID: 20101}, | ||
| }), | ||
| grafana.NewStatPanel(&grafana.StatPanelOptions{ | ||
| PanelOptions: &grafana.PanelOptions{Title: grafana.Pointer("B"), StableID: 20101}, | ||
| }), | ||
| ) | ||
|
|
||
| _, err := builder.Build() | ||
| require.Error(t, err) | ||
| require.Contains(t, err.Error(), "duplicate StableID") | ||
| } | ||
|
|
||
| func TestBuilderWithStablePanelIDs(t *testing.T) { | ||
| builder := grafana.NewBuilder(&grafana.BuilderOptions{Name: "Builder Stable IDs"}) | ||
| builder.WithStablePanelIDs(map[string]uint32{ | ||
| "Inside Row": 20127, | ||
| }) | ||
| builder.AddRow("Row") | ||
| builder.AddPanelToRow("Row", grafana.NewStatPanel(&grafana.StatPanelOptions{ | ||
| PanelOptions: &grafana.PanelOptions{Title: grafana.Pointer("Inside Row")}, | ||
| })) | ||
|
|
||
| o, err := builder.Build() | ||
| require.NoError(t, err) | ||
|
|
||
| id, ok := grafana.PanelIDByTitle(o.Dashboard, "Inside Row") | ||
| require.True(t, ok) | ||
| require.Equal(t, uint32(20127), id) | ||
| } | ||
|
|
||
| func TestBuilderWithStablePanelIDsInvalidMap(t *testing.T) { | ||
| builder := grafana.NewBuilder(&grafana.BuilderOptions{Name: "Invalid Stable IDs"}) | ||
| builder.WithStablePanelIDs(map[string]uint32{ | ||
| "A": 1, | ||
| "B": 1, | ||
| }) | ||
| builder.AddPanel(grafana.NewStatPanel(&grafana.StatPanelOptions{ | ||
| PanelOptions: &grafana.PanelOptions{Title: grafana.Pointer("A")}, | ||
| })) | ||
|
|
||
| _, err := builder.Build() | ||
| require.Error(t, err) | ||
| require.Contains(t, err.Error(), "duplicate stable panel ID") | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.