From d25d57b7b19e204b502c68eeb740b2db8c38df3a Mon Sep 17 00:00:00 2001 From: stendler Date: Sun, 2 Jun 2024 22:51:28 +0200 Subject: [PATCH] feat(alerting): custom - replace placeholders in header contents --- README.md | 3 +- alerting/provider/custom/custom.go | 32 ++++--- alerting/provider/custom/custom_test.go | 112 ++++++++++++++++++------ 3 files changed, 105 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 90409b86f..e601d1755 100644 --- a/README.md +++ b/README.md @@ -1309,7 +1309,8 @@ 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. -Furthermore, you may use the following placeholders 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`), +url (`alerting.custom.url`) and header contents (`alerting.custom.headers`): - `[ALERT_DESCRIPTION]` (resolved from `endpoints[].alerts[].description`) - `[ENDPOINT_NAME]` (resolved from `endpoints[].name`) - `[ENDPOINT_GROUP]` (resolved from `endpoints[].group`) diff --git a/alerting/provider/custom/custom.go b/alerting/provider/custom/custom.go index fc661edf6..429d9a23f 100644 --- a/alerting/provider/custom/custom.go +++ b/alerting/provider/custom/custom.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "io" + "maps" "net/http" "strings" @@ -50,29 +51,32 @@ func (provider *AlertProvider) GetAlertStatePlaceholderValue(resolved bool) stri return status } +// ReplacePlaceholder replaces occurrences of the placeholder in body, url and all headers with content +func (provider *AlertProvider) ReplacePlaceholder(placeholder string, content string, body *string, url *string, headers map[string]string) { + *body = strings.ReplaceAll(*body, placeholder, content) + *url = strings.ReplaceAll(*url, placeholder, content) + for k, v := range headers { + headers[k] = strings.ReplaceAll(v, placeholder, content) + } +} + func (provider *AlertProvider) buildHTTPRequest(ep *endpoint.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]", ep.Name) - url = strings.ReplaceAll(url, "[ENDPOINT_NAME]", ep.Name) - body = strings.ReplaceAll(body, "[ENDPOINT_GROUP]", ep.Group) - url = strings.ReplaceAll(url, "[ENDPOINT_GROUP]", ep.Group) - body = strings.ReplaceAll(body, "[ENDPOINT_URL]", ep.URL) - url = strings.ReplaceAll(url, "[ENDPOINT_URL]", ep.URL) + body, url, method, headers := provider.Body, provider.URL, provider.Method, maps.Clone(provider.Headers) + provider.ReplacePlaceholder("[ALERT_DESCRIPTION]", alert.GetDescription(), &body, &url, headers) + provider.ReplacePlaceholder("[ENDPOINT_NAME]", ep.Name, &body, &url, headers) + provider.ReplacePlaceholder("[ENDPOINT_GROUP]", ep.Group, &body, &url, headers) + provider.ReplacePlaceholder("[ENDPOINT_URL]", ep.URL, &body, &url, headers) if resolved { - body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true)) - url = strings.ReplaceAll(url, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true)) + provider.ReplacePlaceholder("[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true), &body, &url, headers) } else { - body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(false)) - url = strings.ReplaceAll(url, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(false)) + provider.ReplacePlaceholder("[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(false), &body, &url, headers) } if len(method) == 0 { method = http.MethodGet } bodyBuffer := bytes.NewBuffer([]byte(body)) request, _ := http.NewRequest(method, url, bodyBuffer) - for k, v := range provider.Headers { + for k, v := range headers { request.Header.Set(k, v) } return request diff --git a/alerting/provider/custom/custom_test.go b/alerting/provider/custom/custom_test.go index 1678a7fef..bb79e40b2 100644 --- a/alerting/provider/custom/custom_test.go +++ b/alerting/provider/custom/custom_test.go @@ -112,27 +112,31 @@ func TestAlertProvider_Send(t *testing.T) { func TestAlertProvider_buildHTTPRequest(t *testing.T) { customAlertProvider := &AlertProvider{ - 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]", + 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]", + Headers: map[string]string{"Test": "[ENDPOINT_NAME],[ENDPOINT_GROUP],[ALERT_DESCRIPTION],[ENDPOINT_URL],[ALERT_TRIGGERED_OR_RESOLVED]"}, } alertDescription := "alert-description" scenarios := []struct { - AlertProvider *AlertProvider - Resolved bool - ExpectedURL string - ExpectedBody string + AlertProvider *AlertProvider + Resolved bool + ExpectedURL string + ExpectedBody string + ExpectedHeader 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: 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", + ExpectedHeader: "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", + 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", + ExpectedHeader: "endpoint-name,endpoint-group,alert-description,https://example.com,TRIGGERED", }, } for _, scenario := range scenarios { @@ -149,6 +153,10 @@ func TestAlertProvider_buildHTTPRequest(t *testing.T) { if string(body) != scenario.ExpectedBody { t.Error("expected body to be", scenario.ExpectedBody, "got", string(body)) } + header := request.Header.Get("Test") + if header != scenario.ExpectedHeader { + t.Error("expected header to be", scenario.ExpectedHeader, "got", header) + } }) } } @@ -157,7 +165,7 @@ func TestAlertProvider_buildHTTPRequestWithCustomPlaceholder(t *testing.T) { customAlertProvider := &AlertProvider{ 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, + Headers: map[string]string{"Test": "[ENDPOINT_NAME],[ENDPOINT_GROUP],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]"}, Placeholders: map[string]map[string]string{ "ALERT_TRIGGERED_OR_RESOLVED": { "RESOLVED": "fixed", @@ -167,22 +175,25 @@ func TestAlertProvider_buildHTTPRequestWithCustomPlaceholder(t *testing.T) { } alertDescription := "alert-description" scenarios := []struct { - AlertProvider *AlertProvider - Resolved bool - ExpectedURL string - ExpectedBody string + AlertProvider *AlertProvider + Resolved bool + ExpectedURL string + ExpectedBody string + ExpectedHeader 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: true, + ExpectedURL: "https://example.com/endpoint-group/endpoint-name?event=fixed&description=alert-description", + ExpectedBody: "endpoint-name,endpoint-group,alert-description,fixed", + ExpectedHeader: "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", + 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", + ExpectedHeader: "endpoint-name,endpoint-group,alert-description,boom", }, } for _, scenario := range scenarios { @@ -199,6 +210,10 @@ func TestAlertProvider_buildHTTPRequestWithCustomPlaceholder(t *testing.T) { if string(body) != scenario.ExpectedBody { t.Error("expected body to be", scenario.ExpectedBody, "got", string(body)) } + header := request.Header.Get("Test") + if header != scenario.ExpectedHeader { + t.Error("expected header to be", scenario.ExpectedHeader, "got", header) + } }) } } @@ -224,3 +239,46 @@ func TestAlertProvider_GetDefaultAlert(t *testing.T) { t.Error("expected default alert to be nil") } } + +func TestAlertProvider_ReplacePlaceholder(t *testing.T) { + placeholder := "[TEST]" + content := "replaced" + scenarios := []struct { + URL string + Body string + Header map[string]string + ExpectedURL string + ExpectedBody string + ExpectedHeader string + }{ + { + URL: "https://[TEST]/", + Body: "body to be [TEST].", + Header: map[string]string{"Test": "header to be [TEST]."}, + ExpectedURL: "https://replaced/", + ExpectedBody: "body to be replaced.", + ExpectedHeader: "header to be replaced.", + }, + { + URL: "https://TEST/", + Body: "body to be TEST.", + Header: map[string]string{"Test": "header to be TEST."}, + ExpectedURL: "https://TEST/", + ExpectedBody: "body to be TEST.", + ExpectedHeader: "header to be TEST.", + }, + } + for _, scenario := range scenarios { + a := &AlertProvider{} + a.ReplacePlaceholder(placeholder, content, &scenario.Body, &scenario.URL, scenario.Header) + if scenario.Body != scenario.ExpectedBody { + t.Error("expected body to be", scenario.ExpectedBody, "got", scenario.Body) + } + if scenario.URL != scenario.ExpectedURL { + t.Error("expected URL to be", scenario.ExpectedURL, "got", scenario.URL) + } + if scenario.Header["Test"] != scenario.ExpectedHeader { + t.Error("expected header to be", scenario.ExpectedHeader, "got", scenario.Header["Test"]) + } + } +}