Skip to content

Commit c4764e4

Browse files
Add support for updating existing Slack messages
Signed-off-by: Kruchkov Alexandr <[email protected]>
1 parent 7d6cebe commit c4764e4

File tree

6 files changed

+274
-44
lines changed

6 files changed

+274
-44
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
!/cluster/testdata/*.yml
1515
!/config/testdata/*.yml
1616
!/examples/ha/tls/*.yml
17+
!/examples/slack-update-messages.yml
1718
!/notify/email/testdata/*.yml
1819
!/doc/examples/simple.yml
1920
!/circle.yml

config/notifiers.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,9 @@ type SlackConfig struct {
522522
LinkNames bool `yaml:"link_names" json:"link_names,omitempty"`
523523
MrkdwnIn []string `yaml:"mrkdwn_in,omitempty" json:"mrkdwn_in,omitempty"`
524524
Actions []*SlackAction `yaml:"actions,omitempty" json:"actions,omitempty"`
525+
// UpdateMessage enables updating existing Slack messages instead of creating new ones.
526+
// Requires bot token with chat:write scope. Webhook URLs do not support updates.
527+
UpdateMessage bool `yaml:"update_message" json:"update_message,omitempty"`
525528
// Timeout is the maximum time allowed to invoke the slack. Setting this to 0
526529
// does not impose a timeout.
527530
Timeout time.Duration `yaml:"timeout" json:"timeout"`

examples/slack-update-messages.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Alertmanager configuration with Slack message updates
2+
global:
3+
resolve_timeout: 5m
4+
5+
# Route configuration - required!
6+
route:
7+
receiver: 'slack'
8+
group_by: ['alertname', 'cluster', 'service']
9+
group_wait: 10s
10+
group_interval: 10s
11+
repeat_interval: 12h
12+
13+
receivers:
14+
- name: 'slack'
15+
slack_configs:
16+
- send_resolved: true # Required for message updates!
17+
api_url: 'https://slack.com/api/chat.postMessage'
18+
http_config:
19+
authorization:
20+
credentials: 'xoxb-your-bot-token'
21+
channel: '#your-channel'
22+
update_message: true # This enables message updates!
23+
24+
# Template for updated messages
25+
title: '{{ .GroupLabels.alertname }} - {{ .Status | toUpper }}'
26+
text: |
27+
{{ if eq .Status "firing" }}
28+
🔥 *{{ .Alerts.Firing | len }} alert(s) firing*
29+
{{ else }}
30+
✅ *All alerts resolved*
31+
{{ end }}
32+
33+
{{ range .Alerts }}
34+
• {{ if .Annotations.summary }}{{ .Annotations.summary }}{{ else }}{{ .Labels.alertname }}{{ end }}
35+
{{ end }}
36+
37+
color: '{{ if eq .Status "firing" }}danger{{ else }}good{{ end }}'

nflog/nflogpb/nflog.proto

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ message Entry {
3939
repeated uint64 firing_alerts = 6;
4040
// ResolvedAlerts list of hashes of resolved alerts at the last notification time.
4141
repeated uint64 resolved_alerts = 7;
42+
// Metadata holds integration-specific metadata (e.g. Slack message_ts, Jira issue key).
43+
// This allows integrations to store identifiers for updating existing notifications.
44+
map<string, string> metadata = 8;
4245
}
4346

4447
// MeshEntry is a wrapper message to communicate a notify log

notify/metadata_store.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright 2024 Prometheus Team
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package notify
15+
16+
import (
17+
"fmt"
18+
"sync"
19+
20+
"github.com/prometheus/alertmanager/nflog/nflogpb"
21+
)
22+
23+
// MetadataStore is a temporary in-memory store for notification metadata
24+
// (e.g., Slack message_ts, Jira issue keys) until protobuf Entry supports metadata field.
25+
// This allows integrations to update existing notifications instead of creating new ones.
26+
type MetadataStore struct {
27+
mtx sync.RWMutex
28+
data map[string]map[string]string // key: stateKey(groupKey, receiver) -> metadata map
29+
}
30+
31+
// NewMetadataStore creates a new MetadataStore.
32+
func NewMetadataStore() *MetadataStore {
33+
return &MetadataStore{
34+
data: make(map[string]map[string]string),
35+
}
36+
}
37+
38+
// Set stores metadata for a given receiver and group key.
39+
func (s *MetadataStore) Set(receiver *nflogpb.Receiver, groupKey string, metadata map[string]string) {
40+
s.mtx.Lock()
41+
defer s.mtx.Unlock()
42+
43+
key := stateKey(groupKey, receiver)
44+
s.data[key] = metadata
45+
}
46+
47+
// Get retrieves metadata for a given receiver and group key.
48+
func (s *MetadataStore) Get(receiver *nflogpb.Receiver, groupKey string) (map[string]string, bool) {
49+
s.mtx.RLock()
50+
defer s.mtx.RUnlock()
51+
52+
key := stateKey(groupKey, receiver)
53+
metadata, ok := s.data[key]
54+
return metadata, ok
55+
}
56+
57+
// Delete removes metadata for a given receiver and group key.
58+
func (s *MetadataStore) Delete(receiver *nflogpb.Receiver, groupKey string) {
59+
s.mtx.Lock()
60+
defer s.mtx.Unlock()
61+
62+
key := stateKey(groupKey, receiver)
63+
delete(s.data, key)
64+
}
65+
66+
// stateKey returns a string key for a log entry consisting of the group key and receiver.
67+
// This matches the key generation in nflog.
68+
func stateKey(gkey string, r *nflogpb.Receiver) string {
69+
return receiverKey(gkey, r)
70+
}
71+
72+
// receiverKey creates a unique key from group key and receiver.
73+
// Format matches nflog's internal stateKey format: "groupKey:groupName/integration/idx"
74+
func receiverKey(groupKey string, r *nflogpb.Receiver) string {
75+
return groupKey + ":" + receiverString(r)
76+
}
77+
78+
// receiverString returns a string representation of the receiver.
79+
func receiverString(r *nflogpb.Receiver) string {
80+
return fmt.Sprintf("%s/%s/%d", r.GroupName, r.Integration, r.Idx)
81+
}
82+

0 commit comments

Comments
 (0)