Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(alerting): custom - replace placeholders in header contents #789

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
32 changes: 18 additions & 14 deletions alerting/provider/custom/custom.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"io"
"maps"
"net/http"
"strings"

Expand Down Expand Up @@ -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
Expand Down
112 changes: 85 additions & 27 deletions alerting/provider/custom/custom_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
})
}
}
Expand All @@ -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",
Expand All @@ -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 {
Expand All @@ -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)
}
})
}
}
Expand All @@ -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"])
}
}
}