From 44eb3498c91a9c33fe793e34fa23e5227886894f Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Mon, 2 Sep 2024 09:24:48 +0200 Subject: [PATCH 1/2] templates: add AlertingListURL to ExternalData Some of the receivers use the /alerting/list for grouping alerts when their generator URLs aren't the same. However these alerting list URLs are not "org-aware". Instead of passing an orgId to every receiver constructor, we can enrich the template (extended) data with a URL that has the ?orgID=N in case it exists in the common annotations, to be reused by any receiver. --- templates/template_data.go | 39 +++++++++++++++++++------ templates/template_data_test.go | 52 +++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 9 deletions(-) create mode 100644 templates/template_data_test.go diff --git a/templates/template_data.go b/templates/template_data.go index 0e0a6bf4..37fab8ec 100644 --- a/templates/template_data.go +++ b/templates/template_data.go @@ -62,7 +62,8 @@ type ExtendedData struct { CommonLabels KV `json:"commonLabels"` CommonAnnotations KV `json:"commonAnnotations"` - ExternalURL string `json:"externalURL"` + ExternalURL string `json:"externalURL"` + AlertingListURL string `json:"alertingListURL"` } // FromContent calls Parse on all provided template content and returns the resulting Template. Content equivalent to templates.FromGlobs. @@ -134,14 +135,15 @@ func extendAlert(alert template.Alert, externalURL string, logger log.Logger) *E } externalPath := u.Path - generatorURL, err := url.Parse(extended.GeneratorURL) - if err != nil { - level.Debug(logger).Log("msg", "failed to parse generator URL while extending template data", "url", extended.GeneratorURL, "error", err.Error()) - return extended - } - orgID := alert.Annotations[models.OrgIDAnnotation] - if len(orgID) > 0 { + + if len(extended.GeneratorURL) > 0 && len(orgID) > 0 { + generatorURL, err := url.Parse(extended.GeneratorURL) + if err != nil { + level.Debug(logger).Log("msg", "failed to parse generator URL while extending template data", "url", extended.GeneratorURL, "error", err.Error()) + return extended + } + extended.GeneratorURL = setOrgIDQueryParam(generatorURL, orgID) } @@ -208,6 +210,24 @@ func setOrgIDQueryParam(url *url.URL, orgID string) string { return url.String() } +func parseAlertingListURL(data *Data, logger log.Logger) string { + externalURL, err := url.Parse(data.ExternalURL) + if err != nil { + level.Debug(logger).Log("msg", "failed to parse external URL while extending template data", "url", data.ExternalURL, "error", err.Error()) + + return "" + } + + externalURL.Path = path.Join(externalURL.Path, "/alerting/list") + + orgID := data.CommonAnnotations[models.OrgIDAnnotation] + if len(orgID) > 0 { + return setOrgIDQueryParam(externalURL, orgID) + } + + return externalURL.String() +} + func ExtendData(data *Data, logger log.Logger) *ExtendedData { alerts := make([]ExtendedAlert, 0, len(data.Alerts)) @@ -224,7 +244,8 @@ func ExtendData(data *Data, logger log.Logger) *ExtendedData { CommonLabels: removePrivateItems(data.CommonLabels), CommonAnnotations: removePrivateItems(data.CommonAnnotations), - ExternalURL: data.ExternalURL, + ExternalURL: data.ExternalURL, + AlertingListURL: parseAlertingListURL(data, logger), } return extended } diff --git a/templates/template_data_test.go b/templates/template_data_test.go new file mode 100644 index 00000000..2a8785af --- /dev/null +++ b/templates/template_data_test.go @@ -0,0 +1,52 @@ +package templates + +import ( + "testing" + + "github.com/prometheus/alertmanager/template" + "github.com/stretchr/testify/require" + + "github.com/grafana/alerting/logging" + "github.com/grafana/alerting/models" +) + +func TestParseAlertingListURL(t *testing.T) { + testcases := []struct { + name string + data *Data + expectedURI string + }{ + { + name: "without org annotation", + data: &Data{ + ExternalURL: "http://localhost:3000", + }, + expectedURI: "http://localhost:3000/alerting/list", + }, + { + name: "with org annotation", + data: &Data{ + ExternalURL: "http://localhost:3000", + CommonAnnotations: template.KV{ + models.OrgIDAnnotation: "1234", + }, + }, + expectedURI: "http://localhost:3000/alerting/list?orgId=1234", + }, + { + name: "with invalid external URL", + data: &Data{ + ExternalURL: `http%//invalid@url.com`, + }, + expectedURI: "", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + actualURI := parseAlertingListURL(tc.data, &logging.FakeLogger{}) + + require.Equal(t, tc.expectedURI, actualURI) + }) + } +} From 7862878820d49fa8178b594359a75c69dfc688e1 Mon Sep 17 00:00:00 2001 From: Matheus Macabu Date: Mon, 2 Sep 2024 09:25:39 +0200 Subject: [PATCH 2/2] receivers/slack: use common (org-aware) alerting list url --- receivers/slack/slack.go | 10 ++++----- receivers/slack/slack_test.go | 39 +++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/receivers/slack/slack.go b/receivers/slack/slack.go index 1aad87f5..7595ec54 100644 --- a/receivers/slack/slack.go +++ b/receivers/slack/slack.go @@ -283,7 +283,7 @@ func handleSlackJSONResponse(resp *http.Response, logger logging.Logger) (string return result.Ts, nil } -func (sn *Notifier) commonAlertGeneratorURL(_ context.Context, alerts []*types.Alert) bool { +func (sn *Notifier) commonAlertGeneratorURL(_ context.Context, alerts templates.ExtendedAlerts) bool { if len(alerts[0].GeneratorURL) == 0 { return false } @@ -298,13 +298,13 @@ func (sn *Notifier) commonAlertGeneratorURL(_ context.Context, alerts []*types.A func (sn *Notifier) createSlackMessage(ctx context.Context, alerts []*types.Alert) (*slackMessage, error) { var tmplErr error - tmpl, _ := templates.TmplText(ctx, sn.tmpl, alerts, sn.log, &tmplErr) + tmpl, data := templates.TmplText(ctx, sn.tmpl, alerts, sn.log, &tmplErr) - ruleURL := receivers.JoinURLPath(sn.tmpl.ExternalURL.String(), "/alerting/list", sn.log) + ruleURL := data.AlertingListURL // If all alerts have the same GeneratorURL, use that. - if sn.commonAlertGeneratorURL(ctx, alerts) { - ruleURL = alerts[0].GeneratorURL + if sn.commonAlertGeneratorURL(ctx, data.Alerts) { + ruleURL = data.Alerts[0].GeneratorURL } title, truncated := receivers.TruncateInRunes(tmpl(sn.settings.Title), slackMaxTitleLenRunes) diff --git a/receivers/slack/slack_test.go b/receivers/slack/slack_test.go index 5ea6446d..05515fc8 100644 --- a/receivers/slack/slack_test.go +++ b/receivers/slack/slack_test.go @@ -154,6 +154,45 @@ func TestNotify_IncomingWebhook(t *testing.T) { }, }, }, + }, { + name: "Message is sent with orgId", + settings: Config{ + EndpointURL: APIURL, + URL: "https://example.com/hooks/xxxx", + Token: "", + Recipient: "#test", + Text: templates.DefaultMessageEmbed, + Title: templates.DefaultMessageTitleEmbed, + Username: "Grafana", + IconEmoji: ":emoji:", + IconURL: "", + MentionChannel: "", + MentionUsers: nil, + MentionGroups: nil, + }, + alerts: []*types.Alert{{ + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1", "__orgId__": "123"}, + }, + }}, + expectedMessage: &slackMessage{ + Channel: "#test", + Username: "Grafana", + IconEmoji: ":emoji:", + Attachments: []attachment{ + { + Title: "[FIRING:1] (val1)", + TitleLink: "http://localhost/alerting/list?orgId=123", + Text: "**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&orgId=123\n", + Fallback: "[FIRING:1] (val1)", + Fields: nil, + Footer: "Grafana v" + appVersion, + FooterIcon: "https://grafana.com/static/assets/img/fav32.png", + Color: "#D63232", + }, + }, + }, }} for _, test := range tests {