Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
!/cluster/testdata/*.yml
!/config/testdata/*.yml
!/examples/ha/tls/*.yml
!/examples/slack-update-messages.yml
!/notify/email/testdata/*.yml
!/doc/examples/simple.yml
!/circle.yml
Expand Down
3 changes: 3 additions & 0 deletions config/notifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,9 @@ type SlackConfig struct {
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"`
// UpdateMessage enables updating existing Slack messages instead of creating new ones.
// Requires bot token with chat:write scope. Webhook URLs do not support updates.
UpdateMessage bool `yaml:"update_message" json:"update_message,omitempty"`
// Timeout is the maximum time allowed to invoke the slack. Setting this to 0
// does not impose a timeout.
Timeout time.Duration `yaml:"timeout" json:"timeout"`
Expand Down
37 changes: 37 additions & 0 deletions examples/slack-update-messages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Alertmanager configuration with Slack message updates
global:
resolve_timeout: 5m

# Route configuration - required!
route:
receiver: 'slack'
group_by: ['alertname', 'cluster', 'service']
group_wait: 10s
group_interval: 10s
repeat_interval: 12h

receivers:
- name: 'slack'
slack_configs:
- send_resolved: true # Required for message updates!
api_url: 'https://slack.com/api/chat.postMessage'
http_config:
authorization:
credentials: 'xoxb-your-bot-token'
channel: '#your-channel'
update_message: true # This enables message updates!

# Template for updated messages
title: '{{ .GroupLabels.alertname }} - {{ .Status | toUpper }}'
text: |
{{ if eq .Status "firing" }}
πŸ”₯ *{{ .Alerts.Firing | len }} alert(s) firing*
{{ else }}
βœ… *All alerts resolved*
{{ end }}

{{ range .Alerts }}
β€’ {{ if .Annotations.summary }}{{ .Annotations.summary }}{{ else }}{{ .Labels.alertname }}{{ end }}
{{ end }}

color: '{{ if eq .Status "firing" }}danger{{ else }}good{{ end }}'
3 changes: 3 additions & 0 deletions nflog/nflogpb/nflog.proto
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ message Entry {
repeated uint64 firing_alerts = 6;
// ResolvedAlerts list of hashes of resolved alerts at the last notification time.
repeated uint64 resolved_alerts = 7;
// Metadata holds integration-specific metadata (e.g. Slack message_ts, Jira issue key).
// This allows integrations to store identifiers for updating existing notifications.
map<string, string> metadata = 8;
}

// MeshEntry is a wrapper message to communicate a notify log
Expand Down
81 changes: 81 additions & 0 deletions notify/metadata_store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// 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 notify

import (
"fmt"
"sync"

"github.com/prometheus/alertmanager/nflog/nflogpb"
)

// MetadataStore is a temporary in-memory store for notification metadata
// (e.g., Slack message_ts, Jira issue keys) until protobuf Entry supports metadata field.
// This allows integrations to update existing notifications instead of creating new ones.
type MetadataStore struct {
mtx sync.RWMutex
data map[string]map[string]string // key: stateKey(groupKey, receiver) -> metadata map
}

// NewMetadataStore creates a new MetadataStore.
func NewMetadataStore() *MetadataStore {
return &MetadataStore{
data: make(map[string]map[string]string),
}
}

// Set stores metadata for a given receiver and group key.
func (s *MetadataStore) Set(receiver *nflogpb.Receiver, groupKey string, metadata map[string]string) {
s.mtx.Lock()
defer s.mtx.Unlock()

key := stateKey(groupKey, receiver)
s.data[key] = metadata
}

// Get retrieves metadata for a given receiver and group key.
func (s *MetadataStore) Get(receiver *nflogpb.Receiver, groupKey string) (map[string]string, bool) {
s.mtx.RLock()
defer s.mtx.RUnlock()

key := stateKey(groupKey, receiver)
metadata, ok := s.data[key]
return metadata, ok
}

// Delete removes metadata for a given receiver and group key.
func (s *MetadataStore) Delete(receiver *nflogpb.Receiver, groupKey string) {
s.mtx.Lock()
defer s.mtx.Unlock()

key := stateKey(groupKey, receiver)
delete(s.data, key)
}

// stateKey returns a string key for a log entry consisting of the group key and receiver.
// This matches the key generation in nflog.
func stateKey(gkey string, r *nflogpb.Receiver) string {
return receiverKey(gkey, r)
}

// receiverKey creates a unique key from group key and receiver.
// Format matches nflog's internal stateKey format: "groupKey:groupName/integration/idx".
func receiverKey(groupKey string, r *nflogpb.Receiver) string {
return groupKey + ":" + receiverString(r)
}

// receiverString returns a string representation of the receiver.
func receiverString(r *nflogpb.Receiver) string {
return fmt.Sprintf("%s/%s/%d", r.GroupName, r.Integration, r.Idx)
}
Loading