Skip to content
Open
65 changes: 59 additions & 6 deletions observability-lib/grafana/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type Builder struct {
contactPointsBuilder []*alerting.ContactPointBuilder
notificationPoliciesBuilder []*alerting.NotificationPolicyBuilder
panelCounter uint32
usedPanelIDs map[uint32]struct{}
alertsTags map[string]string
rows map[string]*dashboard.RowBuilder
entries []buildEntry
Expand Down Expand Up @@ -141,18 +142,66 @@ func (b *Builder) AddNotificationPolicy(notificationPolicies ...*alerting.Notifi
b.notificationPoliciesBuilder = append(b.notificationPoliciesBuilder, notificationPolicies...)
}

func (b *Builder) reservePanelID(id uint32) error {
if b.usedPanelIDs == nil {
b.usedPanelIDs = make(map[uint32]struct{})
}
if _, taken := b.usedPanelIDs[id]; taken {
return fmt.Errorf("duplicate panel ID %d", id)
}
b.usedPanelIDs[id] = struct{}{}
return nil
}

func (b *Builder) nextAutoPanelID() (uint32, error) {
for {
candidate := b.getPanelCounter()
if b.usedPanelIDs != nil {
if _, taken := b.usedPanelIDs[candidate]; taken {
continue
}
}
if err := b.reservePanelID(candidate); err != nil {
return 0, err
}
return candidate, nil
}
}

// panelID returns a pinned StableID when set; otherwise the next auto-increment ID.
// Auto-increment skips IDs already reserved by StableID panels in this build.
func (b *Builder) panelID(panel *Panel) (uint32, error) {
if panel != nil && panel.stableID > 0 {
if err := b.reservePanelID(panel.stableID); err != nil {
return 0, err
}
return panel.stableID, nil
}
return b.nextAutoPanelID()
}

// 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.panelID(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.panelID(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 +224,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 +238,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 Down
8 changes: 3 additions & 5 deletions observability-lib/grafana/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"reflect"
"strconv"

"github.com/grafana/grafana-foundation-sdk/go/alerting"
"github.com/grafana/grafana-foundation-sdk/go/dashboard"
Expand Down Expand Up @@ -275,12 +276,9 @@ func (o *Observability) DeployToGrafana(options *DeployOptions) error {
}

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 strconv.FormatUint(uint64(id), 10)
}

return ""
}

Expand Down
49 changes: 49 additions & 0 deletions observability-lib/grafana/panel_ids.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package grafana

import (
"github.com/grafana/grafana-foundation-sdk/go/dashboard"
)

// PanelIDByTitle returns the panel ID for a dashboard panel matched by title.
// It searches top-level panels and panels nested inside row containers; row
// container panels themselves are not matched.
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
}
97 changes: 97 additions & 0 deletions observability-lib/grafana/panel_ids_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package grafana_test

import (
"strconv"
"testing"

"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink-common/observability-lib/grafana"
)

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")},
}))
builder.AddPanel(grafana.NewStatPanel(&grafana.StatPanelOptions{
PanelOptions: &grafana.PanelOptions{
Title: grafana.Pointer("Top Level Pinned"),
StableID: 20110,
},
}))

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, "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(20127), id)
require.NotEqual(t, uint32(20110), 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 panel ID 20101")
}

func TestBuilderAutoPanelIDSkipsStableID(t *testing.T) {
builder := grafana.NewBuilder(&grafana.BuilderOptions{Name: "Skip Stable ID"})
builder.AddPanel(grafana.NewStatPanel(&grafana.StatPanelOptions{
PanelOptions: &grafana.PanelOptions{
Title: grafana.Pointer("Pinned Low"),
StableID: 5,
},
}))
for i := 1; i <= 5; i++ {
builder.AddPanel(grafana.NewStatPanel(&grafana.StatPanelOptions{
PanelOptions: &grafana.PanelOptions{
Title: grafana.Pointer("Auto " + strconv.Itoa(i)),
},
}))
}

o, err := builder.Build()
require.NoError(t, err)

pinned, ok := grafana.PanelIDByTitle(o.Dashboard, "Pinned Low")
require.True(t, ok)
require.Equal(t, uint32(5), pinned)

for i := 1; i <= 5; i++ {
title := "Auto " + strconv.Itoa(i)
id, found := grafana.PanelIDByTitle(o.Dashboard, title)
require.True(t, found, "panel %q", title)
require.NotEqual(t, uint32(5), id, "auto panel %q should skip reserved stable id", title)
}

sixth, found := grafana.PanelIDByTitle(o.Dashboard, "Auto 5")
require.True(t, found)
require.Equal(t, uint32(6), sixth)
}
Loading
Loading