diff --git a/alert/alert.go b/alert/alert.go index 4f6e09f6..93797088 100644 --- a/alert/alert.go +++ b/alert/alert.go @@ -51,10 +51,11 @@ type Alert struct { Datasource string DashboardUID string PanelID string + PanelName string } // New creates a new alert. -func New(name string, options ...Option) *Alert { +func New(name, panelName string, options ...Option) *Alert { nope := false alert := &Alert{ @@ -88,6 +89,7 @@ func New(name string, options ...Option) *Alert { }, }, }, + PanelName: panelName, } for _, opt := range append(defaults(), options...) { diff --git a/alert/alert_test.go b/alert/alert_test.go index fd40b715..343f0ee2 100644 --- a/alert/alert_test.go +++ b/alert/alert_test.go @@ -9,8 +9,9 @@ import ( func TestNewAlertCanBeCreated(t *testing.T) { req := require.New(t) alertTitle := "some alert" + panelTitle := "some panel" - a := New(alertTitle) + a := New(alertTitle, panelTitle) req.Len(a.Builder.Rules, 1) @@ -25,6 +26,7 @@ func TestConditionsCanBeCombined(t *testing.T) { req := require.New(t) a := New( + "", "", IfOr(Avg, "A", IsBelow(10)), IfOr(Avg, "B", IsBelow(8)), @@ -36,7 +38,7 @@ func TestConditionsCanBeCombined(t *testing.T) { func TestPanelIDCanBeHooked(t *testing.T) { req := require.New(t) - a := New("") + a := New("", "") a.HookPanelID("id") @@ -46,7 +48,7 @@ func TestPanelIDCanBeHooked(t *testing.T) { func TestDashboardUIDCanBeHooked(t *testing.T) { req := require.New(t) - a := New("") + a := New("", "") a.HookDashboardUID("uid") @@ -57,7 +59,7 @@ func TestDatasourceUIDCanBeHooked(t *testing.T) { req := require.New(t) a := New( - "", + "", "", WithPrometheusQuery("A", "some prometheus query"), IfOr(Avg, "1", IsBelow(10)), ) @@ -87,7 +89,7 @@ func TestDatasourceUIDCanBeHooked(t *testing.T) { func TestSummaryCanBeSet(t *testing.T) { req := require.New(t) - a := New("", Summary("summary content")) + a := New("", "", Summary("summary content")) req.Equal("summary content", a.Builder.Rules[0].Annotations["summary"]) } @@ -95,7 +97,7 @@ func TestSummaryCanBeSet(t *testing.T) { func TestDescriptionCanBeSet(t *testing.T) { req := require.New(t) - a := New("", Description("description content")) + a := New("", "", Description("description content")) req.Equal("description content", a.Builder.Rules[0].Annotations["description"]) } @@ -103,7 +105,7 @@ func TestDescriptionCanBeSet(t *testing.T) { func TestRunbookCanBeSet(t *testing.T) { req := require.New(t) - a := New("", Runbook("runbook url")) + a := New("", "", Runbook("runbook url")) req.Equal("runbook url", a.Builder.Rules[0].Annotations["runbook_url"]) } @@ -111,7 +113,7 @@ func TestRunbookCanBeSet(t *testing.T) { func TestForIntervalCanBeSet(t *testing.T) { req := require.New(t) - a := New("", For("1m")) + a := New("", "", For("1m")) req.Equal("1m", a.Builder.Rules[0].For) } @@ -119,7 +121,7 @@ func TestForIntervalCanBeSet(t *testing.T) { func TestFrequencyCanBeSet(t *testing.T) { req := require.New(t) - a := New("", EvaluateEvery("1m")) + a := New("", "", EvaluateEvery("1m")) req.Equal("1m", a.Builder.Interval) } @@ -127,7 +129,7 @@ func TestFrequencyCanBeSet(t *testing.T) { func TestErrorModeCanBeSet(t *testing.T) { req := require.New(t) - a := New("", OnExecutionError(ErrorKO)) + a := New("", "", OnExecutionError(ErrorKO)) req.Equal(string(ErrorKO), a.Builder.Rules[0].GrafanaAlert.ExecutionErrorState) } @@ -135,7 +137,7 @@ func TestErrorModeCanBeSet(t *testing.T) { func TestNoDataModeCanBeSet(t *testing.T) { req := require.New(t) - a := New("", OnNoData(NoDataAlerting)) + a := New("", "", OnNoData(NoDataAlerting)) req.Equal(string(NoDataAlerting), a.Builder.Rules[0].GrafanaAlert.NoDataState) } @@ -143,7 +145,7 @@ func TestNoDataModeCanBeSet(t *testing.T) { func TestTagsCanBeSet(t *testing.T) { req := require.New(t) - a := New("", Tags(map[string]string{ + a := New("", "", Tags(map[string]string{ "severity": "warning", })) @@ -154,7 +156,7 @@ func TestTagsCanBeSet(t *testing.T) { func TestConditionsCanBeSet(t *testing.T) { req := require.New(t) - a := New("", If(Avg, "1", IsBelow(10))) + a := New("", "", If(Avg, "1", IsBelow(10))) req.Len(a.Builder.Rules[0].GrafanaAlert.Data, 1) } @@ -162,7 +164,7 @@ func TestConditionsCanBeSet(t *testing.T) { func TestOrConditionsCanBeSet(t *testing.T) { req := require.New(t) - a := New("", IfOr(Avg, "1", IsBelow(10))) + a := New("", "", IfOr(Avg, "1", IsBelow(10))) req.Len(a.Builder.Rules[0].GrafanaAlert.Data, 1) } diff --git a/alert/queries_test.go b/alert/queries_test.go index fb64083f..c893c3b1 100644 --- a/alert/queries_test.go +++ b/alert/queries_test.go @@ -10,7 +10,7 @@ import ( func TestPrometheusQueriesCanBeAdded(t *testing.T) { req := require.New(t) - a := New("", WithPrometheusQuery("A", "some prometheus query")) + a := New("", "", WithPrometheusQuery("A", "some prometheus query")) req.Len(a.Builder.Rules[0].GrafanaAlert.Data, 2) } @@ -18,7 +18,7 @@ func TestPrometheusQueriesCanBeAdded(t *testing.T) { func TestGraphiteQueriesCanBeAdded(t *testing.T) { req := require.New(t) - a := New("", WithGraphiteQuery("A", "some graphite query")) + a := New("", "", WithGraphiteQuery("A", "some graphite query")) req.Len(a.Builder.Rules[0].GrafanaAlert.Data, 2) } @@ -26,7 +26,7 @@ func TestGraphiteQueriesCanBeAdded(t *testing.T) { func TestLokiQueriesCanBeAdded(t *testing.T) { req := require.New(t) - a := New("", WithLokiQuery("A", "some loki query")) + a := New("", "", WithLokiQuery("A", "some loki query")) req.Len(a.Builder.Rules[0].GrafanaAlert.Data, 2) } @@ -34,7 +34,7 @@ func TestLokiQueriesCanBeAdded(t *testing.T) { func TestStackdriverQueriesCanBeAdded(t *testing.T) { req := require.New(t) - a := New("", WithStackdriverQuery(stackdriver.Gauge("A", "cloudsql.googleapis.com/database/cpu/utilization"))) + a := New("", "", WithStackdriverQuery(stackdriver.Gauge("A", "cloudsql.googleapis.com/database/cpu/utilization"))) req.Len(a.Builder.Rules[0].GrafanaAlert.Data, 2) } @@ -42,7 +42,7 @@ func TestStackdriverQueriesCanBeAdded(t *testing.T) { func TestInfluxDBQueriesCanBeAdded(t *testing.T) { req := require.New(t) - a := New("", WithInfluxDBQuery("A", "some influxdb query")) + a := New("", "", WithInfluxDBQuery("A", "some influxdb query")) req.Len(a.Builder.Rules[0].GrafanaAlert.Data, 2) } diff --git a/alerts.go b/alerts.go index 99c73c37..a74a816d 100644 --- a/alerts.go +++ b/alerts.go @@ -57,11 +57,6 @@ func (client *Client) AddAlert(ctx context.Context, namespace string, alertDefin alertDefinition.HookDatasourceUID(datasourceUID) - // Before we can add this alert, we need to delete any other alert that might exist for this dashboard and panel - if err := client.DeleteAlertGroup(ctx, namespace, alertDefinition.Builder.Name); err != nil && !errors.Is(err, ErrAlertNotFound) { - return fmt.Errorf("could not delete existing alerts: %w", err) - } - buf, err := json.Marshal(alertDefinition.Builder) if err != nil { return err diff --git a/cmd/builder-example/main.go b/cmd/builder-example/main.go index 1bf6c02b..94d5196b 100644 --- a/cmd/builder-example/main.go +++ b/cmd/builder-example/main.go @@ -113,6 +113,7 @@ func main() { timeseries.Legend(timeseries.Last, timeseries.AsTable), timeseries.Alert( "Too many heap allocations", + alert.Summary("Too many heap allocations"), alert.Description("Yup, too much of {{ app }}"), alert.Runbook("https://google.com"), alert.Tags(map[string]string{ diff --git a/dashboards.go b/dashboards.go index 3af5cb19..68cd305c 100644 --- a/dashboards.go +++ b/dashboards.go @@ -125,11 +125,24 @@ func (client *Client) UpsertDashboard(ctx context.Context, folder *Folder, build return nil, err } + alertPanelNames := make(map[string]struct{}, len(alerts)) + for _, al := range alerts { + alertPanelNames[al.PanelName] = struct{}{} + } + + // Clean alerts by panel name + for panelName := range alertPanelNames { + // Before we can add this alert, we need to delete any other alert that might exist for this dashboard and panel + if err := client.DeleteAlertGroup(ctx, folder.Title, panelName); err != nil && !errors.Is(err, ErrAlertNotFound) { + return nil, fmt.Errorf("could not delete existing alerts: %w", err) + } + } + for i := range alerts { alert := *alerts[i] alert.HookDashboardUID(dashboardFromGrafana.UID) - alert.HookPanelID(panelIDByTitle(dashboardFromGrafana, alert.Builder.Name)) + alert.HookPanelID(panelIDByTitle(dashboardFromGrafana, alert.PanelName)) if err := client.AddAlert(ctx, folder.Title, alert, datasourcesMap); err != nil { return nil, fmt.Errorf("could not add new alerts for dashboard: %w", err) diff --git a/dashboards_test.go b/dashboards_test.go index ded065c9..9bd516aa 100644 --- a/dashboards_test.go +++ b/dashboards_test.go @@ -636,6 +636,7 @@ func TestDashboardsCanBeCreatedWithNewAlertsAndDeletesPreviousAlerts(t *testing. ), timeseries.Alert( "Too many heap allocations", + alert.Summary("Too many heap allocations"), alert.WithPrometheusQuery( "A", "sum(go_memstats_heap_alloc_bytes{app!=\"\"}) by (app)", @@ -843,13 +844,13 @@ func TestDashboardsCanBeCreatedWithNewAlertsAndDeletesPreviousAlerts(t *testing. req.NoError(err) req.JSONEq(`{ - "name": "Heap allocations", + "name": "Too many heap allocations", "interval": "1m", "rules": [ { "for": "5m", "grafana_alert": { - "title": "Heap allocations", + "title": "Too many heap allocations", "condition": "_alert_condition_", "no_data_state": "NoData", "exec_err_state": "Alerting", diff --git a/decoder/alert_targets_test.go b/decoder/alert_targets_test.go index 0cb86f6b..8bbceb8b 100644 --- a/decoder/alert_targets_test.go +++ b/decoder/alert_targets_test.go @@ -45,7 +45,7 @@ func TestDecodingAPrometheusTarget(t *testing.T) { req.NoError(err) - alert := alertBuilder.New("", opt) + alert := alertBuilder.New("", "", opt) req.Len(alert.Builder.Rules, 1) req.Len(alert.Builder.Rules[0].GrafanaAlert.Data, 2) // the query and the condition @@ -101,7 +101,7 @@ func TestDecodingALokiTarget(t *testing.T) { req.NoError(err) - alert := alertBuilder.New("", opt) + alert := alertBuilder.New("", "", opt) req.Len(alert.Builder.Rules, 1) req.Len(alert.Builder.Rules[0].GrafanaAlert.Data, 2) // the query and the condition @@ -156,7 +156,7 @@ func TestDecodingAGraphiteTarget(t *testing.T) { req.NoError(err) - alert := alertBuilder.New("", opt) + alert := alertBuilder.New("", "", opt) req.Len(alert.Builder.Rules, 1) req.Len(alert.Builder.Rules[0].GrafanaAlert.Data, 2) // the query and the condition @@ -269,7 +269,7 @@ func TestDecodingStackdriverTarget(t *testing.T) { req.NoError(err, ErrInvalidStackdriverType) - alert := alertBuilder.New("", opt) + alert := alertBuilder.New("", "", opt) req.Len(alert.Builder.Rules, 1) req.Len(alert.Builder.Rules[0].GrafanaAlert.Data, 2) // the query and the condition @@ -335,7 +335,7 @@ func TestDecodingStackdriverPreprocessor(t *testing.T) { req.NoError(err) - alert := alertBuilder.New("", opt) + alert := alertBuilder.New("", "", opt) query := alert.Builder.Rules[0].GrafanaAlert.Data[1] req.Equal(tc.expected, query.Model.MetricQuery.Preprocessor) @@ -449,7 +449,7 @@ func TestDecodingStackdriverAggregation(t *testing.T) { req.NoError(err) - alert := alertBuilder.New("", opt) + alert := alertBuilder.New("", "", opt) query := alert.Builder.Rules[0].GrafanaAlert.Data[1] req.Equal(string(tc.expected), query.Model.MetricQuery.CrossSeriesReducer) @@ -591,7 +591,7 @@ func TestDecodingStackdriverAlignment(t *testing.T) { req.NoError(err) - alert := alertBuilder.New("", opt) + alert := alertBuilder.New("", "", opt) query := alert.Builder.Rules[0].GrafanaAlert.Data[1] req.Equal(string(tc.expected), query.Model.MetricQuery.PerSeriesAligner) diff --git a/decoder/alert_test.go b/decoder/alert_test.go index 59226657..a993fbc1 100644 --- a/decoder/alert_test.go +++ b/decoder/alert_test.go @@ -32,7 +32,7 @@ func TestDecodingSimpleAlert(t *testing.T) { opts, err := alertDef.toOptions() req.NoError(err) - alertBuilder := alert.New("", opts...) + alertBuilder := alert.New("", "", opts...) rule := alertBuilder.Builder.Rules[0] req.Equal("description", rule.Annotations["description"]) diff --git a/decoder/graph.go b/decoder/graph.go index 4edc1d0b..a5d2b0e4 100644 --- a/decoder/graph.go +++ b/decoder/graph.go @@ -3,6 +3,7 @@ package decoder import ( "fmt" + "github.com/K-Phoen/grabana/alert" "github.com/K-Phoen/grabana/axis" "github.com/K-Phoen/grabana/graph" "github.com/K-Phoen/grabana/graph/series" @@ -82,7 +83,10 @@ func (graphPanel DashboardGraph) toOption() (row.Option, error) { return nil, err } - opts = append(opts, graph.Alert(graphPanel.Alert.Summary, alertOpts...)) + opts = append(opts, graph.Alert( + graphPanel.Alert.Summary, + append(alertOpts, alert.Summary(graphPanel.Alert.Summary))...), + ) } if graphPanel.Visualization != nil { opts = append(opts, graphPanel.Visualization.toOptions()...) diff --git a/decoder/timeseries.go b/decoder/timeseries.go index 29615d16..6dd94cb0 100644 --- a/decoder/timeseries.go +++ b/decoder/timeseries.go @@ -3,6 +3,7 @@ package decoder import ( "fmt" + "github.com/K-Phoen/grabana/alert" "github.com/K-Phoen/grabana/row" "github.com/K-Phoen/grabana/timeseries" "github.com/K-Phoen/grabana/timeseries/axis" @@ -80,7 +81,10 @@ func (timeseriesPanel DashboardTimeSeries) toOption() (row.Option, error) { return nil, err } - opts = append(opts, timeseries.Alert(timeseriesPanel.Alert.Summary, alertOpts...)) + opts = append(opts, timeseries.Alert( + timeseriesPanel.Alert.Summary, + append(alertOpts, alert.Summary(timeseriesPanel.Alert.Summary))..., + )) } if timeseriesPanel.Visualization != nil { vizOpts, err := timeseriesPanel.Visualization.toOptions() diff --git a/graph/graph.go b/graph/graph.go index 8bc3f1f4..4c730f3d 100644 --- a/graph/graph.go +++ b/graph/graph.go @@ -74,12 +74,12 @@ const ( // Graph represents a graph panel. type Graph struct { Builder *sdk.Panel - Alert *alert.Alert + Alerts []*alert.Alert } // New creates a new graph panel. func New(title string, options ...Option) (*Graph, error) { - panel := &Graph{Builder: sdk.NewGraph(title)} + panel := &Graph{Builder: sdk.NewGraph(title), Alerts: make([]*alert.Alert, 0)} panel.Builder.AliasColors = make(map[string]interface{}) panel.Builder.IsNew = false @@ -266,8 +266,8 @@ func XAxis(opts ...axis.Option) Option { // Alert creates an alert for this graph. func Alert(name string, opts ...alert.Option) Option { return func(graph *Graph) error { - graph.Alert = alert.New(graph.Builder.Title, append(opts, alert.Summary(name))...) - graph.Alert.Builder.Name = graph.Builder.Title + al := alert.New(name, graph.Builder.Title, opts...) + graph.Alerts = append(graph.Alerts, al) return nil } diff --git a/graph/graph_test.go b/graph/graph_test.go index 961c9ec2..8e1748c2 100644 --- a/graph/graph_test.go +++ b/graph/graph_test.go @@ -158,8 +158,24 @@ func TestAlertsCanBeConfigured(t *testing.T) { panel, err := New("panel title", Alert("some alert")) req.NoError(err) - req.NotNil(panel.Alert) - req.Equal("panel title", panel.Alert.Builder.Name) + req.NotNil(panel.Alerts) + req.Len(panel.Alerts, 1) + req.Equal("some alert", panel.Alerts[0].Builder.Name) + req.Equal("panel title", panel.Alerts[0].PanelName) +} + +func TestTwoAlertsCanBeConfigured(t *testing.T) { + req := require.New(t) + + panel, err := New("panel title", Alert("some alert"), Alert("other alert")) + + req.NoError(err) + req.NotNil(panel.Alerts) + req.Len(panel.Alerts, 2) + req.Equal("panel title", panel.Alerts[0].PanelName) + req.Equal("panel title", panel.Alerts[1].PanelName) + req.Equal("some alert", panel.Alerts[0].Builder.Name) + req.Equal("other alert", panel.Alerts[1].Builder.Name) } func TestDrawModeCanBeConfigured(t *testing.T) { diff --git a/row/row.go b/row/row.go index d65105aa..1f9f1a9b 100644 --- a/row/row.go +++ b/row/row.go @@ -58,15 +58,17 @@ func WithGraph(title string, options ...graph.Option) Option { row.builder.Add(panel.Builder) - if panel.Alert == nil { + if len(panel.Alerts) == 0 { return nil } - if panel.Builder.Datasource != nil { - panel.Alert.Datasource = panel.Builder.Datasource.LegacyName - } + for _, al := range panel.Alerts { + if panel.Builder.Datasource != nil { + al.Datasource = panel.Builder.Datasource.LegacyName + } - row.alerts = append(row.alerts, panel.Alert) + row.alerts = append(row.alerts, al) + } return nil } @@ -82,15 +84,17 @@ func WithTimeSeries(title string, options ...timeseries.Option) Option { row.builder.Add(panel.Builder) - if panel.Alert == nil { + if len(panel.Alerts) == 0 { return nil } - if panel.Builder.Datasource != nil { - panel.Alert.Datasource = panel.Builder.Datasource.LegacyName - } + for _, al := range panel.Alerts { + if panel.Builder.Datasource != nil { + al.Datasource = panel.Builder.Datasource.LegacyName + } - row.alerts = append(row.alerts, panel.Alert) + row.alerts = append(row.alerts, al) + } return nil } diff --git a/row/row_test.go b/row/row_test.go index 2bad20a7..65a52a71 100644 --- a/row/row_test.go +++ b/row/row_test.go @@ -69,12 +69,20 @@ func TestRowsCanHaveGraphsAndAlert(t *testing.T) { ), alert.If(alert.Avg, "A", alert.IsAbove(3)), ), + graph.Alert( + "No heap allocations", + alert.WithPrometheusQuery( + "A", + "sum(go_memstats_heap_alloc_bytes{app!=\"\"}) by (app)", + ), + alert.If(alert.Avg, "A", alert.IsBelow(0.01)), + ), ), ) req.NoError(err) req.Len(panel.builder.Panels, 1) - req.Len(panel.Alerts(), 1) + req.Len(panel.Alerts(), 2) req.Equal("Prometheus", panel.Alerts()[0].Datasource) } @@ -107,12 +115,20 @@ func TestRowsCanHaveTimeSeriesAndAlert(t *testing.T) { ), alert.If(alert.Avg, "A", alert.IsAbove(3)), ), + timeseries.Alert( + "No heap allocations", + alert.WithPrometheusQuery( + "A", + "sum(go_memstats_heap_alloc_bytes{app!=\"\"}) by (app)", + ), + alert.If(alert.Avg, "A", alert.IsBelow(0.01)), + ), ), ) req.NoError(err) req.Len(panel.builder.Panels, 1) - req.Len(panel.Alerts(), 1) + req.Len(panel.Alerts(), 2) req.Equal("Prometheus", panel.Alerts()[0].Datasource) } diff --git a/timeseries/timeseries.go b/timeseries/timeseries.go index 1fc2a236..e18152f8 100644 --- a/timeseries/timeseries.go +++ b/timeseries/timeseries.go @@ -123,12 +123,12 @@ const ( // TimeSeries represents a time series panel. type TimeSeries struct { Builder *sdk.Panel - Alert *alert.Alert + Alerts []*alert.Alert } // New creates a new time series panel. func New(title string, options ...Option) (*TimeSeries, error) { - panel := &TimeSeries{Builder: sdk.NewTimeseries(title)} + panel := &TimeSeries{Builder: sdk.NewTimeseries(title), Alerts: make([]*alert.Alert, 0)} panel.Builder.IsNew = false for _, opt := range append(defaults(), options...) { @@ -409,8 +409,8 @@ func Transparent() Option { // Alert creates an alert for this graph. func Alert(name string, opts ...alert.Option) Option { return func(timeseries *TimeSeries) error { - timeseries.Alert = alert.New(timeseries.Builder.Title, append(opts, alert.Summary(name))...) - timeseries.Alert.Builder.Name = timeseries.Builder.Title + al := alert.New(name, timeseries.Builder.Title, opts...) + timeseries.Alerts = append(timeseries.Alerts, al) return nil } diff --git a/timeseries/timeseries_test.go b/timeseries/timeseries_test.go index a26974af..2a268bab 100644 --- a/timeseries/timeseries_test.go +++ b/timeseries/timeseries_test.go @@ -144,8 +144,24 @@ func TestAlertsCanBeConfigured(t *testing.T) { panel, err := New("panel title", Alert("some alert")) req.NoError(err) - req.NotNil(panel.Alert) - req.Equal("panel title", panel.Alert.Builder.Name) + req.NotNil(panel.Alerts) + req.Len(panel.Alerts, 1) + req.Equal("panel title", panel.Alerts[0].PanelName) + req.Equal("some alert", panel.Alerts[0].Builder.Name) +} + +func TestTwoAlertsCanBeConfigured(t *testing.T) { + req := require.New(t) + + panel, err := New("panel title", Alert("some alert"), Alert("other alert")) + + req.NoError(err) + req.NotNil(panel.Alerts) + req.Len(panel.Alerts, 2) + req.Equal("panel title", panel.Alerts[0].PanelName) + req.Equal("panel title", panel.Alerts[1].PanelName) + req.Equal("some alert", panel.Alerts[0].Builder.Name) + req.Equal("other alert", panel.Alerts[1].Builder.Name) } func TestLineWidthCanBeConfigured(t *testing.T) {