From 9dc7e752b78e87e3d8de20a842ca1d988dfd3ee8 Mon Sep 17 00:00:00 2001 From: Santiago Date: Wed, 8 Jun 2022 17:55:31 -0300 Subject: [PATCH] Optional custom title and description for OpsGenie (#50131) * optional custom description for OpsGenie * custom title and message, tests * update changelog * check for empty / whitespace only strings * truncate the title to 130 characters if needed * unnecessary validation removed * truncate title to 127 characters and add three dots --- pkg/services/ngalert/CHANGELOG.md | 1 + .../ngalert/notifier/available_channels.go | 14 ++++ .../ngalert/notifier/channels/opsgenie.go | 45 ++++++++--- .../notifier/channels/opsgenie_test.go | 75 ++++++++++++++++++- 4 files changed, 124 insertions(+), 11 deletions(-) diff --git a/pkg/services/ngalert/CHANGELOG.md b/pkg/services/ngalert/CHANGELOG.md index 91de777c579..8399d105de4 100644 --- a/pkg/services/ngalert/CHANGELOG.md +++ b/pkg/services/ngalert/CHANGELOG.md @@ -56,6 +56,7 @@ Scopes must have an order to ensure consistency and ease of search, this helps u - [ENHANCEMENT] Create folder 'General Alerting' when Grafana starts from the scratch #48866 - [ENHANCEMENT] Rule changes authorization logic to use UID folder scope instead of ID scope #48970 - [ENHANCEMENT] Scheduler: ticker to support stopping #48142 +- [ENHANCEMENT] Optional custom title and description for OpsGenie #50131 - [ENHANCEMENT] Scheduler: Adds new metrics to track rules that might be scheduled #49874 - `grafana_alerting_schedule_alert_rules ` - `grafana_alerting_schedule_alert_rules_hash ` diff --git a/pkg/services/ngalert/notifier/available_channels.go b/pkg/services/ngalert/notifier/available_channels.go index 1ba56d6dbcd..948a70102b1 100644 --- a/pkg/services/ngalert/notifier/available_channels.go +++ b/pkg/services/ngalert/notifier/available_channels.go @@ -863,6 +863,20 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin { PropertyName: "apiUrl", Required: true, }, + { + Label: "Message", + Description: "Alert text limited to 130 characters.", + Element: alerting.ElementTypeInput, + InputType: alerting.InputTypeText, + Placeholder: `{{ template "default.title" . }}`, + PropertyName: "message", + }, + { + Label: "Description", + Description: "A description of the incident.", + Element: alerting.ElementTypeTextArea, + PropertyName: "description", + }, { Label: "Auto close incidents", Element: alerting.ElementTypeCheckbox, diff --git a/pkg/services/ngalert/notifier/channels/opsgenie.go b/pkg/services/ngalert/notifier/channels/opsgenie.go index e864c000cc9..b0737161b14 100644 --- a/pkg/services/ngalert/notifier/channels/opsgenie.go +++ b/pkg/services/ngalert/notifier/channels/opsgenie.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "sort" + "strings" "github.com/prometheus/alertmanager/notify" "github.com/prometheus/alertmanager/template" @@ -36,6 +37,8 @@ type OpsgenieNotifier struct { *Base APIKey string APIUrl string + Message string + Description string AutoClose bool OverridePriority bool SendTagsAs string @@ -49,6 +52,8 @@ type OpsgenieConfig struct { *NotificationChannelConfig APIKey string APIUrl string + Message string + Description string AutoClose bool OverridePriority bool SendTagsAs string @@ -82,6 +87,8 @@ func NewOpsgenieConfig(config *NotificationChannelConfig, decryptFunc GetDecrypt APIUrl: config.Settings.Get("apiUrl").MustString(OpsgenieAlertURL), AutoClose: config.Settings.Get("autoClose").MustBool(true), OverridePriority: config.Settings.Get("overridePriority").MustBool(true), + Message: config.Settings.Get("message").MustString(`{{ template "default.title" . }}`), + Description: config.Settings.Get("description").MustString(""), SendTagsAs: sendTagsAs, }, nil } @@ -98,6 +105,8 @@ func NewOpsgenieNotifier(config *OpsgenieConfig, ns notifications.WebhookSender, }), APIKey: config.APIKey, APIUrl: config.APIUrl, + Description: config.Description, + Message: config.Message, AutoClose: config.AutoClose, OverridePriority: config.OverridePriority, SendTagsAs: config.SendTagsAs, @@ -180,13 +189,25 @@ func (on *OpsgenieNotifier) buildOpsgenieMessage(ctx context.Context, alerts mod var tmplErr error tmpl, data := TmplText(ctx, on.tmpl, as, on.log, &tmplErr) - title := tmpl(DefaultMessageTitleEmbed) - description := fmt.Sprintf( - "%s\n%s\n\n%s", - tmpl(DefaultMessageTitleEmbed), - ruleURL, - tmpl(`{{ template "default.message" . }}`), - ) + titleTmpl := on.Message + if strings.TrimSpace(titleTmpl) == "" { + titleTmpl = `{{ template "default.title" . }}` + } + + title := tmpl(titleTmpl) + if len(title) > 130 { + title = title[:127] + "..." + } + + description := tmpl(on.Description) + if strings.TrimSpace(description) == "" { + description = fmt.Sprintf( + "%s\n%s\n\n%s", + tmpl(DefaultMessageTitleEmbed), + ruleURL, + tmpl(`{{ template "default.message" . }}`), + ) + } var priority string @@ -202,6 +223,12 @@ func (on *OpsgenieNotifier) buildOpsgenieMessage(ctx context.Context, alerts mod } } + // Check for templating errors + if tmplErr != nil { + on.log.Warn("failed to template Opsgenie message", "err", tmplErr.Error()) + tmplErr = nil + } + bodyJSON.Set("message", title) bodyJSON.Set("source", "Grafana") bodyJSON.Set("alias", alias) @@ -244,9 +271,9 @@ func (on *OpsgenieNotifier) buildOpsgenieMessage(ctx context.Context, alerts mod bodyJSON.Set("tags", tags) bodyJSON.Set("details", details) apiURL = tmpl(on.APIUrl) - if tmplErr != nil { - on.log.Warn("failed to template Opsgenie message", "err", tmplErr.Error()) + on.log.Warn("failed to template Opsgenie URL", "err", tmplErr.Error(), "fallback", on.APIUrl) + apiURL = on.APIUrl } return bodyJSON, apiURL, nil diff --git a/pkg/services/ngalert/notifier/channels/opsgenie_test.go b/pkg/services/ngalert/notifier/channels/opsgenie_test.go index c1b6179cb59..04b507517cb 100644 --- a/pkg/services/ngalert/notifier/channels/opsgenie_test.go +++ b/pkg/services/ngalert/notifier/channels/opsgenie_test.go @@ -54,10 +54,81 @@ func TestOpsgenieNotifier(t *testing.T) { }`, }, { - name: "Default config with one alert and send tags as tags", + name: "Default config with one alert, custom message and description", + settings: `{"apiKey": "abcdefgh0123456789", "message": "test message", "description": "test description"}`, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh"}, + }, + }, + }, + expMsg: `{ + "alias": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733", + "description": "test description", + "details": { + "url": "http://localhost/alerting/list" + }, + "message": "test message", + "source": "Grafana", + "tags": ["alertname:alert1", "lbl1:val1"] + }`, + }, + { + name: "Default config with one alert, message length > 130", settings: `{ "apiKey": "abcdefgh0123456789", - "sendTagsAs": "tags" + "message": "IyJnsW78xQoiBJ7L7NqASv31JCFf0At3r9KUykqBVxSiC6qkDhvDLDW9VImiFcq0Iw2XwFy5fX4FcbTmlkaZzUzjVwx9VUuokhzqQlJVhWDYFqhj3a5wX0LjyvNQjsqT9WaWJAWOJanwOAWon" + }`, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh"}, + }, + }, + }, + expMsg: `{ + "alias": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733", + "description": "[FIRING:1] (val1)\nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n", + "details": { + "url": "http://localhost/alerting/list" + }, + "message": "IyJnsW78xQoiBJ7L7NqASv31JCFf0At3r9KUykqBVxSiC6qkDhvDLDW9VImiFcq0Iw2XwFy5fX4FcbTmlkaZzUzjVwx9VUuokhzqQlJVhWDYFqhj3a5wX0LjyvNQjsq...", + "source": "Grafana", + "tags": ["alertname:alert1", "lbl1:val1"] + }`, + }, + { + name: "Default config with one alert, templated message and description", + settings: `{"apiKey": "abcdefgh0123456789", "message": "Firing: {{ len .Alerts.Firing }}", "description": "{{ len .Alerts.Firing }} firing, {{ len .Alerts.Resolved }} resolved."}`, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh"}, + }, + }, + }, + expMsg: `{ + "alias": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733", + "description": "1 firing, 0 resolved.", + "details": { + "url": "http://localhost/alerting/list" + }, + "message": "Firing: 1", + "source": "Grafana", + "tags": ["alertname:alert1", "lbl1:val1"] + }`, + }, + { + name: "Default config with one alert and send tags as tags, empty description and message", + settings: `{ + "apiKey": "abcdefgh0123456789", + "sendTagsAs": "tags", + "message": " ", + "description": " " }`, alerts: []*types.Alert{ {