diff --git a/alerting/provider/discord/discord.go b/alerting/provider/discord/discord.go index 64281d7d..74380a50 100644 --- a/alerting/provider/discord/discord.go +++ b/alerting/provider/discord/discord.go @@ -2,6 +2,7 @@ package discord import ( "bytes" + "encoding/json" "fmt" "io" "net/http" @@ -44,7 +45,7 @@ func (provider *AlertProvider) IsValid() bool { // Send an alert using the provider func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { - buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(endpoint, alert, result, resolved))) + buffer := bytes.NewBuffer(provider.buildRequestBody(endpoint, alert, result, resolved)) request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(endpoint.Group), buffer) if err != nil { return err @@ -62,9 +63,27 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, return err } +type Body struct { + Content string `json:"content"` + Embeds []Embed `json:"embeds"` +} + +type Embed struct { + Title string `json:"title"` + Description string `json:"description"` + Color int `json:"color"` + Fields []Field `json:"fields"` +} + +type Field struct { + Name string `json:"name"` + Value string `json:"value"` + Inline bool `json:"inline"` +} + // buildRequestBody builds the request body for the provider -func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string { - var message, results string +func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) []byte { + 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) @@ -73,36 +92,38 @@ 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 } - for _, conditionResult := range result.ConditionResults { + fields := make([]Field, len(result.ConditionResults)) + for i, conditionResult := range result.ConditionResults { var prefix string if conditionResult.Success { prefix = ":white_check_mark:" } else { prefix = ":x:" } - results += fmt.Sprintf("%s - `%s`\\n", prefix, conditionResult.Condition) + fields[i] = Field{ + Value: fmt.Sprintf("%s - `%s`", prefix, conditionResult.Condition), + Inline: false, + } + if i == 0 { + fields[i].Name = "Condition results" + } } var description string if alertDescription := alert.GetDescription(); len(alertDescription) > 0 { - description = ":\\n> " + alertDescription + description = ":\n> " + alertDescription } - return fmt.Sprintf(`{ - "content": "", - "embeds": [ - { - "title": ":helmet_with_white_cross: Gatus", - "description": "%s%s", - "color": %d, - "fields": [ - { - "name": "Condition results", - "value": "%s", - "inline": false - } - ] - } - ] -}`, message, description, colorCode, results) + body, _ := json.Marshal(Body{ + Content: message, + Embeds: []Embed{ + { + Title: ":helmet_with_white_cross: Gatus", + Description: message + description, + Color: colorCode, + Fields: fields, + }, + }, + }) + return body } // 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 f6ef7622..519f1fec 100644 --- a/alerting/provider/discord/discord_test.go +++ b/alerting/provider/discord/discord_test.go @@ -151,14 +151,14 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { Provider: AlertProvider{}, Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3}, Resolved: false, - ExpectedBody: "{\n \"content\": \"\",\n \"embeds\": [\n {\n \"title\": \":helmet_with_white_cross: Gatus\",\n \"description\": \"An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\\n> description-1\",\n \"color\": 15158332,\n \"fields\": [\n {\n \"name\": \"Condition results\",\n \"value\": \":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\n \"inline\": false\n }\n ]\n }\n ]\n}", + ExpectedBody: "{\"content\":\"An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row\",\"embeds\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"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`\",\"inline\":false},{\"name\":\"\",\"value\":\":x: - `[STATUS] == 200`\",\"inline\":false},{\"name\":\"\",\"value\":\":x: - `[BODY] != \\\"\\\"`\",\"inline\":false}]}]}", }, { Name: "resolved", Provider: AlertProvider{}, Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3}, Resolved: true, - ExpectedBody: "{\n \"content\": \"\",\n \"embeds\": [\n {\n \"title\": \":helmet_with_white_cross: Gatus\",\n \"description\": \"An alert for **endpoint-name** has been resolved after passing successfully 5 time(s) in a row:\\n> description-2\",\n \"color\": 3066993,\n \"fields\": [\n {\n \"name\": \"Condition results\",\n \"value\": \":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\n \"inline\": false\n }\n ]\n }\n ]\n}", + ExpectedBody: "{\"content\":\"An alert for **endpoint-name** has been resolved after passing successfully 5 time(s) in a row\",\"embeds\":[{\"title\":\":helmet_with_white_cross: Gatus\",\"description\":\"An alert for **endpoint-name** has been resolved after passing successfully 5 time(s) in a row:\\n\\u003e description-2\",\"color\":3066993,\"fields\":[{\"name\":\"Condition results\",\"value\":\":white_check_mark: - `[CONNECTED] == true`\",\"inline\":false},{\"name\":\"\",\"value\":\":white_check_mark: - `[STATUS] == 200`\",\"inline\":false},{\"name\":\"\",\"value\":\":white_check_mark: - `[BODY] != \\\"\\\"`\",\"inline\":false}]}]}", }, } for _, scenario := range scenarios { @@ -170,15 +170,16 @@ func TestAlertProvider_buildRequestBody(t *testing.T) { ConditionResults: []*core.ConditionResult{ {Condition: "[CONNECTED] == true", Success: scenario.Resolved}, {Condition: "[STATUS] == 200", Success: scenario.Resolved}, + {Condition: "[BODY] != \"\"", Success: scenario.Resolved}, }, }, scenario.Resolved, ) - if body != scenario.ExpectedBody { + if string(body) != scenario.ExpectedBody { t.Errorf("expected %s, got %s", scenario.ExpectedBody, body) } out := make(map[string]interface{}) - if err := json.Unmarshal([]byte(body), &out); err != nil { + if err := json.Unmarshal(body, &out); err != nil { t.Error("expected body to be valid JSON, got error:", err.Error()) } })