From 241956b28cfe5e736dae5a1aa04caa924d23bcd1 Mon Sep 17 00:00:00 2001 From: TwiN Date: Wed, 10 Apr 2024 20:46:17 -0400 Subject: [PATCH] fix(alerting): Support alerts with no conditions for external endpoints (#729) --- alerting/provider/awsses/awsses.go | 23 ++++++---- alerting/provider/discord/discord.go | 28 +++++------ alerting/provider/discord/discord_test.go | 23 ++++++++-- alerting/provider/email/email.go | 22 +++++---- alerting/provider/github/github.go | 21 +++++---- alerting/provider/github/github_test.go | 25 +++++++--- alerting/provider/gitlab/gitlab.go | 31 +++++++------ alerting/provider/googlechat/googlechat.go | 26 ++++++----- alerting/provider/gotify/gotify.go | 11 +++-- .../{space.go => jetbrainsspace.go} | 14 +++--- .../{space_test.go => jetbrainsspace_test.go} | 0 alerting/provider/matrix/matrix.go | 46 +++++++++++-------- alerting/provider/matrix/matrix_test.go | 36 +++++++-------- alerting/provider/mattermost/mattermost.go | 40 ++++++++-------- alerting/provider/ntfy/ntfy.go | 6 +-- alerting/provider/opsgenie/opsgenie.go | 7 +-- alerting/provider/provider.go | 2 +- alerting/provider/slack/slack.go | 28 +++++------ alerting/provider/slack/slack_test.go | 22 +++++++-- alerting/provider/teams/teams.go | 25 +++++----- alerting/provider/teams/teams_test.go | 23 +++++++--- alerting/provider/telegram/telegram.go | 28 ++++++----- alerting/provider/telegram/telegram_test.go | 23 +++++++--- watchdog/alerting_test.go | 2 +- 24 files changed, 303 insertions(+), 209 deletions(-) rename alerting/provider/jetbrainsspace/{space.go => jetbrainsspace.go} (96%) rename alerting/provider/jetbrainsspace/{space_test.go => jetbrainsspace_test.go} (100%) diff --git a/alerting/provider/awsses/awsses.go b/alerting/provider/awsses/awsses.go index 96234b5f..17ba05cc 100644 --- a/alerting/provider/awsses/awsses.go +++ b/alerting/provider/awsses/awsses.go @@ -50,7 +50,6 @@ func (provider *AlertProvider) IsValid() bool { registeredGroups[override.Group] = true } } - // if both AccessKeyID and SecretAccessKey are specified, we'll use these to authenticate, // otherwise if neither are specified, then we'll fall back on IAM authentication. return len(provider.From) > 0 && len(provider.To) > 0 && @@ -112,7 +111,7 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, // buildMessageSubjectAndBody builds the message subject and body func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) (string, string) { - var subject, message, results string + var subject, message string if resolved { subject = fmt.Sprintf("[%s] Alert resolved", endpoint.DisplayName()) message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) @@ -120,20 +119,24 @@ func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoin subject = fmt.Sprintf("[%s] Alert triggered", endpoint.DisplayName()) message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) } - for _, conditionResult := range result.ConditionResults { - var prefix string - if conditionResult.Success { - prefix = "✅" - } else { - prefix = "❌" + var formattedConditionResults string + if len(result.ConditionResults) > 0 { + formattedConditionResults = "\n\nCondition results:\n" + for _, conditionResult := range result.ConditionResults { + var prefix string + if conditionResult.Success { + prefix = "✅" + } else { + prefix = "❌" + } + formattedConditionResults += fmt.Sprintf("%s %s\n", prefix, conditionResult.Condition) } - results += fmt.Sprintf("%s %s\n", prefix, conditionResult.Condition) } var description string if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { description = "\n\nAlert description: " + alertDescription } - return subject, message + description + "\n\nCondition results:\n" + results + return subject, message + description + formattedConditionResults } // getToForGroup returns the appropriate email integration to for a given group diff --git a/alerting/provider/discord/discord.go b/alerting/provider/discord/discord.go index 0696a610..d4ac2c96 100644 --- a/alerting/provider/discord/discord.go +++ b/alerting/provider/discord/discord.go @@ -75,7 +75,7 @@ type Embed struct { Title string `json:"title"` Description string `json:"description"` Color int `json:"color"` - Fields []Field `json:"fields"` + Fields []Field `json:"fields,omitempty"` } type Field struct { @@ -86,7 +86,7 @@ type Field struct { // buildRequestBody builds the request body for the provider func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { - var message, results string + var message string var colorCode int if resolved { message = fmt.Sprintf("An alert for **%s** has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) @@ -95,6 +95,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert * message = fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) colorCode = 15158332 } + var formattedConditionResults string for _, conditionResult := range result.ConditionResults { var prefix string if conditionResult.Success { @@ -102,7 +103,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert * } else { prefix = ":x:" } - results += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition) + formattedConditionResults += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition) } var description string if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { @@ -112,24 +113,25 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert * if provider.Title != "" { title = provider.Title } - body, _ := json.Marshal(Body{ + body := Body{ Content: "", Embeds: []Embed{ { Title: title, Description: message + description, Color: colorCode, - Fields: []Field{ - { - Name: "Condition results", - Value: results, - Inline: false, - }, - }, }, }, - }) - return body + } + if len(formattedConditionResults) > 0 { + body.Embeds[0].Fields = append(body.Embeds[0].Fields, Field{ + Name: "Condition results", + Value: formattedConditionResults, + Inline: false, + }) + } + bodyAsJSON, _ := json.Marshal(body) + return bodyAsJSON } // getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group diff --git a/alerting/provider/discord/discord_test.go b/alerting/provider/discord/discord_test.go index bb69cd83..2c8f1230 100644 --- a/alerting/provider/discord/discord_test.go +++ b/alerting/provider/discord/discord_test.go @@ -155,6 +155,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { Name string Provider AlertProvider Alert alert.Alert + NoConditions bool Resolved bool ExpectedBody string }{ @@ -179,18 +180,30 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { Resolved: false, ExpectedBody: "{\"content\":\"\",\"embeds\":[{\"title\":\"provider-title\",\"description\":\"An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"color\":15158332,\"fields\":[{\"name\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n:x: - `[BODY] != \\\"\\\"`\\n\",\"inline\":false}]}]}", }, + { + Name: "triggered-with-no-conditions", + NoConditions: true, + Provider: AlertProvider{Title: title}, + Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3}, + Resolved: false, + ExpectedBody: "{\"content\":\"\",\"embeds\":[{\"title\":\"provider-title\",\"description\":\"An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"color\":15158332}]}", + }, } for _, scenario := range scenarios { t.Run(scenario.Name, func(t *testing.T) { + var conditionResults []*core.ConditionResult + if !scenario.NoConditions { + conditionResults = []*core.ConditionResult{ + {Condition: "[CONNECTED] == true", Success: scenario.Resolved}, + {Condition: "[STATUS] == 200", Success: scenario.Resolved}, + {Condition: "[BODY] != \"\"", Success: scenario.Resolved}, + } + } body := scenario.Provider.buildRequestBody( &core.Endpoint{Name: "endpoint-name"}, &scenario.Alert, &core.Result{ - ConditionResults: []*core.ConditionResult{ - {Condition: "[CONNECTED] == true", Success: scenario.Resolved}, - {Condition: "[STATUS] == 200", Success: scenario.Resolved}, - {Condition: "[BODY] != \"\"", Success: scenario.Resolved}, - }, + ConditionResults: conditionResults, }, scenario.Resolved, ) diff --git a/alerting/provider/email/email.go b/alerting/provider/email/email.go index 67a6dea8..e2fe542b 100644 --- a/alerting/provider/email/email.go +++ b/alerting/provider/email/email.go @@ -88,7 +88,7 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, // buildMessageSubjectAndBody builds the message subject and body func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) (string, string) { - var subject, message, results string + var subject, message string if resolved { subject = fmt.Sprintf("[%s] Alert resolved", endpoint.DisplayName()) message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) @@ -96,20 +96,24 @@ func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoin subject = fmt.Sprintf("[%s] Alert triggered", endpoint.DisplayName()) message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) } - for _, conditionResult := range result.ConditionResults { - var prefix string - if conditionResult.Success { - prefix = "✅" - } else { - prefix = "❌" + var formattedConditionResults string + if len(result.ConditionResults) > 0 { + formattedConditionResults = "\n\nCondition results:\n" + for _, conditionResult := range result.ConditionResults { + var prefix string + if conditionResult.Success { + prefix = "✅" + } else { + prefix = "❌" + } + formattedConditionResults += fmt.Sprintf("%s %s\n", prefix, conditionResult.Condition) } - results += fmt.Sprintf("%s %s\n", prefix, conditionResult.Condition) } var description string if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { description = "\n\nAlert description: " + alertDescription } - return subject, message + description + "\n\nCondition results:\n" + results + return subject, message + description + formattedConditionResults } // getToForGroup returns the appropriate email integration to for a given group diff --git a/alerting/provider/github/github.go b/alerting/provider/github/github.go index fa25022c..f86b9053 100644 --- a/alerting/provider/github/github.go +++ b/alerting/provider/github/github.go @@ -105,22 +105,25 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, // buildIssueBody builds the body of the issue func (provider *AlertProvider) buildIssueBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result) string { - var results string - for _, conditionResult := range result.ConditionResults { - var prefix string - if conditionResult.Success { - prefix = ":white_check_mark:" - } else { - prefix = ":x:" + var formattedConditionResults string + if len(result.ConditionResults) > 0 { + formattedConditionResults = "\n\n## Condition results\n" + for _, conditionResult := range result.ConditionResults { + var prefix string + if conditionResult.Success { + prefix = ":white_check_mark:" + } else { + prefix = ":x:" + } + formattedConditionResults += fmt.Sprintf("- %s - `%s`\n", prefix, conditionResult.Condition) } - results += fmt.Sprintf("- %s - `%s`\n", prefix, conditionResult.Condition) } var description string if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { description = ":\n> " + alertDescription } message := fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) - return message + description + "\n\n## Condition results\n" + results + return message + description + formattedConditionResults } // GetDefaultAlert returns the provider's default alert configuration diff --git a/alerting/provider/github/github_test.go b/alerting/provider/github/github_test.go index c16e6f62..d1f45e48 100644 --- a/alerting/provider/github/github_test.go +++ b/alerting/provider/github/github_test.go @@ -112,6 +112,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { Endpoint core.Endpoint Provider AlertProvider Alert alert.Alert + NoConditions bool ExpectedBody string }{ { @@ -122,24 +123,34 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\n> description-1\n\n## Condition results\n- :white_check_mark: - `[CONNECTED] == true`\n- :x: - `[STATUS] == 200`", }, { - Name: "no-description", + Name: "triggered-with-no-description", Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"}, Provider: AlertProvider{}, Alert: alert.Alert{FailureThreshold: 10}, ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 10 time(s) in a row\n\n## Condition results\n- :white_check_mark: - `[CONNECTED] == true`\n- :x: - `[STATUS] == 200`", }, + { + Name: "triggered-with-no-conditions", + NoConditions: true, + Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"}, + Provider: AlertProvider{}, + Alert: alert.Alert{Description: &firstDescription, FailureThreshold: 10}, + ExpectedBody: "An alert for **endpoint-name** has been triggered due to having failed 10 time(s) in a row:\n> description-1", + }, } for _, scenario := range scenarios { t.Run(scenario.Name, func(t *testing.T) { + var conditionResults []*core.ConditionResult + if !scenario.NoConditions { + conditionResults = []*core.ConditionResult{ + {Condition: "[CONNECTED] == true", Success: true}, + {Condition: "[STATUS] == 200", Success: false}, + } + } body := scenario.Provider.buildIssueBody( &scenario.Endpoint, &scenario.Alert, - &core.Result{ - ConditionResults: []*core.ConditionResult{ - {Condition: "[CONNECTED] == true", Success: true}, - {Condition: "[STATUS] == 200", Success: false}, - }, - }, + &core.Result{ConditionResults: conditionResults}, ) if strings.TrimSpace(body) != strings.TrimSpace(scenario.ExpectedBody) { t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body) diff --git a/alerting/provider/gitlab/gitlab.go b/alerting/provider/gitlab/gitlab.go index 11d337ff..f87b4c4e 100644 --- a/alerting/provider/gitlab/gitlab.go +++ b/alerting/provider/gitlab/gitlab.go @@ -25,10 +25,13 @@ type AlertProvider struct { // Severity can be one of: critical, high, medium, low, info, unknown. Defaults to critical Severity string `yaml:"severity,omitempty"` + // MonitoringTool overrides the name sent to gitlab. Defaults to gatus MonitoringTool string `yaml:"monitoring-tool,omitempty"` + // EnvironmentName is the name of the associated GitLab environment. Required to display alerts on a dashboard. EnvironmentName string `yaml:"environment-name,omitempty"` + // Service affected. Defaults to endpoint display name Service string `yaml:"service,omitempty"` } @@ -52,7 +55,6 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, if len(alert.ResolveKey) == 0 { alert.ResolveKey = uuid.NewString() } - buffer := bytes.NewBuffer(provider.buildAlertBody(endpoint, alert, result, resolved)) request, err := http.NewRequest(http.MethodPost, provider.WebhookURL, buffer) if err != nil { @@ -114,16 +116,18 @@ func (provider *AlertProvider) buildAlertBody(endpoint *core.Endpoint, alert *al if resolved { body.EndTime = result.Timestamp.Format(time.RFC3339) } - - var results string - for _, conditionResult := range result.ConditionResults { - var prefix string - if conditionResult.Success { - prefix = ":white_check_mark:" - } else { - prefix = ":x:" + var formattedConditionResults string + if len(result.ConditionResults) > 0 { + formattedConditionResults = "\n\n## Condition results\n" + for _, conditionResult := range result.ConditionResults { + var prefix string + if conditionResult.Success { + prefix = ":white_check_mark:" + } else { + prefix = ":x:" + } + formattedConditionResults += fmt.Sprintf("- %s - `%s`\n", prefix, conditionResult.Condition) } - results += fmt.Sprintf("- %s - `%s`\n", prefix, conditionResult.Condition) } var description string if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { @@ -135,10 +139,9 @@ func (provider *AlertProvider) buildAlertBody(endpoint *core.Endpoint, alert *al } else { message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) } - body.Description = message + description + "\n\n## Condition results\n" + results - - json, _ := json.Marshal(body) - return json + body.Description = message + description + formattedConditionResults + bodyAsJSON, _ := json.Marshal(body) + return bodyAsJSON } // GetDefaultAlert returns the provider's default alert configuration diff --git a/alerting/provider/googlechat/googlechat.go b/alerting/provider/googlechat/googlechat.go index ea1b4b8c..19330c92 100644 --- a/alerting/provider/googlechat/googlechat.go +++ b/alerting/provider/googlechat/googlechat.go @@ -121,7 +121,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert * color = "#DD0000" message = fmt.Sprintf("An alert has been triggered due to having failed %d time(s) in a row", color, alert.FailureThreshold) } - var results string + var formattedConditionResults string for _, conditionResult := range result.ConditionResults { var prefix string if conditionResult.Success { @@ -129,7 +129,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert * } else { prefix = "❌" } - results += fmt.Sprintf("%s %s
", prefix, conditionResult.Condition) + formattedConditionResults += fmt.Sprintf("%s %s
", prefix, conditionResult.Condition) } var description string if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { @@ -150,20 +150,22 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert * Icon: "BOOKMARK", }, }, - { - KeyValue: &KeyValue{ - TopLabel: "Condition results", - Content: results, - ContentMultiline: "true", - Icon: "DESCRIPTION", - }, - }, }, }, }, }, }, } + if len(formattedConditionResults) > 0 { + payload.Cards[0].Sections[0].Widgets = append(payload.Cards[0].Sections[0].Widgets, Widgets{ + KeyValue: &KeyValue{ + TopLabel: "Condition results", + Content: formattedConditionResults, + ContentMultiline: "true", + Icon: "DESCRIPTION", + }, + }) + } if endpoint.Type() == core.EndpointTypeHTTP { // We only include a button targeting the URL if the endpoint is an HTTP endpoint // If the URL isn't prefixed with https://, Google Chat will just display a blank message aynways. @@ -179,8 +181,8 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert * }, }) } - body, _ := json.Marshal(payload) - return body + bodyAsJSON, _ := json.Marshal(payload) + return bodyAsJSON } // getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group diff --git a/alerting/provider/gotify/gotify.go b/alerting/provider/gotify/gotify.go index e6e35a7c..7f4bfcf6 100644 --- a/alerting/provider/gotify/gotify.go +++ b/alerting/provider/gotify/gotify.go @@ -68,12 +68,13 @@ type Body struct { // buildRequestBody builds the request body for the provider func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { - var message, results string + var message string if resolved { message = fmt.Sprintf("An alert for `%s` has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) } else { message = fmt.Sprintf("An alert for `%s` has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) } + var formattedConditionResults string for _, conditionResult := range result.ConditionResults { var prefix string if conditionResult.Success { @@ -81,22 +82,22 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert * } else { prefix = "✕" } - results += fmt.Sprintf("\n%s - %s", prefix, conditionResult.Condition) + formattedConditionResults += fmt.Sprintf("\n%s - %s", prefix, conditionResult.Condition) } if len(alert.GetDescription()) > 0 { message += " with the following description: " + alert.GetDescription() } - message += results + message += formattedConditionResults title := "Gatus: " + endpoint.DisplayName() if provider.Title != "" { title = provider.Title } - body, _ := json.Marshal(Body{ + bodyAsJSON, _ := json.Marshal(Body{ Message: message, Title: title, Priority: provider.Priority, }) - return body + return bodyAsJSON } // GetDefaultAlert returns the provider's default alert configuration diff --git a/alerting/provider/jetbrainsspace/space.go b/alerting/provider/jetbrainsspace/jetbrainsspace.go similarity index 96% rename from alerting/provider/jetbrainsspace/space.go rename to alerting/provider/jetbrainsspace/jetbrainsspace.go index 55046450..bf031663 100644 --- a/alerting/provider/jetbrainsspace/space.go +++ b/alerting/provider/jetbrainsspace/jetbrainsspace.go @@ -17,8 +17,10 @@ type AlertProvider struct { Project string `yaml:"project"` // JetBrains Space Project name ChannelID string `yaml:"channel-id"` // JetBrains Space Chat Channel ID Token string `yaml:"token"` // JetBrains Space Bearer Token - // DefaultAlert is the defarlt alert configuration to use for endpoints with an alert of the appropriate type + + // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"` + // Overrides is a list of Override that may be prioritized over the default configuration Overrides []Override `yaml:"overrides,omitempty"` } @@ -73,7 +75,7 @@ type Body struct { type Content struct { ClassName string `json:"className"` Style string `json:"style"` - Sections []Section `json:"sections"` + Sections []Section `json:"sections,omitempty"` } type Section struct { @@ -112,7 +114,6 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert * }}, }, } - if resolved { body.Content.Style = "SUCCESS" body.Content.Sections[0].Header = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) @@ -120,7 +121,6 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert * body.Content.Style = "WARNING" body.Content.Sections[0].Header = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) } - for _, conditionResult := range result.ConditionResults { icon := "warning" style := "WARNING" @@ -128,7 +128,6 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert * icon = "success" style = "SUCCESS" } - body.Content.Sections[0].Elements = append(body.Content.Sections[0].Elements, Element{ ClassName: "MessageText", Accessory: Accessory{ @@ -141,9 +140,8 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert * Content: conditionResult.Condition, }) } - - jsonBody, _ := json.Marshal(body) - return jsonBody + bodyAsJSON, _ := json.Marshal(body) + return bodyAsJSON } // getChannelIDForGroup returns the appropriate channel ID to for a given group override diff --git a/alerting/provider/jetbrainsspace/space_test.go b/alerting/provider/jetbrainsspace/jetbrainsspace_test.go similarity index 100% rename from alerting/provider/jetbrainsspace/space_test.go rename to alerting/provider/jetbrainsspace/jetbrainsspace_test.go diff --git a/alerting/provider/matrix/matrix.go b/alerting/provider/matrix/matrix.go index 7a6ff17c..92907889 100644 --- a/alerting/provider/matrix/matrix.go +++ b/alerting/provider/matrix/matrix.go @@ -17,7 +17,7 @@ import ( // AlertProvider is the configuration necessary for sending an alert using Matrix type AlertProvider struct { - MatrixProviderConfig `yaml:",inline"` + ProviderConfig `yaml:",inline"` // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"` @@ -30,12 +30,12 @@ type AlertProvider struct { type Override struct { Group string `yaml:"group"` - MatrixProviderConfig `yaml:",inline"` + ProviderConfig `yaml:",inline"` } -const defaultHomeserverURL = "https://matrix-client.matrix.org" +const defaultServerURL = "https://matrix-client.matrix.org" -type MatrixProviderConfig struct { +type ProviderConfig struct { // ServerURL is the custom homeserver to use (optional) ServerURL string `yaml:"server-url"` @@ -65,7 +65,7 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved)) config := provider.getConfigForGroup(endpoint.Group) if config.ServerURL == "" { - config.ServerURL = defaultHomeserverURL + config.ServerURL = defaultServerURL } // The Matrix endpoint requires a unique transaction ID for each event sent txnId := randStringBytes(24) @@ -115,12 +115,13 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert * // buildPlaintextMessageBody builds the message body in plaintext to include in request func buildPlaintextMessageBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string { - var message, results string + var message string if resolved { message = fmt.Sprintf("An alert for `%s` has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) } else { message = fmt.Sprintf("An alert for `%s` has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) } + var formattedConditionResults string for _, conditionResult := range result.ConditionResults { var prefix string if conditionResult.Success { @@ -128,49 +129,54 @@ func buildPlaintextMessageBody(endpoint *core.Endpoint, alert *alert.Alert, resu } else { prefix = "✕" } - results += fmt.Sprintf("\n%s - %s", prefix, conditionResult.Condition) + formattedConditionResults += fmt.Sprintf("\n%s - %s", prefix, conditionResult.Condition) } var description string if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { description = "\n" + alertDescription } - return fmt.Sprintf("%s%s\n%s", message, description, results) + return fmt.Sprintf("%s%s\n%s", message, description, formattedConditionResults) } // buildHTMLMessageBody builds the message body in HTML to include in request func buildHTMLMessageBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string { - var message, results string + var message string if resolved { message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) } else { message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) } - for _, conditionResult := range result.ConditionResults { - var prefix string - if conditionResult.Success { - prefix = "✅" - } else { - prefix = "❌" + var formattedConditionResults string + if len(result.ConditionResults) > 0 { + formattedConditionResults = "\n
Condition results
" } var description string if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { description = fmt.Sprintf("\n
%s
", alertDescription) } - return fmt.Sprintf("

%s

%s\n
Condition results
", message, description, results) + return fmt.Sprintf("

%s

%s%s", message, description, formattedConditionResults) } // getConfigForGroup returns the appropriate configuration for a given group -func (provider *AlertProvider) getConfigForGroup(group string) MatrixProviderConfig { +func (provider *AlertProvider) getConfigForGroup(group string) ProviderConfig { if provider.Overrides != nil { for _, override := range provider.Overrides { if group == override.Group { - return override.MatrixProviderConfig + return override.ProviderConfig } } } - return provider.MatrixProviderConfig + return provider.ProviderConfig } func randStringBytes(n int) string { diff --git a/alerting/provider/matrix/matrix_test.go b/alerting/provider/matrix/matrix_test.go index 00b6b131..2d9ae57a 100644 --- a/alerting/provider/matrix/matrix_test.go +++ b/alerting/provider/matrix/matrix_test.go @@ -13,7 +13,7 @@ import ( func TestAlertProvider_IsValid(t *testing.T) { invalidProvider := AlertProvider{ - MatrixProviderConfig: MatrixProviderConfig{ + ProviderConfig: ProviderConfig{ AccessToken: "", InternalRoomID: "", }, @@ -22,7 +22,7 @@ func TestAlertProvider_IsValid(t *testing.T) { t.Error("provider shouldn't have been valid") } validProvider := AlertProvider{ - MatrixProviderConfig: MatrixProviderConfig{ + ProviderConfig: ProviderConfig{ AccessToken: "1", InternalRoomID: "!a:example.com", }, @@ -31,7 +31,7 @@ func TestAlertProvider_IsValid(t *testing.T) { t.Error("provider should've been valid") } validProviderWithHomeserver := AlertProvider{ - MatrixProviderConfig: MatrixProviderConfig{ + ProviderConfig: ProviderConfig{ ServerURL: "https://example.com", AccessToken: "1", InternalRoomID: "!a:example.com", @@ -47,7 +47,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) { Overrides: []Override{ { Group: "", - MatrixProviderConfig: MatrixProviderConfig{ + ProviderConfig: ProviderConfig{ AccessToken: "", InternalRoomID: "", }, @@ -61,7 +61,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) { Overrides: []Override{ { Group: "group", - MatrixProviderConfig: MatrixProviderConfig{ + ProviderConfig: ProviderConfig{ AccessToken: "", InternalRoomID: "", }, @@ -72,14 +72,14 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) { t.Error("provider integration key shouldn't have been valid") } providerWithValidOverride := AlertProvider{ - MatrixProviderConfig: MatrixProviderConfig{ + ProviderConfig: ProviderConfig{ AccessToken: "1", InternalRoomID: "!a:example.com", }, Overrides: []Override{ { Group: "group", - MatrixProviderConfig: MatrixProviderConfig{ + ProviderConfig: ProviderConfig{ ServerURL: "https://example.com", AccessToken: "1", InternalRoomID: "!a:example.com", @@ -232,12 +232,12 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) { Name string Provider AlertProvider InputGroup string - ExpectedOutput MatrixProviderConfig + ExpectedOutput ProviderConfig }{ { Name: "provider-no-override-specify-no-group-should-default", Provider: AlertProvider{ - MatrixProviderConfig: MatrixProviderConfig{ + ProviderConfig: ProviderConfig{ ServerURL: "https://example.com", AccessToken: "1", InternalRoomID: "!a:example.com", @@ -245,7 +245,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) { Overrides: nil, }, InputGroup: "", - ExpectedOutput: MatrixProviderConfig{ + ExpectedOutput: ProviderConfig{ ServerURL: "https://example.com", AccessToken: "1", InternalRoomID: "!a:example.com", @@ -254,7 +254,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) { { Name: "provider-no-override-specify-group-should-default", Provider: AlertProvider{ - MatrixProviderConfig: MatrixProviderConfig{ + ProviderConfig: ProviderConfig{ ServerURL: "https://example.com", AccessToken: "1", InternalRoomID: "!a:example.com", @@ -262,7 +262,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) { Overrides: nil, }, InputGroup: "group", - ExpectedOutput: MatrixProviderConfig{ + ExpectedOutput: ProviderConfig{ ServerURL: "https://example.com", AccessToken: "1", InternalRoomID: "!a:example.com", @@ -271,7 +271,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) { { Name: "provider-with-override-specify-no-group-should-default", Provider: AlertProvider{ - MatrixProviderConfig: MatrixProviderConfig{ + ProviderConfig: ProviderConfig{ ServerURL: "https://example.com", AccessToken: "1", InternalRoomID: "!a:example.com", @@ -279,7 +279,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) { Overrides: []Override{ { Group: "group", - MatrixProviderConfig: MatrixProviderConfig{ + ProviderConfig: ProviderConfig{ ServerURL: "https://example01.com", AccessToken: "12", InternalRoomID: "!a:example01.com", @@ -288,7 +288,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) { }, }, InputGroup: "", - ExpectedOutput: MatrixProviderConfig{ + ExpectedOutput: ProviderConfig{ ServerURL: "https://example.com", AccessToken: "1", InternalRoomID: "!a:example.com", @@ -297,7 +297,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) { { Name: "provider-with-override-specify-group-should-override", Provider: AlertProvider{ - MatrixProviderConfig: MatrixProviderConfig{ + ProviderConfig: ProviderConfig{ ServerURL: "https://example.com", AccessToken: "1", InternalRoomID: "!a:example.com", @@ -305,7 +305,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) { Overrides: []Override{ { Group: "group", - MatrixProviderConfig: MatrixProviderConfig{ + ProviderConfig: ProviderConfig{ ServerURL: "https://example01.com", AccessToken: "12", InternalRoomID: "!a:example01.com", @@ -314,7 +314,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) { }, }, InputGroup: "group", - ExpectedOutput: MatrixProviderConfig{ + ExpectedOutput: ProviderConfig{ ServerURL: "https://example01.com", AccessToken: "12", InternalRoomID: "!a:example01.com", diff --git a/alerting/provider/mattermost/mattermost.go b/alerting/provider/mattermost/mattermost.go index c5fb9139..41b34542 100644 --- a/alerting/provider/mattermost/mattermost.go +++ b/alerting/provider/mattermost/mattermost.go @@ -93,7 +93,7 @@ type Field struct { // buildRequestBody builds the request body for the provider func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { - var message, color, results string + 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", endpoint.DisplayName(), alert.SuccessThreshold) color = "#36A64F" @@ -101,20 +101,23 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert * message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) color = "#DD0000" } - for _, conditionResult := range result.ConditionResults { - var prefix string - if conditionResult.Success { - prefix = ":white_check_mark:" - } else { - prefix = ":x:" + var formattedConditionResults string + if len(result.ConditionResults) > 0 { + for _, conditionResult := range result.ConditionResults { + var prefix string + if conditionResult.Success { + prefix = ":white_check_mark:" + } else { + prefix = ":x:" + } + formattedConditionResults += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition) } - results += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition) } var description string if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { description = ":\n> " + alertDescription } - body, _ := json.Marshal(Body{ + body := Body{ Text: "", Username: "gatus", IconURL: "https://raw.githubusercontent.com/TwiN/gatus/master/.github/assets/logo.png", @@ -125,17 +128,18 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert * Text: message + description, Short: false, Color: color, - Fields: []Field{ - { - Title: "Condition results", - Value: results, - Short: false, - }, - }, }, }, - }) - return body + } + if len(formattedConditionResults) > 0 { + body.Attachments[0].Fields = append(body.Attachments[0].Fields, Field{ + Title: "Condition results", + Value: formattedConditionResults, + Short: false, + }) + } + bodyAsJSON, _ := json.Marshal(body) + return bodyAsJSON } // getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group diff --git a/alerting/provider/ntfy/ntfy.go b/alerting/provider/ntfy/ntfy.go index 4036ea8a..bd10ef83 100644 --- a/alerting/provider/ntfy/ntfy.go +++ b/alerting/provider/ntfy/ntfy.go @@ -78,7 +78,7 @@ type Body struct { // buildRequestBody builds the request body for the provider func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { - var message, results, tag string + var message, formattedConditionResults, tag string if resolved { tag = "white_check_mark" message = "An alert has been resolved after passing successfully " + strconv.Itoa(alert.SuccessThreshold) + " time(s) in a row" @@ -93,12 +93,12 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert * } else { prefix = "🔴" } - results += fmt.Sprintf("\n%s %s", prefix, conditionResult.Condition) + formattedConditionResults += fmt.Sprintf("\n%s %s", prefix, conditionResult.Condition) } if len(alert.GetDescription()) > 0 { message += " with the following description: " + alert.GetDescription() } - message += results + message += formattedConditionResults body, _ := json.Marshal(Body{ Topic: provider.Topic, Title: "Gatus: " + endpoint.DisplayName(), diff --git a/alerting/provider/opsgenie/opsgenie.go b/alerting/provider/opsgenie/opsgenie.go index 8d7f2a81..5d105661 100644 --- a/alerting/provider/opsgenie/opsgenie.go +++ b/alerting/provider/opsgenie/opsgenie.go @@ -116,7 +116,7 @@ func (provider *AlertProvider) sendRequest(url, method string, payload interface } func (provider *AlertProvider) buildCreateRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) alertCreateRequest { - var message, description, results string + var message, description string if resolved { message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.Name, alert.GetDescription()) description = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) @@ -127,6 +127,7 @@ func (provider *AlertProvider) buildCreateRequestBody(endpoint *core.Endpoint, a if endpoint.Group != "" { message = fmt.Sprintf("[%s] %s", endpoint.Group, message) } + var formattedConditionResults string for _, conditionResult := range result.ConditionResults { var prefix string if conditionResult.Success { @@ -134,9 +135,9 @@ func (provider *AlertProvider) buildCreateRequestBody(endpoint *core.Endpoint, a } else { prefix = "▢" } - results += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition) + formattedConditionResults += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition) } - description = description + "\n" + results + description = description + "\n" + formattedConditionResults key := buildKey(endpoint) details := map[string]string{ "endpoint:url": endpoint.URL, diff --git a/alerting/provider/provider.go b/alerting/provider/provider.go index 5059e0f0..f8c5257b 100644 --- a/alerting/provider/provider.go +++ b/alerting/provider/provider.go @@ -24,7 +24,7 @@ import ( "github.com/TwiN/gatus/v5/core" ) -// AlertProvider is the interface that each providers should implement +// AlertProvider is the interface that each provider should implement type AlertProvider interface { // IsValid returns whether the provider's configuration is valid IsValid() bool diff --git a/alerting/provider/slack/slack.go b/alerting/provider/slack/slack.go index 017257cc..e498cee1 100644 --- a/alerting/provider/slack/slack.go +++ b/alerting/provider/slack/slack.go @@ -71,7 +71,7 @@ type Attachment struct { Text string `json:"text"` Short bool `json:"short"` Color string `json:"color"` - Fields []Field `json:"fields"` + Fields []Field `json:"fields,omitempty"` } type Field struct { @@ -82,7 +82,7 @@ type Field struct { // buildRequestBody builds the request body for the provider func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { - var message, color, results string + 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", endpoint.DisplayName(), alert.SuccessThreshold) color = "#36A64F" @@ -90,6 +90,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert * message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) color = "#DD0000" } + var formattedConditionResults string for _, conditionResult := range result.ConditionResults { var prefix string if conditionResult.Success { @@ -97,13 +98,13 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert * } else { prefix = ":x:" } - results += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition) + formattedConditionResults += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition) } var description string if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { description = ":\n> " + alertDescription } - body, _ := json.Marshal(Body{ + body := Body{ Text: "", Attachments: []Attachment{ { @@ -111,17 +112,18 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert * Text: message + description, Short: false, Color: color, - Fields: []Field{ - { - Title: "Condition results", - Value: results, - Short: false, - }, - }, }, }, - }) - return body + } + if len(formattedConditionResults) > 0 { + body.Attachments[0].Fields = append(body.Attachments[0].Fields, Field{ + Title: "Condition results", + Value: formattedConditionResults, + Short: false, + }) + } + bodyAsJSON, _ := json.Marshal(body) + return bodyAsJSON } // getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group diff --git a/alerting/provider/slack/slack_test.go b/alerting/provider/slack/slack_test.go index cc218eca..4bd10e15 100644 --- a/alerting/provider/slack/slack_test.go +++ b/alerting/provider/slack/slack_test.go @@ -144,6 +144,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { Provider AlertProvider Endpoint core.Endpoint Alert alert.Alert + NoConditions bool Resolved bool ExpectedBody string }{ @@ -163,6 +164,15 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { Resolved: false, ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *group/name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\",\"fields\":[{\"title\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\"short\":false}]}]}", }, + { + Name: "triggered-with-no-conditions", + NoConditions: true, + Provider: AlertProvider{}, + Endpoint: core.Endpoint{Name: "name"}, + Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3}, + Resolved: false, + ExpectedBody: "{\"text\":\"\",\"attachments\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"text\":\"An alert for *name* has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"short\":false,\"color\":\"#DD0000\"}]}", + }, { Name: "resolved", Provider: AlertProvider{}, @@ -182,14 +192,18 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { } for _, scenario := range scenarios { t.Run(scenario.Name, func(t *testing.T) { + var conditionResults []*core.ConditionResult + if !scenario.NoConditions { + conditionResults = []*core.ConditionResult{ + {Condition: "[CONNECTED] == true", Success: scenario.Resolved}, + {Condition: "[STATUS] == 200", Success: scenario.Resolved}, + } + } body := scenario.Provider.buildRequestBody( &scenario.Endpoint, &scenario.Alert, &core.Result{ - ConditionResults: []*core.ConditionResult{ - {Condition: "[CONNECTED] == true", Success: scenario.Resolved}, - {Condition: "[STATUS] == 200", Success: scenario.Resolved}, - }, + ConditionResults: conditionResults, }, scenario.Resolved, ) diff --git a/alerting/provider/teams/teams.go b/alerting/provider/teams/teams.go index 390e6aaf..569689bc 100644 --- a/alerting/provider/teams/teams.go +++ b/alerting/provider/teams/teams.go @@ -69,7 +69,7 @@ type Body struct { ThemeColor string `json:"themeColor"` Title string `json:"title"` Text string `json:"text"` - Sections []Section `json:"sections"` + Sections []Section `json:"sections,omitempty"` } type Section struct { @@ -87,7 +87,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert * message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) color = "#DD0000" } - var results string + var formattedConditionResults string for _, conditionResult := range result.ConditionResults { var prefix string if conditionResult.Success { @@ -95,26 +95,27 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert * } else { prefix = "❌" } - results += fmt.Sprintf("%s - `%s`
", prefix, conditionResult.Condition) + formattedConditionResults += fmt.Sprintf("%s - `%s`
", prefix, conditionResult.Condition) } var description string if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { description = ": " + alertDescription } - body, _ := json.Marshal(Body{ + body := Body{ Type: "MessageCard", Context: "http://schema.org/extensions", ThemeColor: color, Title: "🚨 Gatus", Text: message + description, - Sections: []Section{ - { - ActivityTitle: "Condition results", - Text: results, - }, - }, - }) - return body + } + if len(formattedConditionResults) > 0 { + body.Sections = append(body.Sections, Section{ + ActivityTitle: "Condition results", + Text: formattedConditionResults, + }) + } + bodyAsJSON, _ := json.Marshal(body) + return bodyAsJSON } // getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group diff --git a/alerting/provider/teams/teams_test.go b/alerting/provider/teams/teams_test.go index 00df33d3..7f7cdb21 100644 --- a/alerting/provider/teams/teams_test.go +++ b/alerting/provider/teams/teams_test.go @@ -143,6 +143,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { Name string Provider AlertProvider Alert alert.Alert + NoConditions bool Resolved bool ExpectedBody string }{ @@ -160,18 +161,28 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { Resolved: true, ExpectedBody: "{\"@type\":\"MessageCard\",\"@context\":\"http://schema.org/extensions\",\"themeColor\":\"#36A64F\",\"title\":\"\\u0026#x1F6A8; Gatus\",\"text\":\"An alert for *endpoint-name* has been resolved after passing successfully 5 time(s) in a row: description-2\",\"sections\":[{\"activityTitle\":\"Condition results\",\"text\":\"\\u0026#x2705; - `[CONNECTED] == true`\\u003cbr/\\u003e\\u0026#x2705; - `[STATUS] == 200`\\u003cbr/\\u003e\"}]}", }, + { + Name: "resolved-with-no-conditions", + NoConditions: true, + Provider: AlertProvider{}, + Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3}, + Resolved: true, + ExpectedBody: "{\"@type\":\"MessageCard\",\"@context\":\"http://schema.org/extensions\",\"themeColor\":\"#36A64F\",\"title\":\"\\u0026#x1F6A8; Gatus\",\"text\":\"An alert for *endpoint-name* has been resolved after passing successfully 5 time(s) in a row: description-2\"}", + }, } for _, scenario := range scenarios { t.Run(scenario.Name, func(t *testing.T) { + var conditionResults []*core.ConditionResult + if !scenario.NoConditions { + conditionResults = []*core.ConditionResult{ + {Condition: "[CONNECTED] == true", Success: scenario.Resolved}, + {Condition: "[STATUS] == 200", Success: scenario.Resolved}, + } + } body := scenario.Provider.buildRequestBody( &core.Endpoint{Name: "endpoint-name"}, &scenario.Alert, - &core.Result{ - ConditionResults: []*core.ConditionResult{ - {Condition: "[CONNECTED] == true", Success: scenario.Resolved}, - {Condition: "[STATUS] == 200", Success: scenario.Resolved}, - }, - }, + &core.Result{ConditionResults: conditionResults}, scenario.Resolved, ) if string(body) != scenario.ExpectedBody { diff --git a/alerting/provider/telegram/telegram.go b/alerting/provider/telegram/telegram.go index 7b2a357b..db9ea0a0 100644 --- a/alerting/provider/telegram/telegram.go +++ b/alerting/provider/telegram/telegram.go @@ -67,33 +67,37 @@ type Body struct { // buildRequestBody builds the request body for the provider func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { - var message, results string + var message string if resolved { message = fmt.Sprintf("An alert for *%s* has been resolved:\n—\n _healthcheck passing successfully %d time(s) in a row_\n— ", endpoint.DisplayName(), alert.SuccessThreshold) } else { message = fmt.Sprintf("An alert for *%s* has been triggered:\n—\n _healthcheck failed %d time(s) in a row_\n— ", endpoint.DisplayName(), alert.FailureThreshold) } - for _, conditionResult := range result.ConditionResults { - var prefix string - if conditionResult.Success { - prefix = "✅" - } else { - prefix = "❌" + var formattedConditionResults string + if len(result.ConditionResults) > 0 { + formattedConditionResults = "\n*Condition results*\n" + for _, conditionResult := range result.ConditionResults { + var prefix string + if conditionResult.Success { + prefix = "✅" + } else { + prefix = "❌" + } + formattedConditionResults += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition) } - results += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition) } var text string if len(alert.GetDescription()) > 0 { - text = fmt.Sprintf("⛑ *Gatus* \n%s \n*Description* \n_%s_ \n\n*Condition results*\n%s", message, alert.GetDescription(), results) + text = fmt.Sprintf("⛑ *Gatus* \n%s \n*Description* \n_%s_ \n%s", message, alert.GetDescription(), formattedConditionResults) } else { - text = fmt.Sprintf("⛑ *Gatus* \n%s \n*Condition results*\n%s", message, results) + text = fmt.Sprintf("⛑ *Gatus* \n%s%s", message, formattedConditionResults) } - body, _ := json.Marshal(Body{ + bodyAsJSON, _ := json.Marshal(Body{ ChatID: provider.ID, Text: text, ParseMode: "MARKDOWN", }) - return body + return bodyAsJSON } // GetDefaultAlert returns the provider's default alert configuration diff --git a/alerting/provider/telegram/telegram_test.go b/alerting/provider/telegram/telegram_test.go index d3e0f900..3dcebb07 100644 --- a/alerting/provider/telegram/telegram_test.go +++ b/alerting/provider/telegram/telegram_test.go @@ -116,6 +116,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { Name string Provider AlertProvider Alert alert.Alert + NoConditions bool Resolved bool ExpectedBody string }{ @@ -133,18 +134,28 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { Resolved: true, ExpectedBody: "{\"chat_id\":\"123\",\"text\":\"⛑ *Gatus* \\nAn alert for *endpoint-name* has been resolved:\\n—\\n _healthcheck passing successfully 5 time(s) in a row_\\n— \\n*Description* \\n_description-2_ \\n\\n*Condition results*\\n✅ - `[CONNECTED] == true`\\n✅ - `[STATUS] == 200`\\n\",\"parse_mode\":\"MARKDOWN\"}", }, + { + Name: "resolved-with-no-conditions", + NoConditions: true, + Provider: AlertProvider{ID: "123"}, + Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3}, + Resolved: true, + ExpectedBody: "{\"chat_id\":\"123\",\"text\":\"⛑ *Gatus* \\nAn alert for *endpoint-name* has been resolved:\\n—\\n _healthcheck passing successfully 5 time(s) in a row_\\n— \\n*Description* \\n_description-2_ \\n\",\"parse_mode\":\"MARKDOWN\"}", + }, } for _, scenario := range scenarios { t.Run(scenario.Name, func(t *testing.T) { + var conditionResults []*core.ConditionResult + if !scenario.NoConditions { + conditionResults = []*core.ConditionResult{ + {Condition: "[CONNECTED] == true", Success: scenario.Resolved}, + {Condition: "[STATUS] == 200", Success: scenario.Resolved}, + } + } body := scenario.Provider.buildRequestBody( &core.Endpoint{Name: "endpoint-name"}, &scenario.Alert, - &core.Result{ - ConditionResults: []*core.ConditionResult{ - {Condition: "[CONNECTED] == true", Success: scenario.Resolved}, - {Condition: "[STATUS] == 200", Success: scenario.Resolved}, - }, - }, + &core.Result{ConditionResults: conditionResults}, scenario.Resolved, ) if string(body) != scenario.ExpectedBody { diff --git a/watchdog/alerting_test.go b/watchdog/alerting_test.go index eb334601..358412d8 100644 --- a/watchdog/alerting_test.go +++ b/watchdog/alerting_test.go @@ -377,7 +377,7 @@ func TestHandleAlertingWithProviderThatReturnsAnError(t *testing.T) { AlertType: alert.TypeMatrix, AlertingConfig: &alerting.Config{ Matrix: &matrix.AlertProvider{ - MatrixProviderConfig: matrix.MatrixProviderConfig{ + ProviderConfig: matrix.ProviderConfig{ ServerURL: "https://example.com", AccessToken: "1", InternalRoomID: "!a:example.com",