Repository: TwiN/gatus (go)

Issue:
Include group in alert message
My Gatus instance is monitoring the same service on multiple servers. I've grouped everything per server which makes everything very clear on the dashboard. 

However I just received an alert on Telegram and it didn't include the group in the message. So I still had to open the dashboard to check which server the message is about.

It would be great if the group could be included in the message!

Pull request:
Add group in alert messages + Validate Name/Group/Description 
closes #199 
closes #220 (sorry for stealing your PR 😅)

This is technically a breaking change, because characters like `"` and `\` can no longer be put in alert descriptions, endpoint groups, and endpoint names.

I have however decided to not make this a breaking change due to how minor the breaking change is and mostly inconsequential outside of a very small subset of users.

Patch:
diff --git a/alerting/alert/alert.go b/alerting/alert/alert.go
index bb247729..e1a220e4 100644
--- a/alerting/alert/alert.go
+++ b/alerting/alert/alert.go
@@ -1,5 +1,15 @@
 package alert
 
+import (
+	"errors"
+	"strings"
+)
+
+var (
+	// ErrAlertWithInvalidDescription is the error with which Gatus will panic if an alert has an invalid character
+	ErrAlertWithInvalidDescription = errors.New("alert description must not have \" or \\")
+)
+
 // Alert is a core.Endpoint's alert configuration
 type Alert struct {
 	// Type of alert (required)
@@ -44,6 +54,20 @@ type Alert struct {
 	Triggered bool `yaml:"-"`
 }
 
+// ValidateAndSetDefaults validates the alert's configuration and sets the default value of fields that have one
+func (alert *Alert) ValidateAndSetDefaults() error {
+	if alert.FailureThreshold <= 0 {
+		alert.FailureThreshold = 3
+	}
+	if alert.SuccessThreshold <= 0 {
+		alert.SuccessThreshold = 2
+	}
+	if strings.ContainsAny(alert.GetDescription(), "\"\\") {
+		return ErrAlertWithInvalidDescription
+	}
+	return nil
+}
+
 // GetDescription retrieves the description of the alert
 func (alert Alert) GetDescription() string {
 	if alert.Description == nil {
diff --git a/alerting/provider/discord/discord.go b/alerting/provider/discord/discord.go
index 94574e4f..33e279a5 100644
--- a/alerting/provider/discord/discord.go
+++ b/alerting/provider/discord/discord.go
@@ -48,10 +48,10 @@ func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *
 	var message, results 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.Name, 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)
 		colorCode = 3066993
 	} else {
-		message = fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", endpoint.Name, 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
 	}
 	for _, conditionResult := range result.ConditionResults {
diff --git a/alerting/provider/email/email.go b/alerting/provider/email/email.go
index 721eca08..03277aa1 100644
--- a/alerting/provider/email/email.go
+++ b/alerting/provider/email/email.go
@@ -43,11 +43,11 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
 func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) (string, string) {
 	var subject, message, results string
 	if resolved {
-		subject = fmt.Sprintf("[%s] Alert resolved", endpoint.Name)
-		message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", endpoint.Name, alert.SuccessThreshold)
+		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)
 	} else {
-		subject = fmt.Sprintf("[%s] Alert triggered", endpoint.Name)
-		message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", endpoint.Name, alert.FailureThreshold)
+		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
diff --git a/alerting/provider/mattermost/mattermost.go b/alerting/provider/mattermost/mattermost.go
index 066ced72..964aefc0 100644
--- a/alerting/provider/mattermost/mattermost.go
+++ b/alerting/provider/mattermost/mattermost.go
@@ -53,10 +53,10 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
 func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) 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.Name, 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"
 	} else {
-		message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.Name, 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"
 	}
 	var results string
diff --git a/alerting/provider/messagebird/messagebird.go b/alerting/provider/messagebird/messagebird.go
index 1bcfbf7a..6dc10c0a 100644
--- a/alerting/provider/messagebird/messagebird.go
+++ b/alerting/provider/messagebird/messagebird.go
@@ -55,9 +55,9 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
 func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
 	var message string
 	if resolved {
-		message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.Name, alert.GetDescription())
+		message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
 	} else {
-		message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.Name, alert.GetDescription())
+		message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
 	}
 	return fmt.Sprintf(`{
   "originator": "%s",
diff --git a/alerting/provider/opsgenie/opsgenie.go b/alerting/provider/opsgenie/opsgenie.go
index a4b5746b..dd98d707 100644
--- a/alerting/provider/opsgenie/opsgenie.go
+++ b/alerting/provider/opsgenie/opsgenie.go
@@ -120,10 +120,10 @@ func (provider *AlertProvider) buildCreateRequestBody(endpoint *core.Endpoint, a
 	var message, description, results 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.Name, 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)
 	} else {
 		message = fmt.Sprintf("%s - %s", endpoint.Name, alert.GetDescription())
-		description = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.Name, alert.FailureThreshold)
+		description = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
 	}
 	if endpoint.Group != "" {
 		message = fmt.Sprintf("[%s] %s", endpoint.Group, message)
diff --git a/alerting/provider/pagerduty/pagerduty.go b/alerting/provider/pagerduty/pagerduty.go
index b009bfab..a1f790fb 100644
--- a/alerting/provider/pagerduty/pagerduty.go
+++ b/alerting/provider/pagerduty/pagerduty.go
@@ -90,11 +90,11 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
 func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
 	var message, eventAction, resolveKey string
 	if resolved {
-		message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.Name, alert.GetDescription())
+		message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
 		eventAction = "resolve"
 		resolveKey = alert.ResolveKey
 	} else {
-		message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.Name, alert.GetDescription())
+		message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
 		eventAction = "trigger"
 		resolveKey = ""
 	}
diff --git a/alerting/provider/slack/slack.go b/alerting/provider/slack/slack.go
index d82c50a5..e48cf0f6 100644
--- a/alerting/provider/slack/slack.go
+++ b/alerting/provider/slack/slack.go
@@ -47,10 +47,10 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
 func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
 	var message, color, results string
 	if resolved {
-		message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", endpoint.Name, 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"
 	} else {
-		message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.Name, 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"
 	}
 	for _, conditionResult := range result.ConditionResults {
diff --git a/alerting/provider/teams/teams.go b/alerting/provider/teams/teams.go
index 500750c2..e2351ad8 100644
--- a/alerting/provider/teams/teams.go
+++ b/alerting/provider/teams/teams.go
@@ -47,10 +47,10 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
 func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) 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.Name, 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"
 	} else {
-		message = fmt.Sprintf("An alert for *%s* has been triggered due to having failed %d time(s) in a row", endpoint.Name, 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"
 	}
 	var results string
diff --git a/alerting/provider/telegram/telegram.go b/alerting/provider/telegram/telegram.go
index eb5a59f0..7a109f56 100644
--- a/alerting/provider/telegram/telegram.go
+++ b/alerting/provider/telegram/telegram.go
@@ -48,9 +48,9 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
 func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
 	var message, results 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.Name, alert.FailureThreshold)
+		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.FailureThreshold)
 	} else {
-		message = fmt.Sprintf("An alert for *%s* has been triggered:\\n—\\n    _healthcheck failed %d time(s) in a row_\\n—  ", endpoint.Name, 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 prefix string
diff --git a/alerting/provider/twilio/twilio.go b/alerting/provider/twilio/twilio.go
index d03347fd..ed0a04e3 100644
--- a/alerting/provider/twilio/twilio.go
+++ b/alerting/provider/twilio/twilio.go
@@ -53,9 +53,9 @@ func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert,
 func (provider *AlertProvider) buildRequestBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) string {
 	var message string
 	if resolved {
-		message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.Name, alert.GetDescription())
+		message = fmt.Sprintf("RESOLVED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
 	} else {
-		message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.Name, alert.GetDescription())
+		message = fmt.Sprintf("TRIGGERED: %s - %s", endpoint.DisplayName(), alert.GetDescription())
 	}
 	return url.Values{
 		"To":   {provider.To},
diff --git a/core/endpoint.go b/core/endpoint.go
index f46c15af..1b908ae2 100644
--- a/core/endpoint.go
+++ b/core/endpoint.go
@@ -41,6 +41,9 @@ var (
 
 	// ErrEndpointWithNoName is the error with which Gatus will panic if an endpoint is configured with no name
 	ErrEndpointWithNoName = errors.New("you must specify a name for each endpoint")
+
+	// ErrEndpointWithInvalidNameOrGroup is the error with which Gatus will panic if an endpoint has an invalid character where it shouldn't
+	ErrEndpointWithInvalidNameOrGroup = errors.New("endpoint name and group must not have \" or \\")
 )
 
 // Endpoint is the configuration of a monitored
@@ -132,16 +135,16 @@ func (endpoint *Endpoint) ValidateAndSetDefaults() error {
 		endpoint.Headers[ContentTypeHeader] = "application/json"
 	}
 	for _, endpointAlert := range endpoint.Alerts {
-		if endpointAlert.FailureThreshold <= 0 {
-			endpointAlert.FailureThreshold = 3
-		}
-		if endpointAlert.SuccessThreshold <= 0 {
-			endpointAlert.SuccessThreshold = 2
+		if err := endpointAlert.ValidateAndSetDefaults(); err != nil {
+			return err
 		}
 	}
 	if len(endpoint.Name) == 0 {
 		return ErrEndpointWithNoName
 	}
+	if strings.ContainsAny(endpoint.Name, "\"\\") || strings.ContainsAny(endpoint.Group, "\"\\") {
+		return ErrEndpointWithInvalidNameOrGroup
+	}
 	if len(endpoint.URL) == 0 {
 		return ErrEndpointWithNoURL
 	}
@@ -159,6 +162,14 @@ func (endpoint *Endpoint) ValidateAndSetDefaults() error {
 	return nil
 }
 
+// DisplayName returns an identifier made up of the Name and, if not empty, the Group.
+func (endpoint Endpoint) DisplayName() string {
+	if len(endpoint.Group) > 0 {
+		return endpoint.Group + "/" + endpoint.Name
+	}
+	return endpoint.Name
+}
+
 // Key returns the unique key for the Endpoint
 func (endpoint Endpoint) Key() string {
 	return util.ConvertGroupAndEndpointNameToKey(endpoint.Group, endpoint.Name)


Test patch:
diff --git a/alerting/alert/alert_test.go b/alerting/alert/alert_test.go
index a061a53d..b764aeb0 100644
--- a/alerting/alert/alert_test.go
+++ b/alerting/alert/alert_test.go
@@ -1,6 +1,55 @@
 package alert
 
-import "testing"
+import (
+	"testing"
+)
+
+func TestAlert_ValidateAndSetDefaults(t *testing.T) {
+	invalidDescription := "\""
+	scenarios := []struct {
+		name                     string
+		alert                    Alert
+		expectedError            error
+		expectedSuccessThreshold int
+		expectedFailureThreshold int
+	}{
+		{
+			name: "valid-empty",
+			alert: Alert{
+				Description:      nil,
+				FailureThreshold: 0,
+				SuccessThreshold: 0,
+			},
+			expectedError:            nil,
+			expectedFailureThreshold: 3,
+			expectedSuccessThreshold: 2,
+		},
+		{
+			name: "invalid-description",
+			alert: Alert{
+				Description:      &invalidDescription,
+				FailureThreshold: 10,
+				SuccessThreshold: 5,
+			},
+			expectedError:            ErrAlertWithInvalidDescription,
+			expectedFailureThreshold: 10,
+			expectedSuccessThreshold: 5,
+		},
+	}
+	for _, scenario := range scenarios {
+		t.Run(scenario.name, func(t *testing.T) {
+			if err := scenario.alert.ValidateAndSetDefaults(); err != scenario.expectedError {
+				t.Errorf("expected error %v, got %v", scenario.expectedError, err)
+			}
+			if scenario.alert.SuccessThreshold != scenario.expectedSuccessThreshold {
+				t.Errorf("expected success threshold %v, got %v", scenario.expectedSuccessThreshold, scenario.alert.SuccessThreshold)
+			}
+			if scenario.alert.FailureThreshold != scenario.expectedFailureThreshold {
+				t.Errorf("expected failure threshold %v, got %v", scenario.expectedFailureThreshold, scenario.alert.FailureThreshold)
+			}
+		})
+	}
+}
 
 func TestAlert_IsEnabled(t *testing.T) {
 	if (Alert{Enabled: nil}).IsEnabled() {
diff --git a/alerting/provider/opsgenie/opsgenie_test.go b/alerting/provider/opsgenie/opsgenie_test.go
index d6b9cd95..244fac92 100644
--- a/alerting/provider/opsgenie/opsgenie_test.go
+++ b/alerting/provider/opsgenie/opsgenie_test.go
@@ -155,7 +155,7 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
 				FailureThreshold: 3,
 			},
 			Endpoint: &core.Endpoint{
-				Name: "my supper app",
+				Name: "my super app",
 			},
 			Result: &core.Result{
 				ConditionResults: []*core.ConditionResult{
@@ -171,16 +171,14 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
 			},
 			Resolved: false,
 			want: alertCreateRequest{
-				Message:  "my supper app - " + description,
-				Priority: "P1",
-				Source:   "gatus",
-				Entity:   "gatus-my-supper-app",
-				Alias:    "gatus-healthcheck-my-supper-app",
-				Description: "An alert for *my supper app* has been triggered due to having failed 3 time(s) in a row\n" +
-					"▣ - `[STATUS] == 200`\n" +
-					"▢ - `[BODY] == OK`\n",
-				Tags:    nil,
-				Details: map[string]string{},
+				Message:     "my super app - " + description,
+				Priority:    "P1",
+				Source:      "gatus",
+				Entity:      "gatus-my-super-app",
+				Alias:       "gatus-healthcheck-my-super-app",
+				Description: "An alert for *my super app* has been triggered due to having failed 3 time(s) in a row\n▣ - `[STATUS] == 200`\n▢ - `[BODY] == OK`\n",
+				Tags:        nil,
+				Details:     map[string]string{},
 			},
 		},
 		{
@@ -209,15 +207,14 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
 			},
 			Resolved: true,
 			want: alertCreateRequest{
-				Message:  "RESOLVED: my mega app - " + description,
-				Priority: "P5",
-				Source:   "gatus-hc",
-				Entity:   "oompa-my-mega-app",
-				Alias:    "loompa-my-mega-app",
-				Description: "An alert for *my mega app* has been resolved after passing successfully 4 time(s) in a row\n" +
-					"▣ - `[STATUS] == 200`\n",
-				Tags:    []string{"do-ba-dee-doo"},
-				Details: map[string]string{},
+				Message:     "RESOLVED: my mega app - " + description,
+				Priority:    "P5",
+				Source:      "gatus-hc",
+				Entity:      "oompa-my-mega-app",
+				Alias:       "loompa-my-mega-app",
+				Description: "An alert for *my mega app* has been resolved after passing successfully 4 time(s) in a row\n▣ - `[STATUS] == 200`\n",
+				Tags:        []string{"do-ba-dee-doo"},
+				Details:     map[string]string{},
 			},
 		},
 		{
@@ -248,14 +245,13 @@ func TestAlertProvider_buildCreateRequestBody(t *testing.T) {
 			},
 			Resolved: false,
 			want: alertCreateRequest{
-				Message:  "[end game] my app - " + description,
-				Priority: "P1",
-				Source:   "gatus",
-				Entity:   "gatus-end-game-my-app",
-				Alias:    "gatus-healthcheck-end-game-my-app",
-				Description: "An alert for *my app* has been triggered due to having failed 6 time(s) in a row\n" +
-					"▢ - `[STATUS] == 200`\n",
-				Tags: []string{"foo"},
+				Message:     "[end game] my app - " + description,
+				Priority:    "P1",
+				Source:      "gatus",
+				Entity:      "gatus-end-game-my-app",
+				Alias:       "gatus-healthcheck-end-game-my-app",
+				Description: "An alert for *end game/my app* has been triggered due to having failed 6 time(s) in a row\n▢ - `[STATUS] == 200`\n",
+				Tags:        []string{"foo"},
 				Details: map[string]string{
 					"endpoint:url":       "https://my.go/app",
 					"endpoint:group":     "end game",
diff --git a/alerting/provider/slack/slack_test.go b/alerting/provider/slack/slack_test.go
index 7dd04bdc..1f60f547 100644
--- a/alerting/provider/slack/slack_test.go
+++ b/alerting/provider/slack/slack_test.go
@@ -16,7 +16,7 @@ func TestAlertProvider_IsValid(t *testing.T) {
 	if invalidProvider.IsValid() {
 		t.Error("provider shouldn't have been valid")
 	}
-	validProvider := AlertProvider{WebhookURL: "http://example.com"}
+	validProvider := AlertProvider{WebhookURL: "https://example.com"}
 	if !validProvider.IsValid() {
 		t.Error("provider should've been valid")
 	}
@@ -79,7 +79,7 @@ func TestAlertProvider_Send(t *testing.T) {
 		t.Run(scenario.Name, func(t *testing.T) {
 			client.InjectHTTPClient(&http.Client{Transport: scenario.MockRoundTripper})
 			err := scenario.Provider.Send(
-				&core.Endpoint{Name: "endpoint-name"},
+				&core.Endpoint{Name: "name"},
 				&scenario.Alert,
 				&core.Result{
 					ConditionResults: []*core.ConditionResult{
@@ -105,6 +105,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
 	scenarios := []struct {
 		Name         string
 		Provider     AlertProvider
+		Endpoint     core.Endpoint
 		Alert        alert.Alert
 		Resolved     bool
 		ExpectedBody string
@@ -112,22 +113,40 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
 		{
 			Name:         "triggered",
 			Provider:     AlertProvider{},
+			Endpoint:     core.Endpoint{Name: "name"},
 			Alert:        alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
 			Resolved:     false,
-			ExpectedBody: "{\n  \"text\": \"\",\n  \"attachments\": [\n    {\n      \"title\": \":helmet_with_white_cross: Gatus\",\n      \"text\": \"An alert for *endpoint-name* has been triggered due to having failed 3 time(s) in a row:\\n> description-1\",\n      \"short\": false,\n      \"color\": \"#DD0000\",\n      \"fields\": [\n        {\n          \"title\": \"Condition results\",\n          \"value\": \":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\n          \"short\": false\n        }\n      ]\n    }\n  ]\n}",
+			ExpectedBody: "{\n  \"text\": \"\",\n  \"attachments\": [\n    {\n      \"title\": \":helmet_with_white_cross: Gatus\",\n      \"text\": \"An alert for *name* has been triggered due to having failed 3 time(s) in a row:\\n> description-1\",\n      \"short\": false,\n      \"color\": \"#DD0000\",\n      \"fields\": [\n        {\n          \"title\": \"Condition results\",\n          \"value\": \":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\n          \"short\": false\n        }\n      ]\n    }\n  ]\n}",
+		},
+		{
+			Name:         "triggered-with-group",
+			Provider:     AlertProvider{},
+			Endpoint:     core.Endpoint{Name: "name", Group: "group"},
+			Alert:        alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
+			Resolved:     false,
+			ExpectedBody: "{\n  \"text\": \"\",\n  \"attachments\": [\n    {\n      \"title\": \":helmet_with_white_cross: Gatus\",\n      \"text\": \"An alert for *group/name* has been triggered due to having failed 3 time(s) in a row:\\n> description-1\",\n      \"short\": false,\n      \"color\": \"#DD0000\",\n      \"fields\": [\n        {\n          \"title\": \"Condition results\",\n          \"value\": \":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n\",\n          \"short\": false\n        }\n      ]\n    }\n  ]\n}",
 		},
 		{
 			Name:         "resolved",
 			Provider:     AlertProvider{},
+			Endpoint:     core.Endpoint{Name: "name"},
+			Alert:        alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
+			Resolved:     true,
+			ExpectedBody: "{\n  \"text\": \"\",\n  \"attachments\": [\n    {\n      \"title\": \":helmet_with_white_cross: Gatus\",\n      \"text\": \"An alert for *name* has been resolved after passing successfully 5 time(s) in a row:\\n> description-2\",\n      \"short\": false,\n      \"color\": \"#36A64F\",\n      \"fields\": [\n        {\n          \"title\": \"Condition results\",\n          \"value\": \":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\n          \"short\": false\n        }\n      ]\n    }\n  ]\n}",
+		},
+		{
+			Name:         "resolved-with-group",
+			Provider:     AlertProvider{},
+			Endpoint:     core.Endpoint{Name: "name", Group: "group"},
 			Alert:        alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
 			Resolved:     true,
-			ExpectedBody: "{\n  \"text\": \"\",\n  \"attachments\": [\n    {\n      \"title\": \":helmet_with_white_cross: Gatus\",\n      \"text\": \"An alert for *endpoint-name* has been resolved after passing successfully 5 time(s) in a row:\\n> description-2\",\n      \"short\": false,\n      \"color\": \"#36A64F\",\n      \"fields\": [\n        {\n          \"title\": \"Condition results\",\n          \"value\": \":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\n          \"short\": false\n        }\n      ]\n    }\n  ]\n}",
+			ExpectedBody: "{\n  \"text\": \"\",\n  \"attachments\": [\n    {\n      \"title\": \":helmet_with_white_cross: Gatus\",\n      \"text\": \"An alert for *group/name* has been resolved after passing successfully 5 time(s) in a row:\\n> description-2\",\n      \"short\": false,\n      \"color\": \"#36A64F\",\n      \"fields\": [\n        {\n          \"title\": \"Condition results\",\n          \"value\": \":white_check_mark: - `[CONNECTED] == true`\\n:white_check_mark: - `[STATUS] == 200`\\n\",\n          \"short\": false\n        }\n      ]\n    }\n  ]\n}",
 		},
 	}
 	for _, scenario := range scenarios {
 		t.Run(scenario.Name, func(t *testing.T) {
 			body := scenario.Provider.buildRequestBody(
-				&core.Endpoint{Name: "endpoint-name"},
+				&scenario.Endpoint,
 				&scenario.Alert,
 				&core.Result{
 					ConditionResults: []*core.ConditionResult{