-
Notifications
You must be signed in to change notification settings - Fork 2.5k
feat(slack): optional blokkit payload for Slack notifications #5414
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| // Copyright The Prometheus Authors | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| package slack | ||
|
|
||
| import ( | ||
| "fmt" | ||
|
|
||
| "github.com/prometheus/alertmanager/template" | ||
| ) | ||
|
|
||
| func composeBlockKitPayload(blocksTmpl any, channel, text string, tmplText func(string) string, tmplTextErr *error) (map[string]any, error) { | ||
| tmplTextFunc := func(tmpl string) (string, error) { | ||
| return tmplText(tmpl), *tmplTextErr | ||
| } | ||
|
|
||
| renderedBlocks, err := template.DeepCopyWithTemplate(blocksTmpl, tmplTextFunc) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| return map[string]any{ | ||
| "channel": channel, | ||
| "text": text, | ||
| "blocks": renderedBlocks, | ||
|
Comment on lines
+27
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Keep rendered Block Kit values as strings.
Render template substitutions without YAML-decoding the rendered result. Add coverage for colon-space and scalar-looking rendered text. 🤖 Prompt for AI Agents |
||
| }, nil | ||
| } | ||
|
|
||
| func setBlockKitPayloadStringValue(payload any, key, value string) error { | ||
| payloadMap, ok := payload.(map[string]any) | ||
| if !ok { | ||
| return fmt.Errorf("block_kit_payload must render to an object, got %T", payload) | ||
| } | ||
| payloadMap[key] = value | ||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| // Copyright The Prometheus Authors | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| package slack | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/prometheus/alertmanager/notify" | ||
| "github.com/prometheus/alertmanager/notify/test" | ||
| "github.com/prometheus/alertmanager/template" | ||
| ) | ||
|
|
||
| func TestComposeBlockKitPayload(t *testing.T) { | ||
| blocksTmpl := []any{ | ||
| map[string]any{ | ||
| "type": "section", | ||
| "text": map[string]any{ | ||
| "type": "mrkdwn", | ||
| "text": `*Summary:* {{ index .CommonAnnotations "summary" }}`, | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| tmpl := test.CreateTmpl(t) | ||
| data := &template.Data{ | ||
| Status: "firing", | ||
| CommonAnnotations: template.KV{"summary": "CPU usage above 90 percent"}, | ||
| } | ||
| var tmplTextErr error | ||
| tmplText := notify.TmplText(tmpl, data, &tmplTextErr) | ||
|
|
||
| payload, err := composeBlockKitPayload(blocksTmpl, "#alerts-channel", tmplText(`{{ .Status }}`), tmplText, &tmplTextErr) | ||
| require.NoError(t, err) | ||
| require.NoError(t, tmplTextErr) | ||
|
|
||
| encoded, err := json.Marshal(payload) | ||
| require.NoError(t, err) | ||
|
|
||
| require.JSONEq(t, `{ | ||
| "channel": "#alerts-channel", | ||
| "text": "firing", | ||
| "blocks": [ | ||
| { | ||
| "type": "section", | ||
| "text": { | ||
| "type": "mrkdwn", | ||
| "text": "*Summary:* CPU usage above 90 percent" | ||
| } | ||
| } | ||
| ] | ||
| }`, string(encoded)) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| // Copyright The Prometheus Authors | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| package slack | ||
|
|
||
| import ( | ||
| "log/slog" | ||
|
|
||
| "github.com/prometheus/alertmanager/config" | ||
| "github.com/prometheus/alertmanager/notify" | ||
| ) | ||
|
|
||
| // https://api.slack.com/reference/messaging/attachments#legacy_fields - 1024, no units given, assuming runes or characters. | ||
| const maxTitleLenRunes = 1024 | ||
|
|
||
| // composePlainRequest builds the current attachment-based Slack payload. | ||
| func composePlainRequest(c *config.SlackConfig, tmplText func(string) string, logger *slog.Logger) *request { | ||
| var markdownIn []string | ||
| if len(c.MrkdwnIn) == 0 { | ||
| markdownIn = []string{"fallback", "pretext", "text"} | ||
| } else { | ||
| markdownIn = c.MrkdwnIn | ||
| } | ||
|
|
||
| title, truncated := notify.TruncateInRunes(tmplText(c.Title), maxTitleLenRunes) | ||
| if truncated { | ||
| logger.Warn("Truncated title", "max_runes", maxTitleLenRunes) | ||
| } | ||
|
|
||
| att := attachment{ | ||
| Title: title, | ||
| TitleLink: tmplText(c.TitleLink), | ||
| Pretext: tmplText(c.Pretext), | ||
| Text: tmplText(c.Text), | ||
| Fallback: tmplText(c.Fallback), | ||
| CallbackID: tmplText(c.CallbackID), | ||
| ImageURL: tmplText(c.ImageURL), | ||
| ThumbURL: tmplText(c.ThumbURL), | ||
| Footer: tmplText(c.Footer), | ||
| Color: tmplText(c.Color), | ||
| MrkdwnIn: markdownIn, | ||
| } | ||
|
|
||
| numFields := len(c.Fields) | ||
| if numFields > 0 { | ||
| fields := make([]config.SlackField, numFields) | ||
| for index, field := range c.Fields { | ||
| // Check if short was defined for the field otherwise fallback to the global setting. | ||
| var short bool | ||
| if field.Short != nil { | ||
| short = *field.Short | ||
| } else { | ||
| short = c.ShortFields | ||
| } | ||
|
|
||
| // Rebuild the field by executing templates and preserving short semantics. | ||
| fields[index] = config.SlackField{ | ||
| Title: tmplText(field.Title), | ||
| Value: tmplText(field.Value), | ||
| Short: &short, | ||
| } | ||
| } | ||
| att.Fields = fields | ||
| } | ||
|
|
||
| numActions := len(c.Actions) | ||
| if numActions > 0 { | ||
| actions := make([]config.SlackAction, numActions) | ||
| for index, action := range c.Actions { | ||
| slackAction := config.SlackAction{ | ||
| Type: tmplText(action.Type), | ||
| Text: tmplText(action.Text), | ||
| URL: tmplText(action.URL), | ||
| Style: tmplText(action.Style), | ||
| Name: tmplText(action.Name), | ||
| Value: tmplText(action.Value), | ||
| } | ||
|
|
||
| if action.ConfirmField != nil { | ||
| slackAction.ConfirmField = &config.SlackConfirmationField{ | ||
| Title: tmplText(action.ConfirmField.Title), | ||
| Text: tmplText(action.ConfirmField.Text), | ||
| OkText: tmplText(action.ConfirmField.OkText), | ||
| DismissText: tmplText(action.ConfirmField.DismissText), | ||
| } | ||
| } | ||
|
|
||
| actions[index] = slackAction | ||
| } | ||
| att.Actions = actions | ||
| } | ||
|
|
||
| return &request{ | ||
| Channel: tmplText(c.Channel), | ||
| Username: tmplText(c.Username), | ||
| IconEmoji: tmplText(c.IconEmoji), | ||
| IconURL: tmplText(c.IconURL), | ||
| LinkNames: c.LinkNames, | ||
| Text: tmplText(c.MessageText), | ||
| Attachments: []attachment{att}, | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Correct the exported Block Kit field names.
BlocKitEnabeldandBlocKitPayloadexpose misspelled Go API names. Rename them toBlockKitEnabledandBlockKitPayloadbefore release. Update all notifier and test references.🤖 Prompt for AI Agents