Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 52 additions & 6 deletions observability-lib/grafana/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ type Builder struct {
contactPointsBuilder []*alerting.ContactPointBuilder
notificationPoliciesBuilder []*alerting.NotificationPolicyBuilder
panelCounter uint32
usedStableIDs map[uint32]struct{}
stablePanelIDs map[string]uint32
alertsTags map[string]string
rows map[string]*dashboard.RowBuilder
entries []buildEntry
Expand Down Expand Up @@ -141,18 +143,53 @@ func (b *Builder) AddNotificationPolicy(notificationPolicies ...*alerting.Notifi
b.notificationPoliciesBuilder = append(b.notificationPoliciesBuilder, notificationPolicies...)
}

// WithStablePanelIDs pins Grafana panel IDs by panel title after the dashboard is built.
// Prefer PanelOptions.StableID at panel definition time; this remains for legacy title-based pinning.
func (b *Builder) WithStablePanelIDs(byTitle map[string]uint32) *Builder {
if len(byTitle) == 0 {
b.stablePanelIDs = nil
return b
}
b.stablePanelIDs = maps.Clone(byTitle)
return b
}

func (b *Builder) assignPanelID(panel *Panel) (uint32, error) {
if panel == nil || panel.stableID == 0 {
return b.getPanelCounter(), nil
}
if b.usedStableIDs == nil {
b.usedStableIDs = make(map[uint32]struct{})
}
if _, dup := b.usedStableIDs[panel.stableID]; dup {
return 0, fmt.Errorf("duplicate StableID %d", panel.stableID)
}
b.usedStableIDs[panel.stableID] = struct{}{}
return panel.stableID, nil
}

// addPanelToBuilder assigns an ID and adds the panel to the dashboard builder.
func (b *Builder) addPanelToBuilder(item *Panel) {
if pb := item.panelBuilder(b.getPanelCounter()); pb != nil {
func (b *Builder) addPanelToBuilder(item *Panel) error {
id, err := b.assignPanelID(item)
if err != nil {
return err
}
if pb := item.panelBuilder(id); pb != nil {
b.dashboardBuilder.WithPanel(pb)
}
return nil
}

// addPanelToRow assigns an ID and adds the panel to a row builder.
func (b *Builder) addPanelToRow(row *dashboard.RowBuilder, item *Panel) {
if pb := item.panelBuilder(b.getPanelCounter()); pb != nil {
func (b *Builder) addPanelToRow(row *dashboard.RowBuilder, item *Panel) error {
id, err := b.assignPanelID(item)
if err != nil {
return err
}
if pb := item.panelBuilder(id); pb != nil {
row.WithPanel(pb)
}
return nil
}

func (b *Builder) Build() (*Observability, error) {
Expand All @@ -175,7 +212,9 @@ func (b *Builder) Build() (*Observability, error) {
if !ok {
return nil, fmt.Errorf("AddPanelToRow references unknown row %q; call AddRow first", e.rowTitle)
}
b.addPanelToRow(row, e.panel)
if err := b.addPanelToRow(row, e.panel); err != nil {
return nil, err
}
}
}

Expand All @@ -187,7 +226,9 @@ func (b *Builder) Build() (*Observability, error) {
b.dashboardBuilder.WithRow(row)
}
case entryPanel:
b.addPanelToBuilder(e.panel)
if err := b.addPanelToBuilder(e.panel); err != nil {
return nil, err
}
default:
continue
}
Expand All @@ -198,6 +239,11 @@ func (b *Builder) Build() (*Observability, error) {
return nil, errBuildDashboard
}
observability.Dashboard = &db
if len(b.stablePanelIDs) > 0 {
if err := ApplyStablePanelIDs(observability.Dashboard, b.stablePanelIDs); err != nil {
return nil, err
}
}
}

var alerts []alerting.Rule
Expand Down
7 changes: 2 additions & 5 deletions observability-lib/grafana/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,12 +275,9 @@
}

func panelIDByTitle(db *dashboard.Dashboard, title string) string {
for _, panel := range db.Panels {
if panel.Panel != nil && panel.Panel.Title != nil && *panel.Panel.Title == title {
return fmt.Sprintf("%d", *panel.Panel.Id)
}
if id, ok := PanelIDByTitle(db, title); ok {
return fmt.Sprintf("%d", id)

Check failure on line 279 in observability-lib/grafana/dashboard.go

View workflow job for this annotation

GitHub Actions / lint-module (observability-lib)

integer-format: fmt.Sprintf can be replaced with faster strconv.FormatUint (perfsprint)
}

return ""
}

Expand Down
107 changes: 107 additions & 0 deletions observability-lib/grafana/panel_ids.go
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) {
Comment thread
cawthorne marked this conversation as resolved.
Outdated
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
}
148 changes: 148 additions & 0 deletions observability-lib/grafana/panel_ids_test.go
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,

Check failure on line 57 in observability-lib/grafana/panel_ids_test.go

View workflow job for this annotation

GitHub Actions / lint-module (observability-lib)

File is not properly formatted (goimports)
"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")
}
Loading
Loading