Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 19 additions & 17 deletions config/notifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,23 +328,25 @@ type SlackConfig struct {
Username string `yaml:"username,omitempty" json:"username,omitempty"`
Color string `yaml:"color,omitempty" json:"color,omitempty"`

Title string `yaml:"title,omitempty" json:"title,omitempty"`
TitleLink string `yaml:"title_link,omitempty" json:"title_link,omitempty"`
Pretext string `yaml:"pretext,omitempty" json:"pretext,omitempty"`
Text string `yaml:"text,omitempty" json:"text,omitempty"`
MessageText string `yaml:"message_text,omitempty" json:"message_text,omitempty"`
Fields []*SlackField `yaml:"fields,omitempty" json:"fields,omitempty"`
ShortFields bool `yaml:"short_fields" json:"short_fields,omitempty"`
Footer string `yaml:"footer,omitempty" json:"footer,omitempty"`
Fallback string `yaml:"fallback,omitempty" json:"fallback,omitempty"`
CallbackID string `yaml:"callback_id,omitempty" json:"callback_id,omitempty"`
IconEmoji string `yaml:"icon_emoji,omitempty" json:"icon_emoji,omitempty"`
IconURL string `yaml:"icon_url,omitempty" json:"icon_url,omitempty"`
ImageURL string `yaml:"image_url,omitempty" json:"image_url,omitempty"`
ThumbURL string `yaml:"thumb_url,omitempty" json:"thumb_url,omitempty"`
LinkNames bool `yaml:"link_names" json:"link_names,omitempty"`
MrkdwnIn []string `yaml:"mrkdwn_in,omitempty" json:"mrkdwn_in,omitempty"`
Actions []*SlackAction `yaml:"actions,omitempty" json:"actions,omitempty"`
Title string `yaml:"title,omitempty" json:"title,omitempty"`
TitleLink string `yaml:"title_link,omitempty" json:"title_link,omitempty"`
Pretext string `yaml:"pretext,omitempty" json:"pretext,omitempty"`
Text string `yaml:"text,omitempty" json:"text,omitempty"`
MessageText string `yaml:"message_text,omitempty" json:"message_text,omitempty"`
Fields []*SlackField `yaml:"fields,omitempty" json:"fields,omitempty"`
ShortFields bool `yaml:"short_fields" json:"short_fields,omitempty"`
Footer string `yaml:"footer,omitempty" json:"footer,omitempty"`
Fallback string `yaml:"fallback,omitempty" json:"fallback,omitempty"`
CallbackID string `yaml:"callback_id,omitempty" json:"callback_id,omitempty"`
IconEmoji string `yaml:"icon_emoji,omitempty" json:"icon_emoji,omitempty"`
IconURL string `yaml:"icon_url,omitempty" json:"icon_url,omitempty"`
ImageURL string `yaml:"image_url,omitempty" json:"image_url,omitempty"`
ThumbURL string `yaml:"thumb_url,omitempty" json:"thumb_url,omitempty"`
LinkNames bool `yaml:"link_names" json:"link_names,omitempty"`
MrkdwnIn []string `yaml:"mrkdwn_in,omitempty" json:"mrkdwn_in,omitempty"`
Actions []*SlackAction `yaml:"actions,omitempty" json:"actions,omitempty"`
BlocKitEnabeld *bool `yaml:"use_block_kit,omitempty" json:"use_block_kit,omitempty"`
BlocKitPayload any `yaml:"block_kit_payload,omitempty" json:"block_kit_payload,omitempty"`
Comment on lines +348 to +349

Copy link
Copy Markdown

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.

BlocKitEnabeld and BlocKitPayload expose misspelled Go API names. Rename them to BlockKitEnabled and BlockKitPayload before release. Update all notifier and test references.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@config/notifiers.go` around lines 348 - 349, Rename the exported fields
BlocKitEnabeld and BlocKitPayload to BlockKitEnabled and BlockKitPayload in the
notifier configuration, then update every notifier and test reference to use the
corrected names while preserving their existing YAML and JSON tags.


// UpdateMessage enables updating existing Slack messages instead of creating new ones.
// Requires bot token with chat:write scope. Webhook URLs do not support updates.
Expand Down
43 changes: 43 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -1743,6 +1743,20 @@ fields:
# Enables updating existing Slack messages instead of creating new ones on alert state change.
# Webhook URLs do not support updates.
[ update_message: <boolean> | default = false ]

# Enables Slack Block Kit layout instead of the legacy attachment format.
[ use_block_kit: <boolean> | default = false ]

# A list of Block Kit block objects rendered as the 'blocks' field of the Slack API payload.
# Template expressions in string values are expanded with the same data available to other
# tmpl_string fields. The channel and fallback text come from the top-level 'channel' and
# 'message_text' fields respectively.
# See https://api.slack.com/reference/block-kit/blocks for the block schema.
# NOTE: String values that render to a YAML/JSON mapping (e.g. "key: value") are parsed into
# a map structure by the template engine. Avoid bare 'key: value' patterns in rendered text;
# use a different separator or quote the value in the template if needed.
block_kit_payload:
[ - <block_object>, ... ]
```

#### `<action_config>` (Slack)
Expand Down Expand Up @@ -1782,6 +1796,35 @@ value: <tmpl_string>
[ short: <boolean> | default = slack_config.short_fields ]
```

#### Block Kit example

When `use_block_kit: true` the `block_kit_payload` field accepts a list of [Block Kit block objects](https://api.slack.com/reference/block-kit/blocks). The `channel` and fallback `message_text` are set at the top level of the receiver config as usual.

```yaml
slack_configs:
- api_url: 'https://slack.com/api/chat.postMessage'
http_config:
authorization:
credentials: '<bot-token>'
channel: '#alerts'
message_text: 'fallback string'
use_block_kit: true
block_kit_payload:
- type: header
text:
type: plain_text
text: '{{ .CommonAnnotations.SortedPairs.Values | join " " | printf "%q" }}'
- type: section
text:
type: mrkdwn
text: |-
*Alert:* {{ .CommonAnnotations.description }} - Severity: {{ .CommonLabels.severity_id }}

*Description:* {{ .CommonAnnotations.description }}
```

> **Note:** String values inside `block_kit_payload` are rendered as Go templates and then passed through a YAML parser. A rendered value that matches the `key: value` pattern (colon followed by a space) will be interpreted as a YAML mapping and converted to a map rather than remaining a plain string. To keep such text as a string, avoid the bare `key: value` pattern in rendered output or use a template that wraps the value in quotes (e.g. `{{ .Value | printf "%q" }}`).

### `<sns_config>`

```yaml
Expand Down
46 changes: 46 additions & 0 deletions notify/slack/message_blockkit.go
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Keep rendered Block Kit values as strings.

DeepCopyWithTemplate reparses rendered string values. A valid Slack text value such as summary: disk full becomes a map, which violates the Block Kit schema. The documented quote workaround does not make normal templated text safe.

Render template substitutions without YAML-decoding the rendered result. Add coverage for colon-space and scalar-looking rendered text.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@notify/slack/message_blockkit.go` around lines 27 - 35, Update the rendering
flow around DeepCopyWithTemplate so template substitutions are rendered without
reparsing rendered string values as YAML, preserving Block Kit text fields as
strings. Keep the existing block structure and template behavior intact, and add
coverage for rendered text containing colon-space and scalar-looking values.

}, 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
}
66 changes: 66 additions & 0 deletions notify/slack/message_blockkit_test.go
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))
}
112 changes: 112 additions & 0 deletions notify/slack/message_plain.go
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},
}
}
Loading
Loading