From e6576e908029c301ad8f8aea9b00a002d307411d Mon Sep 17 00:00:00 2001 From: mehdiMj Date: Sun, 21 Sep 2025 03:51:46 +0330 Subject: [PATCH] fix(alerting): Support custom slack title (#1079) --- README.md | 17 +++++++++-------- alerting/provider/slack/slack.go | 15 +++++++++++---- alerting/provider/slack/slack_test.go | 23 ++++++++++++++++++----- 3 files changed, 38 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index f9724df4..3e607978 100644 --- a/README.md +++ b/README.md @@ -1902,14 +1902,15 @@ endpoints: #### Configuring Slack alerts -| Parameter | Description | Default | -|:-----------------------------------|:-------------------------------------------------------------------------------------------|:--------------| -| `alerting.slack` | Configuration for alerts of type `slack` | `{}` | -| `alerting.slack.webhook-url` | Slack Webhook URL | Required `""` | -| `alerting.slack.default-alert` | Default alert configuration.
See [Setting a default alert](#setting-a-default-alert) | N/A | -| `alerting.slack.overrides` | List of overrides that may be prioritized over the default configuration | `[]` | -| `alerting.slack.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` | -| `alerting.slack.overrides[].*` | See `alerting.slack.*` parameters | `{}` | +| Parameter | Description | Default | +|:-----------------------------------|:-------------------------------------------------------------------------------------------|:------------------------------------| +| `alerting.slack` | Configuration for alerts of type `slack` | `{}` | +| `alerting.slack.webhook-url` | Slack Webhook URL | Required `""` | +| `alerting.slack.title` | Title of the notification | `":helmet_with_white_cross: Gatus"` | +| `alerting.slack.default-alert` | Default alert configuration.
See [Setting a default alert](#setting-a-default-alert) | N/A | +| `alerting.slack.overrides` | List of overrides that may be prioritized over the default configuration | `[]` | +| `alerting.slack.overrides[].group` | Endpoint group for which the configuration will be overridden by this configuration | `""` | +| `alerting.slack.overrides[].*` | See `alerting.slack.*` parameters | `{}` | ```yaml alerting: diff --git a/alerting/provider/slack/slack.go b/alerting/provider/slack/slack.go index e8f5376f..2cef6d3c 100644 --- a/alerting/provider/slack/slack.go +++ b/alerting/provider/slack/slack.go @@ -20,7 +20,8 @@ var ( ) type Config struct { - WebhookURL string `yaml:"webhook-url"` // Slack webhook URL + WebhookURL string `yaml:"webhook-url"` // Slack webhook URL + Title string `yaml:"title,omitempty"` // Title of the message that will be sent } func (cfg *Config) Validate() error { @@ -34,6 +35,9 @@ func (cfg *Config) Merge(override *Config) { if len(override.WebhookURL) > 0 { cfg.WebhookURL = override.WebhookURL } + if len(override.Title) > 0 { + cfg.Title = override.Title + } } // AlertProvider is the configuration necessary for sending an alert using Slack @@ -73,7 +77,7 @@ func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, r if err != nil { return err } - buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved)) + buffer := bytes.NewBuffer(provider.buildRequestBody(cfg, ep, alert, result, resolved)) request, err := http.NewRequest(http.MethodPost, cfg.WebhookURL, buffer) if err != nil { return err @@ -111,7 +115,7 @@ type Field struct { } // buildRequestBody builds the request body for the provider -func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte { +func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte { var message, color string if resolved { message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold) @@ -138,13 +142,16 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al Text: "", Attachments: []Attachment{ { - Title: ":helmet_with_white_cross: Gatus", + Title: cfg.Title, Text: message + description, Short: false, Color: color, }, }, } + if len(body.Attachments[0].Title) == 0 { + body.Attachments[0].Title = ":helmet_with_white_cross: Gatus" + } if len(formattedConditionResults) > 0 { body.Attachments[0].Fields = append(body.Attachments[0].Fields, Field{ Title: "Condition results", diff --git a/alerting/provider/slack/slack_test.go b/alerting/provider/slack/slack_test.go index aa9bd1d3..b696f695 100644 --- a/alerting/provider/slack/slack_test.go +++ b/alerting/provider/slack/slack_test.go @@ -150,7 +150,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { }{ { Name: "triggered", - Provider: AlertProvider{}, + Provider: AlertProvider{DefaultConfig: Config{WebhookURL: "http://example.com"}}, Endpoint: endpoint.Endpoint{Name: "name"}, Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3}, Resolved: false, @@ -158,7 +158,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { }, { Name: "triggered-with-group", - Provider: AlertProvider{}, + Provider: AlertProvider{DefaultConfig: Config{WebhookURL: "http://example.com"}}, Endpoint: endpoint.Endpoint{Name: "name", Group: "group"}, Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3}, Resolved: false, @@ -167,7 +167,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { { Name: "triggered-with-no-conditions", NoConditions: true, - Provider: AlertProvider{}, + Provider: AlertProvider{DefaultConfig: Config{WebhookURL: "http://example.com"}}, Endpoint: endpoint.Endpoint{Name: "name"}, Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3}, Resolved: false, @@ -175,7 +175,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { }, { Name: "resolved", - Provider: AlertProvider{}, + Provider: AlertProvider{DefaultConfig: Config{WebhookURL: "http://example.com"}}, Endpoint: endpoint.Endpoint{Name: "name"}, Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3}, Resolved: true, @@ -183,12 +183,20 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { }, { Name: "resolved-with-group", - Provider: AlertProvider{}, + Provider: AlertProvider{DefaultConfig: Config{WebhookURL: "http://example.com"}}, Endpoint: endpoint.Endpoint{Name: "name", Group: "group"}, Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3}, Resolved: true, ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *group/name* has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"short\":false,\"color\":\"#36A64F\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\"short\":false}]}]}", }, + { + Name: "resolved-with-group-and-custom-title", + Provider: AlertProvider{DefaultConfig: Config{WebhookURL: "http://example.com", Title: "custom title"}}, + Endpoint: endpoint.Endpoint{Name: "name", Group: "group"}, + Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3}, + Resolved: true, + ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\"custom title\",\"text\":\"An alert for *group/name* has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"short\":false,\"color\":\"#36A64F\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\"short\":false}]}]}", + }, } for _, scenario := range scenarios { t.Run(scenario.Name, func(t *testing.T) { @@ -199,7 +207,12 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { {Condition: "[STATUS] == 200", Success: scenario.Resolved}, } } + cfg, err := scenario.Provider.GetConfig(scenario.Endpoint.Group, &scenario.Alert) + if err != nil { + t.Fatal("couldn't get config:", err.Error()) + } body := scenario.Provider.buildRequestBody( + cfg, &scenario.Endpoint, &scenario.Alert, &endpoint.Result{