diff --git a/README.md b/README.md index e5a9c455..7ac4f0e3 100644 --- a/README.md +++ b/README.md @@ -803,8 +803,11 @@ leveraging Gatus, you could have Gatus call that application endpoint when an en would then check if the endpoint that started failing was part of the recently deployed application, and if it was, then automatically roll it back. -The placeholders `[ALERT_DESCRIPTION]` and `[ENDPOINT_NAME]` are automatically substituted for the alert description and -the endpoint name. These placeholders can be used in the body (`alerting.custom.body`) and in the url (`alerting.custom.url`). +Furthermore, you may use the following placeholders in the body (`alerting.custom.body`) and in the url (`alerting.custom.url`): +- `[ALERT_DESCRIPTION]` (resolved from `endpoints[].alerts[].description`) +- `[ENDPOINT_NAME]` (resolved from `endpoints[].name`) +- `[ENDPOINT_GROUP]` (resolved from `endpoints[].group`) +- `[ENDPOINT_URL]` (resolved from `endpoints[].url`) If you have an alert using the `custom` provider with `send-on-resolved` set to `true`, you can use the `[ALERT_TRIGGERED_OR_RESOLVED]` placeholder to differentiate the notifications. @@ -819,7 +822,7 @@ alerting: method: "POST" body: | { - "text": "[ALERT_TRIGGERED_OR_RESOLVED]: [ENDPOINT_NAME] - [ALERT_DESCRIPTION]" + "text": "[ALERT_TRIGGERED_OR_RESOLVED]: [ENDPOINT_GROUP] - [ENDPOINT_NAME] - [ALERT_DESCRIPTION]" } endpoints: - name: website diff --git a/alerting/provider/custom/custom.go b/alerting/provider/custom/custom.go index 147a9417..967afedc 100644 --- a/alerting/provider/custom/custom.go +++ b/alerting/provider/custom/custom.go @@ -50,48 +50,28 @@ func (provider *AlertProvider) GetAlertStatePlaceholderValue(resolved bool) stri return status } -func (provider *AlertProvider) buildHTTPRequest(endpointName, alertDescription string, resolved bool) *http.Request { - body := provider.Body - providerURL := provider.URL - method := provider.Method - - if strings.Contains(body, "[ALERT_DESCRIPTION]") { - body = strings.ReplaceAll(body, "[ALERT_DESCRIPTION]", alertDescription) - } - if strings.Contains(body, "[SERVICE_NAME]") { // XXX: Remove this in v4.0.0 - body = strings.ReplaceAll(body, "[SERVICE_NAME]", endpointName) - } - if strings.Contains(body, "[ENDPOINT_NAME]") { - body = strings.ReplaceAll(body, "[ENDPOINT_NAME]", endpointName) - } - if strings.Contains(body, "[ALERT_TRIGGERED_OR_RESOLVED]") { - if resolved { - body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true)) - } else { - body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(false)) - } - } - if strings.Contains(providerURL, "[ALERT_DESCRIPTION]") { - providerURL = strings.ReplaceAll(providerURL, "[ALERT_DESCRIPTION]", alertDescription) - } - if strings.Contains(providerURL, "[SERVICE_NAME]") { // XXX: Remove this in v4.0.0 - providerURL = strings.ReplaceAll(providerURL, "[SERVICE_NAME]", endpointName) - } - if strings.Contains(providerURL, "[ENDPOINT_NAME]") { - providerURL = strings.ReplaceAll(providerURL, "[ENDPOINT_NAME]", endpointName) - } - if strings.Contains(providerURL, "[ALERT_TRIGGERED_OR_RESOLVED]") { - if resolved { - providerURL = strings.ReplaceAll(providerURL, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true)) - } else { - providerURL = strings.ReplaceAll(providerURL, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(false)) - } +func (provider *AlertProvider) buildHTTPRequest(endpoint *core.Endpoint, alert *alert.Alert, resolved bool) *http.Request { + body, url, method := provider.Body, provider.URL, provider.Method + body = strings.ReplaceAll(body, "[ALERT_DESCRIPTION]", alert.GetDescription()) + url = strings.ReplaceAll(url, "[ALERT_DESCRIPTION]", alert.GetDescription()) + body = strings.ReplaceAll(body, "[ENDPOINT_NAME]", endpoint.Name) + url = strings.ReplaceAll(url, "[ENDPOINT_NAME]", endpoint.Name) + body = strings.ReplaceAll(body, "[ENDPOINT_GROUP]", endpoint.Group) + url = strings.ReplaceAll(url, "[ENDPOINT_GROUP]", endpoint.Group) + body = strings.ReplaceAll(body, "[ENDPOINT_URL]", endpoint.URL) + url = strings.ReplaceAll(url, "[ENDPOINT_URL]", endpoint.URL) + if resolved { + body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true)) + url = strings.ReplaceAll(url, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true)) + } else { + body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(false)) + url = strings.ReplaceAll(url, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(false)) } if len(method) == 0 { method = http.MethodGet } bodyBuffer := bytes.NewBuffer([]byte(body)) - request, _ := http.NewRequest(method, providerURL, bodyBuffer) + request, _ := http.NewRequest(method, url, bodyBuffer) for k, v := range provider.Headers { request.Header.Set(k, v) } @@ -99,7 +79,7 @@ func (provider *AlertProvider) buildHTTPRequest(endpointName, alertDescription s } func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error { - request := provider.buildHTTPRequest(endpoint.Name, alert.GetDescription(), resolved) + request := provider.buildHTTPRequest(endpoint, alert, resolved) response, err := client.GetHTTPClient(provider.ClientConfig).Do(request) if err != nil { return err diff --git a/alerting/provider/custom/custom_test.go b/alerting/provider/custom/custom_test.go index 050d72d0..24fe0e10 100644 --- a/alerting/provider/custom/custom_test.go +++ b/alerting/provider/custom/custom_test.go @@ -1,6 +1,7 @@ package custom import ( + "fmt" "io" "net/http" "testing" @@ -99,77 +100,103 @@ func TestAlertProvider_Send(t *testing.T) { } } -func TestAlertProvider_buildHTTPRequestWhenResolved(t *testing.T) { - const ( - ExpectedURL = "https://example.com/endpoint-name?event=RESOLVED&description=alert-description" - ExpectedBody = "endpoint-name,alert-description,RESOLVED" - ) +func TestAlertProvider_buildHTTPRequest(t *testing.T) { customAlertProvider := &AlertProvider{ - URL: "https://example.com/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]", - Body: "[ENDPOINT_NAME],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]", - Headers: nil, + URL: "https://example.com/[ENDPOINT_GROUP]/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]&url=[ENDPOINT_URL]", + Body: "[ENDPOINT_NAME],[ENDPOINT_GROUP],[ALERT_DESCRIPTION],[ENDPOINT_URL],[ALERT_TRIGGERED_OR_RESOLVED]", } - request := customAlertProvider.buildHTTPRequest("endpoint-name", "alert-description", true) - if request.URL.String() != ExpectedURL { - t.Error("expected URL to be", ExpectedURL, "was", request.URL.String()) + alertDescription := "alert-description" + scenarios := []struct { + AlertProvider *AlertProvider + Resolved bool + ExpectedURL string + ExpectedBody string + }{ + { + AlertProvider: customAlertProvider, + Resolved: true, + ExpectedURL: "https://example.com/endpoint-group/endpoint-name?event=RESOLVED&description=alert-description&url=https://example.com", + ExpectedBody: "endpoint-name,endpoint-group,alert-description,https://example.com,RESOLVED", + }, + { + AlertProvider: customAlertProvider, + Resolved: false, + ExpectedURL: "https://example.com/endpoint-group/endpoint-name?event=TRIGGERED&description=alert-description&url=https://example.com", + ExpectedBody: "endpoint-name,endpoint-group,alert-description,https://example.com,TRIGGERED", + }, } - body, _ := io.ReadAll(request.Body) - if string(body) != ExpectedBody { - t.Error("expected body to be", ExpectedBody, "was", string(body)) - } -} - -func TestAlertProvider_buildHTTPRequestWhenTriggered(t *testing.T) { - const ( - ExpectedURL = "https://example.com/endpoint-name?event=TRIGGERED&description=alert-description" - ExpectedBody = "endpoint-name,alert-description,TRIGGERED" - ) - customAlertProvider := &AlertProvider{ - URL: "https://example.com/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]", - Body: "[ENDPOINT_NAME],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]", - Headers: map[string]string{"Authorization": "Basic hunter2"}, - } - request := customAlertProvider.buildHTTPRequest("endpoint-name", "alert-description", false) - if request.URL.String() != ExpectedURL { - t.Error("expected URL to be", ExpectedURL, "was", request.URL.String()) - } - body, _ := io.ReadAll(request.Body) - if string(body) != ExpectedBody { - t.Error("expected body to be", ExpectedBody, "was", string(body)) + for _, scenario := range scenarios { + t.Run(fmt.Sprintf("resolved-%v-with-default-placeholders", scenario.Resolved), func(t *testing.T) { + request := customAlertProvider.buildHTTPRequest( + &core.Endpoint{Name: "endpoint-name", Group: "endpoint-group", URL: "https://example.com"}, + &alert.Alert{Description: &alertDescription}, + scenario.Resolved, + ) + if request.URL.String() != scenario.ExpectedURL { + t.Error("expected URL to be", scenario.ExpectedURL, "got", request.URL.String()) + } + body, _ := io.ReadAll(request.Body) + if string(body) != scenario.ExpectedBody { + t.Error("expected body to be", scenario.ExpectedBody, "got", string(body)) + } + }) } } func TestAlertProvider_buildHTTPRequestWithCustomPlaceholder(t *testing.T) { - const ( - ExpectedURL = "https://example.com/endpoint-name?event=test&description=alert-description" - ExpectedBody = "endpoint-name,alert-description,test" - ) customAlertProvider := &AlertProvider{ - URL: "https://example.com/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]", - Body: "[ENDPOINT_NAME],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]", + URL: "https://example.com/[ENDPOINT_GROUP]/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]", + Body: "[ENDPOINT_NAME],[ENDPOINT_GROUP],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]", Headers: nil, Placeholders: map[string]map[string]string{ "ALERT_TRIGGERED_OR_RESOLVED": { - "RESOLVED": "test", + "RESOLVED": "fixed", + "TRIGGERED": "boom", }, }, } - request := customAlertProvider.buildHTTPRequest("endpoint-name", "alert-description", true) - if request.URL.String() != ExpectedURL { - t.Error("expected URL to be", ExpectedURL, "was", request.URL.String()) + alertDescription := "alert-description" + scenarios := []struct { + AlertProvider *AlertProvider + Resolved bool + ExpectedURL string + ExpectedBody string + }{ + { + AlertProvider: customAlertProvider, + Resolved: true, + ExpectedURL: "https://example.com/endpoint-group/endpoint-name?event=fixed&description=alert-description", + ExpectedBody: "endpoint-name,endpoint-group,alert-description,fixed", + }, + { + AlertProvider: customAlertProvider, + Resolved: false, + ExpectedURL: "https://example.com/endpoint-group/endpoint-name?event=boom&description=alert-description", + ExpectedBody: "endpoint-name,endpoint-group,alert-description,boom", + }, } - body, _ := io.ReadAll(request.Body) - if string(body) != ExpectedBody { - t.Error("expected body to be", ExpectedBody, "was", string(body)) + for _, scenario := range scenarios { + t.Run(fmt.Sprintf("resolved-%v-with-custom-placeholders", scenario.Resolved), func(t *testing.T) { + request := customAlertProvider.buildHTTPRequest( + &core.Endpoint{Name: "endpoint-name", Group: "endpoint-group"}, + &alert.Alert{Description: &alertDescription}, + scenario.Resolved, + ) + if request.URL.String() != scenario.ExpectedURL { + t.Error("expected URL to be", scenario.ExpectedURL, "got", request.URL.String()) + } + body, _ := io.ReadAll(request.Body) + if string(body) != scenario.ExpectedBody { + t.Error("expected body to be", scenario.ExpectedBody, "got", string(body)) + } + }) } } func TestAlertProvider_GetAlertStatePlaceholderValueDefaults(t *testing.T) { customAlertProvider := &AlertProvider{ - URL: "https://example.com/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]", - Body: "[ENDPOINT_NAME],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]", - Headers: nil, - Placeholders: nil, + URL: "https://example.com/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]", + Body: "[ENDPOINT_NAME],[ENDPOINT_GROUP],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]", } if customAlertProvider.GetAlertStatePlaceholderValue(true) != "RESOLVED" { t.Error("expected RESOLVED, got", customAlertProvider.GetAlertStatePlaceholderValue(true)) @@ -187,26 +214,3 @@ func TestAlertProvider_GetDefaultAlert(t *testing.T) { t.Error("expected default alert to be nil") } } - -// TestAlertProvider_isBackwardCompatibleWithServiceRename checks if the custom alerting provider still supports -// service placeholders after the migration from "service" to "endpoint" -// -// XXX: Remove this in v4.0.0 -func TestAlertProvider_isBackwardCompatibleWithServiceRename(t *testing.T) { - const ( - ExpectedURL = "https://example.com/endpoint-name?event=TRIGGERED&description=alert-description" - ExpectedBody = "endpoint-name,alert-description,TRIGGERED" - ) - customAlertProvider := &AlertProvider{ - URL: "https://example.com/[SERVICE_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]", - Body: "[SERVICE_NAME],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]", - } - request := customAlertProvider.buildHTTPRequest("endpoint-name", "alert-description", false) - if request.URL.String() != ExpectedURL { - t.Error("expected URL to be", ExpectedURL, "was", request.URL.String()) - } - body, _ := io.ReadAll(request.Body) - if string(body) != ExpectedBody { - t.Error("expected body to be", ExpectedBody, "was", string(body)) - } -}