fix(alerting): Support alerts with no conditions for external endpoints (#729)

This commit is contained in:
TwiN 2024-04-10 20:46:17 -04:00 committed by GitHub
parent a4bc3c4dfe
commit 241956b28c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 303 additions and 209 deletions

View File

@ -50,7 +50,6 @@ func (provider *AlertProvider) IsValid() bool {
registeredGroups[override.Group] = true registeredGroups[override.Group] = true
} }
} }
// if both AccessKeyID and SecretAccessKey are specified, we'll use these to authenticate, // 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. // otherwise if neither are specified, then we'll fall back on IAM authentication.
return len(provider.From) > 0 && len(provider.To) > 0 && 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 // buildMessageSubjectAndBody builds the message subject and body
func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) (string, string) { 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 { if resolved {
subject = fmt.Sprintf("[%s] Alert resolved", endpoint.DisplayName()) 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) 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()) 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) 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 formattedConditionResults string
var prefix string if len(result.ConditionResults) > 0 {
if conditionResult.Success { formattedConditionResults = "\n\nCondition results:\n"
prefix = "✅" for _, conditionResult := range result.ConditionResults {
} else { var prefix string
prefix = "❌" 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 var description string
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
description = "\n\nAlert description: " + alertDescription 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 // getToForGroup returns the appropriate email integration to for a given group

View File

@ -75,7 +75,7 @@ type Embed struct {
Title string `json:"title"` Title string `json:"title"`
Description string `json:"description"` Description string `json:"description"`
Color int `json:"color"` Color int `json:"color"`
Fields []Field `json:"fields"` Fields []Field `json:"fields,omitempty"`
} }
type Field struct { type Field struct {
@ -86,7 +86,7 @@ type Field struct {
// buildRequestBody builds the request body for the provider // buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { 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 var colorCode int
if resolved { 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) 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) 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 colorCode = 15158332
} }
var formattedConditionResults string
for _, conditionResult := range result.ConditionResults { for _, conditionResult := range result.ConditionResults {
var prefix string var prefix string
if conditionResult.Success { if conditionResult.Success {
@ -102,7 +103,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
} else { } else {
prefix = ":x:" prefix = ":x:"
} }
results += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition) formattedConditionResults += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition)
} }
var description string var description string
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
@ -112,24 +113,25 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
if provider.Title != "" { if provider.Title != "" {
title = provider.Title title = provider.Title
} }
body, _ := json.Marshal(Body{ body := Body{
Content: "", Content: "",
Embeds: []Embed{ Embeds: []Embed{
{ {
Title: title, Title: title,
Description: message + description, Description: message + description,
Color: colorCode, 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 // getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group

View File

@ -155,6 +155,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Name string Name string
Provider AlertProvider Provider AlertProvider
Alert alert.Alert Alert alert.Alert
NoConditions bool
Resolved bool Resolved bool
ExpectedBody string ExpectedBody string
}{ }{
@ -179,18 +180,30 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Resolved: false, 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}]}]}", 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 { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { 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( body := scenario.Provider.buildRequestBody(
&core.Endpoint{Name: "endpoint-name"}, &core.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &core.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: conditionResults,
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
{Condition: "[BODY] != \"\"", Success: scenario.Resolved},
},
}, },
scenario.Resolved, scenario.Resolved,
) )

View File

@ -88,7 +88,7 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
// buildMessageSubjectAndBody builds the message subject and body // buildMessageSubjectAndBody builds the message subject and body
func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) (string, string) { 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 { if resolved {
subject = fmt.Sprintf("[%s] Alert resolved", endpoint.DisplayName()) 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) 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()) 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) 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 formattedConditionResults string
var prefix string if len(result.ConditionResults) > 0 {
if conditionResult.Success { formattedConditionResults = "\n\nCondition results:\n"
prefix = "✅" for _, conditionResult := range result.ConditionResults {
} else { var prefix string
prefix = "❌" 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 var description string
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
description = "\n\nAlert description: " + alertDescription 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 // getToForGroup returns the appropriate email integration to for a given group

View File

@ -105,22 +105,25 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
// buildIssueBody builds the body of the issue // buildIssueBody builds the body of the issue
func (provider *AlertProvider) buildIssueBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result) string { func (provider *AlertProvider) buildIssueBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result) string {
var results string var formattedConditionResults string
for _, conditionResult := range result.ConditionResults { if len(result.ConditionResults) > 0 {
var prefix string formattedConditionResults = "\n\n## Condition results\n"
if conditionResult.Success { for _, conditionResult := range result.ConditionResults {
prefix = ":white_check_mark:" var prefix string
} else { if conditionResult.Success {
prefix = ":x:" 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 var description string
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
description = ":\n> " + alertDescription 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) 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 // GetDefaultAlert returns the provider's default alert configuration

View File

@ -112,6 +112,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Endpoint core.Endpoint Endpoint core.Endpoint
Provider AlertProvider Provider AlertProvider
Alert alert.Alert Alert alert.Alert
NoConditions bool
ExpectedBody string 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`", 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"}, Endpoint: core.Endpoint{Name: "endpoint-name", URL: "https://example.org"},
Provider: AlertProvider{}, Provider: AlertProvider{},
Alert: alert.Alert{FailureThreshold: 10}, 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`", 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 { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { 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( body := scenario.Provider.buildIssueBody(
&scenario.Endpoint, &scenario.Endpoint,
&scenario.Alert, &scenario.Alert,
&core.Result{ &core.Result{ConditionResults: conditionResults},
ConditionResults: []*core.ConditionResult{
{Condition: "[CONNECTED] == true", Success: true},
{Condition: "[STATUS] == 200", Success: false},
},
},
) )
if strings.TrimSpace(body) != strings.TrimSpace(scenario.ExpectedBody) { if strings.TrimSpace(body) != strings.TrimSpace(scenario.ExpectedBody) {
t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body) t.Errorf("expected:\n%s\ngot:\n%s", scenario.ExpectedBody, body)

View File

@ -25,10 +25,13 @@ type AlertProvider struct {
// Severity can be one of: critical, high, medium, low, info, unknown. Defaults to critical // Severity can be one of: critical, high, medium, low, info, unknown. Defaults to critical
Severity string `yaml:"severity,omitempty"` Severity string `yaml:"severity,omitempty"`
// MonitoringTool overrides the name sent to gitlab. Defaults to gatus // MonitoringTool overrides the name sent to gitlab. Defaults to gatus
MonitoringTool string `yaml:"monitoring-tool,omitempty"` MonitoringTool string `yaml:"monitoring-tool,omitempty"`
// EnvironmentName is the name of the associated GitLab environment. Required to display alerts on a dashboard. // EnvironmentName is the name of the associated GitLab environment. Required to display alerts on a dashboard.
EnvironmentName string `yaml:"environment-name,omitempty"` EnvironmentName string `yaml:"environment-name,omitempty"`
// Service affected. Defaults to endpoint display name // Service affected. Defaults to endpoint display name
Service string `yaml:"service,omitempty"` Service string `yaml:"service,omitempty"`
} }
@ -52,7 +55,6 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
if len(alert.ResolveKey) == 0 { if len(alert.ResolveKey) == 0 {
alert.ResolveKey = uuid.NewString() alert.ResolveKey = uuid.NewString()
} }
buffer := bytes.NewBuffer(provider.buildAlertBody(endpoint, alert, result, resolved)) buffer := bytes.NewBuffer(provider.buildAlertBody(endpoint, alert, result, resolved))
request, err := http.NewRequest(http.MethodPost, provider.WebhookURL, buffer) request, err := http.NewRequest(http.MethodPost, provider.WebhookURL, buffer)
if err != nil { if err != nil {
@ -114,16 +116,18 @@ func (provider *AlertProvider) buildAlertBody(endpoint *core.Endpoint, alert *al
if resolved { if resolved {
body.EndTime = result.Timestamp.Format(time.RFC3339) body.EndTime = result.Timestamp.Format(time.RFC3339)
} }
var formattedConditionResults string
var results string if len(result.ConditionResults) > 0 {
for _, conditionResult := range result.ConditionResults { formattedConditionResults = "\n\n## Condition results\n"
var prefix string for _, conditionResult := range result.ConditionResults {
if conditionResult.Success { var prefix string
prefix = ":white_check_mark:" if conditionResult.Success {
} else { prefix = ":white_check_mark:"
prefix = ":x:" } else {
prefix = ":x:"
}
formattedConditionResults += fmt.Sprintf("- %s - `%s`\n", prefix, conditionResult.Condition)
} }
results += fmt.Sprintf("- %s - `%s`\n", prefix, conditionResult.Condition)
} }
var description string var description string
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
@ -135,10 +139,9 @@ func (provider *AlertProvider) buildAlertBody(endpoint *core.Endpoint, alert *al
} else { } 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) 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 body.Description = message + description + formattedConditionResults
bodyAsJSON, _ := json.Marshal(body)
json, _ := json.Marshal(body) return bodyAsJSON
return json
} }
// GetDefaultAlert returns the provider's default alert configuration // GetDefaultAlert returns the provider's default alert configuration

View File

@ -121,7 +121,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
color = "#DD0000" color = "#DD0000"
message = fmt.Sprintf("<font color='%s'>An alert has been triggered due to having failed %d time(s) in a row</font>", color, alert.FailureThreshold) message = fmt.Sprintf("<font color='%s'>An alert has been triggered due to having failed %d time(s) in a row</font>", color, alert.FailureThreshold)
} }
var results string var formattedConditionResults string
for _, conditionResult := range result.ConditionResults { for _, conditionResult := range result.ConditionResults {
var prefix string var prefix string
if conditionResult.Success { if conditionResult.Success {
@ -129,7 +129,7 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
} else { } else {
prefix = "❌" prefix = "❌"
} }
results += fmt.Sprintf("%s %s<br>", prefix, conditionResult.Condition) formattedConditionResults += fmt.Sprintf("%s %s<br>", prefix, conditionResult.Condition)
} }
var description string var description string
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
@ -150,20 +150,22 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
Icon: "BOOKMARK", 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 { if endpoint.Type() == core.EndpointTypeHTTP {
// We only include a button targeting the URL if the endpoint is an HTTP endpoint // 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. // 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) bodyAsJSON, _ := json.Marshal(payload)
return body return bodyAsJSON
} }
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group // getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group

View File

@ -68,12 +68,13 @@ type Body struct {
// buildRequestBody builds the request body for the provider // buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { 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 { 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) message = fmt.Sprintf("An alert for `%s` has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
} else { } 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) 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 { for _, conditionResult := range result.ConditionResults {
var prefix string var prefix string
if conditionResult.Success { if conditionResult.Success {
@ -81,22 +82,22 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
} else { } else {
prefix = "✕" prefix = "✕"
} }
results += fmt.Sprintf("\n%s - %s", prefix, conditionResult.Condition) formattedConditionResults += fmt.Sprintf("\n%s - %s", prefix, conditionResult.Condition)
} }
if len(alert.GetDescription()) > 0 { if len(alert.GetDescription()) > 0 {
message += " with the following description: " + alert.GetDescription() message += " with the following description: " + alert.GetDescription()
} }
message += results message += formattedConditionResults
title := "Gatus: " + endpoint.DisplayName() title := "Gatus: " + endpoint.DisplayName()
if provider.Title != "" { if provider.Title != "" {
title = provider.Title title = provider.Title
} }
body, _ := json.Marshal(Body{ bodyAsJSON, _ := json.Marshal(Body{
Message: message, Message: message,
Title: title, Title: title,
Priority: provider.Priority, Priority: provider.Priority,
}) })
return body return bodyAsJSON
} }
// GetDefaultAlert returns the provider's default alert configuration // GetDefaultAlert returns the provider's default alert configuration

View File

@ -17,8 +17,10 @@ type AlertProvider struct {
Project string `yaml:"project"` // JetBrains Space Project name Project string `yaml:"project"` // JetBrains Space Project name
ChannelID string `yaml:"channel-id"` // JetBrains Space Chat Channel ID ChannelID string `yaml:"channel-id"` // JetBrains Space Chat Channel ID
Token string `yaml:"token"` // JetBrains Space Bearer Token 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"` DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
// Overrides is a list of Override that may be prioritized over the default configuration // Overrides is a list of Override that may be prioritized over the default configuration
Overrides []Override `yaml:"overrides,omitempty"` Overrides []Override `yaml:"overrides,omitempty"`
} }
@ -73,7 +75,7 @@ type Body struct {
type Content struct { type Content struct {
ClassName string `json:"className"` ClassName string `json:"className"`
Style string `json:"style"` Style string `json:"style"`
Sections []Section `json:"sections"` Sections []Section `json:"sections,omitempty"`
} }
type Section struct { type Section struct {
@ -112,7 +114,6 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
}}, }},
}, },
} }
if resolved { if resolved {
body.Content.Style = "SUCCESS" 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) 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.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) 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 { for _, conditionResult := range result.ConditionResults {
icon := "warning" icon := "warning"
style := "WARNING" style := "WARNING"
@ -128,7 +128,6 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
icon = "success" icon = "success"
style = "SUCCESS" style = "SUCCESS"
} }
body.Content.Sections[0].Elements = append(body.Content.Sections[0].Elements, Element{ body.Content.Sections[0].Elements = append(body.Content.Sections[0].Elements, Element{
ClassName: "MessageText", ClassName: "MessageText",
Accessory: Accessory{ Accessory: Accessory{
@ -141,9 +140,8 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
Content: conditionResult.Condition, Content: conditionResult.Condition,
}) })
} }
bodyAsJSON, _ := json.Marshal(body)
jsonBody, _ := json.Marshal(body) return bodyAsJSON
return jsonBody
} }
// getChannelIDForGroup returns the appropriate channel ID to for a given group override // getChannelIDForGroup returns the appropriate channel ID to for a given group override

View File

@ -17,7 +17,7 @@ import (
// AlertProvider is the configuration necessary for sending an alert using Matrix // AlertProvider is the configuration necessary for sending an alert using Matrix
type AlertProvider struct { 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 is the default alert configuration to use for endpoints with an alert of the appropriate type
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"` DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
@ -30,12 +30,12 @@ type AlertProvider struct {
type Override struct { type Override struct {
Group string `yaml:"group"` 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 is the custom homeserver to use (optional)
ServerURL string `yaml:"server-url"` 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)) buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved))
config := provider.getConfigForGroup(endpoint.Group) config := provider.getConfigForGroup(endpoint.Group)
if config.ServerURL == "" { if config.ServerURL == "" {
config.ServerURL = defaultHomeserverURL config.ServerURL = defaultServerURL
} }
// The Matrix endpoint requires a unique transaction ID for each event sent // The Matrix endpoint requires a unique transaction ID for each event sent
txnId := randStringBytes(24) 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 // 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 { func buildPlaintextMessageBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
var message, results string var message string
if resolved { 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) message = fmt.Sprintf("An alert for `%s` has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
} else { } 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) 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 { for _, conditionResult := range result.ConditionResults {
var prefix string var prefix string
if conditionResult.Success { if conditionResult.Success {
@ -128,49 +129,54 @@ func buildPlaintextMessageBody(endpoint *core.Endpoint, alert *alert.Alert, resu
} else { } else {
prefix = "✕" prefix = "✕"
} }
results += fmt.Sprintf("\n%s - %s", prefix, conditionResult.Condition) formattedConditionResults += fmt.Sprintf("\n%s - %s", prefix, conditionResult.Condition)
} }
var description string var description string
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
description = "\n" + alertDescription 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 // 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 { func buildHTMLMessageBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
var message, results string var message string
if resolved { if resolved {
message = fmt.Sprintf("An alert for <code>%s</code> has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold) message = fmt.Sprintf("An alert for <code>%s</code> has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
} else { } else {
message = fmt.Sprintf("An alert for <code>%s</code> has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold) message = fmt.Sprintf("An alert for <code>%s</code> has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
} }
for _, conditionResult := range result.ConditionResults { var formattedConditionResults string
var prefix string if len(result.ConditionResults) > 0 {
if conditionResult.Success { formattedConditionResults = "\n<h5>Condition results</h5><ul>"
prefix = "✅" for _, conditionResult := range result.ConditionResults {
} else { var prefix string
prefix = "❌" if conditionResult.Success {
prefix = "✅"
} else {
prefix = "❌"
}
formattedConditionResults += fmt.Sprintf("<li>%s - <code>%s</code></li>", prefix, conditionResult.Condition)
} }
results += fmt.Sprintf("<li>%s - <code>%s</code></li>", prefix, conditionResult.Condition) formattedConditionResults += "</ul>"
} }
var description string var description string
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
description = fmt.Sprintf("\n<blockquote>%s</blockquote>", alertDescription) description = fmt.Sprintf("\n<blockquote>%s</blockquote>", alertDescription)
} }
return fmt.Sprintf("<h3>%s</h3>%s\n<h5>Condition results</h5><ul>%s</ul>", message, description, results) return fmt.Sprintf("<h3>%s</h3>%s%s", message, description, formattedConditionResults)
} }
// getConfigForGroup returns the appropriate configuration for a given group // 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 { if provider.Overrides != nil {
for _, override := range provider.Overrides { for _, override := range provider.Overrides {
if group == override.Group { if group == override.Group {
return override.MatrixProviderConfig return override.ProviderConfig
} }
} }
} }
return provider.MatrixProviderConfig return provider.ProviderConfig
} }
func randStringBytes(n int) string { func randStringBytes(n int) string {

View File

@ -13,7 +13,7 @@ import (
func TestAlertProvider_IsValid(t *testing.T) { func TestAlertProvider_IsValid(t *testing.T) {
invalidProvider := AlertProvider{ invalidProvider := AlertProvider{
MatrixProviderConfig: MatrixProviderConfig{ ProviderConfig: ProviderConfig{
AccessToken: "", AccessToken: "",
InternalRoomID: "", InternalRoomID: "",
}, },
@ -22,7 +22,7 @@ func TestAlertProvider_IsValid(t *testing.T) {
t.Error("provider shouldn't have been valid") t.Error("provider shouldn't have been valid")
} }
validProvider := AlertProvider{ validProvider := AlertProvider{
MatrixProviderConfig: MatrixProviderConfig{ ProviderConfig: ProviderConfig{
AccessToken: "1", AccessToken: "1",
InternalRoomID: "!a:example.com", InternalRoomID: "!a:example.com",
}, },
@ -31,7 +31,7 @@ func TestAlertProvider_IsValid(t *testing.T) {
t.Error("provider should've been valid") t.Error("provider should've been valid")
} }
validProviderWithHomeserver := AlertProvider{ validProviderWithHomeserver := AlertProvider{
MatrixProviderConfig: MatrixProviderConfig{ ProviderConfig: ProviderConfig{
ServerURL: "https://example.com", ServerURL: "https://example.com",
AccessToken: "1", AccessToken: "1",
InternalRoomID: "!a:example.com", InternalRoomID: "!a:example.com",
@ -47,7 +47,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
Overrides: []Override{ Overrides: []Override{
{ {
Group: "", Group: "",
MatrixProviderConfig: MatrixProviderConfig{ ProviderConfig: ProviderConfig{
AccessToken: "", AccessToken: "",
InternalRoomID: "", InternalRoomID: "",
}, },
@ -61,7 +61,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
Overrides: []Override{ Overrides: []Override{
{ {
Group: "group", Group: "group",
MatrixProviderConfig: MatrixProviderConfig{ ProviderConfig: ProviderConfig{
AccessToken: "", AccessToken: "",
InternalRoomID: "", InternalRoomID: "",
}, },
@ -72,14 +72,14 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
t.Error("provider integration key shouldn't have been valid") t.Error("provider integration key shouldn't have been valid")
} }
providerWithValidOverride := AlertProvider{ providerWithValidOverride := AlertProvider{
MatrixProviderConfig: MatrixProviderConfig{ ProviderConfig: ProviderConfig{
AccessToken: "1", AccessToken: "1",
InternalRoomID: "!a:example.com", InternalRoomID: "!a:example.com",
}, },
Overrides: []Override{ Overrides: []Override{
{ {
Group: "group", Group: "group",
MatrixProviderConfig: MatrixProviderConfig{ ProviderConfig: ProviderConfig{
ServerURL: "https://example.com", ServerURL: "https://example.com",
AccessToken: "1", AccessToken: "1",
InternalRoomID: "!a:example.com", InternalRoomID: "!a:example.com",
@ -232,12 +232,12 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) {
Name string Name string
Provider AlertProvider Provider AlertProvider
InputGroup string InputGroup string
ExpectedOutput MatrixProviderConfig ExpectedOutput ProviderConfig
}{ }{
{ {
Name: "provider-no-override-specify-no-group-should-default", Name: "provider-no-override-specify-no-group-should-default",
Provider: AlertProvider{ Provider: AlertProvider{
MatrixProviderConfig: MatrixProviderConfig{ ProviderConfig: ProviderConfig{
ServerURL: "https://example.com", ServerURL: "https://example.com",
AccessToken: "1", AccessToken: "1",
InternalRoomID: "!a:example.com", InternalRoomID: "!a:example.com",
@ -245,7 +245,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) {
Overrides: nil, Overrides: nil,
}, },
InputGroup: "", InputGroup: "",
ExpectedOutput: MatrixProviderConfig{ ExpectedOutput: ProviderConfig{
ServerURL: "https://example.com", ServerURL: "https://example.com",
AccessToken: "1", AccessToken: "1",
InternalRoomID: "!a:example.com", InternalRoomID: "!a:example.com",
@ -254,7 +254,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) {
{ {
Name: "provider-no-override-specify-group-should-default", Name: "provider-no-override-specify-group-should-default",
Provider: AlertProvider{ Provider: AlertProvider{
MatrixProviderConfig: MatrixProviderConfig{ ProviderConfig: ProviderConfig{
ServerURL: "https://example.com", ServerURL: "https://example.com",
AccessToken: "1", AccessToken: "1",
InternalRoomID: "!a:example.com", InternalRoomID: "!a:example.com",
@ -262,7 +262,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) {
Overrides: nil, Overrides: nil,
}, },
InputGroup: "group", InputGroup: "group",
ExpectedOutput: MatrixProviderConfig{ ExpectedOutput: ProviderConfig{
ServerURL: "https://example.com", ServerURL: "https://example.com",
AccessToken: "1", AccessToken: "1",
InternalRoomID: "!a:example.com", InternalRoomID: "!a:example.com",
@ -271,7 +271,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) {
{ {
Name: "provider-with-override-specify-no-group-should-default", Name: "provider-with-override-specify-no-group-should-default",
Provider: AlertProvider{ Provider: AlertProvider{
MatrixProviderConfig: MatrixProviderConfig{ ProviderConfig: ProviderConfig{
ServerURL: "https://example.com", ServerURL: "https://example.com",
AccessToken: "1", AccessToken: "1",
InternalRoomID: "!a:example.com", InternalRoomID: "!a:example.com",
@ -279,7 +279,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) {
Overrides: []Override{ Overrides: []Override{
{ {
Group: "group", Group: "group",
MatrixProviderConfig: MatrixProviderConfig{ ProviderConfig: ProviderConfig{
ServerURL: "https://example01.com", ServerURL: "https://example01.com",
AccessToken: "12", AccessToken: "12",
InternalRoomID: "!a:example01.com", InternalRoomID: "!a:example01.com",
@ -288,7 +288,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) {
}, },
}, },
InputGroup: "", InputGroup: "",
ExpectedOutput: MatrixProviderConfig{ ExpectedOutput: ProviderConfig{
ServerURL: "https://example.com", ServerURL: "https://example.com",
AccessToken: "1", AccessToken: "1",
InternalRoomID: "!a:example.com", InternalRoomID: "!a:example.com",
@ -297,7 +297,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) {
{ {
Name: "provider-with-override-specify-group-should-override", Name: "provider-with-override-specify-group-should-override",
Provider: AlertProvider{ Provider: AlertProvider{
MatrixProviderConfig: MatrixProviderConfig{ ProviderConfig: ProviderConfig{
ServerURL: "https://example.com", ServerURL: "https://example.com",
AccessToken: "1", AccessToken: "1",
InternalRoomID: "!a:example.com", InternalRoomID: "!a:example.com",
@ -305,7 +305,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) {
Overrides: []Override{ Overrides: []Override{
{ {
Group: "group", Group: "group",
MatrixProviderConfig: MatrixProviderConfig{ ProviderConfig: ProviderConfig{
ServerURL: "https://example01.com", ServerURL: "https://example01.com",
AccessToken: "12", AccessToken: "12",
InternalRoomID: "!a:example01.com", InternalRoomID: "!a:example01.com",
@ -314,7 +314,7 @@ func TestAlertProvider_getConfigForGroup(t *testing.T) {
}, },
}, },
InputGroup: "group", InputGroup: "group",
ExpectedOutput: MatrixProviderConfig{ ExpectedOutput: ProviderConfig{
ServerURL: "https://example01.com", ServerURL: "https://example01.com",
AccessToken: "12", AccessToken: "12",
InternalRoomID: "!a:example01.com", InternalRoomID: "!a:example01.com",

View File

@ -93,7 +93,7 @@ type Field struct {
// buildRequestBody builds the request body for the provider // buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { 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 { 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) 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" 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) 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" color = "#DD0000"
} }
for _, conditionResult := range result.ConditionResults { var formattedConditionResults string
var prefix string if len(result.ConditionResults) > 0 {
if conditionResult.Success { for _, conditionResult := range result.ConditionResults {
prefix = ":white_check_mark:" var prefix string
} else { if conditionResult.Success {
prefix = ":x:" 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 var description string
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
description = ":\n> " + alertDescription description = ":\n> " + alertDescription
} }
body, _ := json.Marshal(Body{ body := Body{
Text: "", Text: "",
Username: "gatus", Username: "gatus",
IconURL: "https://raw.githubusercontent.com/TwiN/gatus/master/.github/assets/logo.png", 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, Text: message + description,
Short: false, Short: false,
Color: color, 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 // getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group

View File

@ -78,7 +78,7 @@ type Body struct {
// buildRequestBody builds the request body for the provider // buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { 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 { if resolved {
tag = "white_check_mark" tag = "white_check_mark"
message = "An alert has been resolved after passing successfully " + strconv.Itoa(alert.SuccessThreshold) + " time(s) in a row" 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 { } else {
prefix = "🔴" prefix = "🔴"
} }
results += fmt.Sprintf("\n%s %s", prefix, conditionResult.Condition) formattedConditionResults += fmt.Sprintf("\n%s %s", prefix, conditionResult.Condition)
} }
if len(alert.GetDescription()) > 0 { if len(alert.GetDescription()) > 0 {
message += " with the following description: " + alert.GetDescription() message += " with the following description: " + alert.GetDescription()
} }
message += results message += formattedConditionResults
body, _ := json.Marshal(Body{ body, _ := json.Marshal(Body{
Topic: provider.Topic, Topic: provider.Topic,
Title: "Gatus: " + endpoint.DisplayName(), Title: "Gatus: " + endpoint.DisplayName(),

View File

@ -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 { 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 { if resolved {
message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.Name, alert.GetDescription()) 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) 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 != "" { if endpoint.Group != "" {
message = fmt.Sprintf("[%s] %s", endpoint.Group, message) message = fmt.Sprintf("[%s] %s", endpoint.Group, message)
} }
var formattedConditionResults string
for _, conditionResult := range result.ConditionResults { for _, conditionResult := range result.ConditionResults {
var prefix string var prefix string
if conditionResult.Success { if conditionResult.Success {
@ -134,9 +135,9 @@ func (provider *AlertProvider) buildCreateRequestBody(endpoint *core.Endpoint, a
} else { } else {
prefix = "▢" 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) key := buildKey(endpoint)
details := map[string]string{ details := map[string]string{
"endpoint:url": endpoint.URL, "endpoint:url": endpoint.URL,

View File

@ -24,7 +24,7 @@ import (
"github.com/TwiN/gatus/v5/core" "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 { type AlertProvider interface {
// IsValid returns whether the provider's configuration is valid // IsValid returns whether the provider's configuration is valid
IsValid() bool IsValid() bool

View File

@ -71,7 +71,7 @@ type Attachment struct {
Text string `json:"text"` Text string `json:"text"`
Short bool `json:"short"` Short bool `json:"short"`
Color string `json:"color"` Color string `json:"color"`
Fields []Field `json:"fields"` Fields []Field `json:"fields,omitempty"`
} }
type Field struct { type Field struct {
@ -82,7 +82,7 @@ type Field struct {
// buildRequestBody builds the request body for the provider // buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { 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 { 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) 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" 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) 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" color = "#DD0000"
} }
var formattedConditionResults string
for _, conditionResult := range result.ConditionResults { for _, conditionResult := range result.ConditionResults {
var prefix string var prefix string
if conditionResult.Success { if conditionResult.Success {
@ -97,13 +98,13 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
} else { } else {
prefix = ":x:" prefix = ":x:"
} }
results += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition) formattedConditionResults += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition)
} }
var description string var description string
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
description = ":\n> " + alertDescription description = ":\n> " + alertDescription
} }
body, _ := json.Marshal(Body{ body := Body{
Text: "", Text: "",
Attachments: []Attachment{ Attachments: []Attachment{
{ {
@ -111,17 +112,18 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
Text: message + description, Text: message + description,
Short: false, Short: false,
Color: color, 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 // getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group

View File

@ -144,6 +144,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Provider AlertProvider Provider AlertProvider
Endpoint core.Endpoint Endpoint core.Endpoint
Alert alert.Alert Alert alert.Alert
NoConditions bool
Resolved bool Resolved bool
ExpectedBody string ExpectedBody string
}{ }{
@ -163,6 +164,15 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Resolved: false, 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}]}]}", 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", Name: "resolved",
Provider: AlertProvider{}, Provider: AlertProvider{},
@ -182,14 +192,18 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
} }
for _, scenario := range scenarios { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { 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( body := scenario.Provider.buildRequestBody(
&scenario.Endpoint, &scenario.Endpoint,
&scenario.Alert, &scenario.Alert,
&core.Result{ &core.Result{
ConditionResults: []*core.ConditionResult{ ConditionResults: conditionResults,
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
},
}, },
scenario.Resolved, scenario.Resolved,
) )

View File

@ -69,7 +69,7 @@ type Body struct {
ThemeColor string `json:"themeColor"` ThemeColor string `json:"themeColor"`
Title string `json:"title"` Title string `json:"title"`
Text string `json:"text"` Text string `json:"text"`
Sections []Section `json:"sections"` Sections []Section `json:"sections,omitempty"`
} }
type Section struct { 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) 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" color = "#DD0000"
} }
var results string var formattedConditionResults string
for _, conditionResult := range result.ConditionResults { for _, conditionResult := range result.ConditionResults {
var prefix string var prefix string
if conditionResult.Success { if conditionResult.Success {
@ -95,26 +95,27 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
} else { } else {
prefix = "&#x274C;" prefix = "&#x274C;"
} }
results += fmt.Sprintf("%s - `%s`<br/>", prefix, conditionResult.Condition) formattedConditionResults += fmt.Sprintf("%s - `%s`<br/>", prefix, conditionResult.Condition)
} }
var description string var description string
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
description = ": " + alertDescription description = ": " + alertDescription
} }
body, _ := json.Marshal(Body{ body := Body{
Type: "MessageCard", Type: "MessageCard",
Context: "http://schema.org/extensions", Context: "http://schema.org/extensions",
ThemeColor: color, ThemeColor: color,
Title: "&#x1F6A8; Gatus", Title: "&#x1F6A8; Gatus",
Text: message + description, Text: message + description,
Sections: []Section{ }
{ if len(formattedConditionResults) > 0 {
ActivityTitle: "Condition results", body.Sections = append(body.Sections, Section{
Text: results, ActivityTitle: "Condition results",
}, Text: formattedConditionResults,
}, })
}) }
return body bodyAsJSON, _ := json.Marshal(body)
return bodyAsJSON
} }
// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group // getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group

View File

@ -143,6 +143,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Name string Name string
Provider AlertProvider Provider AlertProvider
Alert alert.Alert Alert alert.Alert
NoConditions bool
Resolved bool Resolved bool
ExpectedBody string ExpectedBody string
}{ }{
@ -160,18 +161,28 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Resolved: true, 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\"}]}", 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 { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { 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( body := scenario.Provider.buildRequestBody(
&core.Endpoint{Name: "endpoint-name"}, &core.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &core.Result{ConditionResults: conditionResults},
ConditionResults: []*core.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
},
},
scenario.Resolved, scenario.Resolved,
) )
if string(body) != scenario.ExpectedBody { if string(body) != scenario.ExpectedBody {

View File

@ -67,33 +67,37 @@ type Body struct {
// buildRequestBody builds the request body for the provider // buildRequestBody builds the request body for the provider
func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { 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 { 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) 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 { } 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) 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 formattedConditionResults string
var prefix string if len(result.ConditionResults) > 0 {
if conditionResult.Success { formattedConditionResults = "\n*Condition results*\n"
prefix = "✅" for _, conditionResult := range result.ConditionResults {
} else { var prefix string
prefix = "❌" 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 var text string
if len(alert.GetDescription()) > 0 { 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 { } 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, ChatID: provider.ID,
Text: text, Text: text,
ParseMode: "MARKDOWN", ParseMode: "MARKDOWN",
}) })
return body return bodyAsJSON
} }
// GetDefaultAlert returns the provider's default alert configuration // GetDefaultAlert returns the provider's default alert configuration

View File

@ -116,6 +116,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Name string Name string
Provider AlertProvider Provider AlertProvider
Alert alert.Alert Alert alert.Alert
NoConditions bool
Resolved bool Resolved bool
ExpectedBody string ExpectedBody string
}{ }{
@ -133,18 +134,28 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
Resolved: true, 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\"}", 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 { for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) { 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( body := scenario.Provider.buildRequestBody(
&core.Endpoint{Name: "endpoint-name"}, &core.Endpoint{Name: "endpoint-name"},
&scenario.Alert, &scenario.Alert,
&core.Result{ &core.Result{ConditionResults: conditionResults},
ConditionResults: []*core.ConditionResult{
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
},
},
scenario.Resolved, scenario.Resolved,
) )
if string(body) != scenario.ExpectedBody { if string(body) != scenario.ExpectedBody {

View File

@ -377,7 +377,7 @@ func TestHandleAlertingWithProviderThatReturnsAnError(t *testing.T) {
AlertType: alert.TypeMatrix, AlertType: alert.TypeMatrix,
AlertingConfig: &alerting.Config{ AlertingConfig: &alerting.Config{
Matrix: &matrix.AlertProvider{ Matrix: &matrix.AlertProvider{
MatrixProviderConfig: matrix.MatrixProviderConfig{ ProviderConfig: matrix.ProviderConfig{
ServerURL: "https://example.com", ServerURL: "https://example.com",
AccessToken: "1", AccessToken: "1",
InternalRoomID: "!a:example.com", InternalRoomID: "!a:example.com",