1
0
Fork 0
mirror of https://github.com/TwiN/gatus.git synced 2024-12-14 11:58:04 +00:00

feat(alerting): Add ENDPOINT_GROUP and ENDPOINT_URL placeholders for custom provider

related: #282

note: this also phases out the deprecated [SERVICE_NAME] placeholder
This commit is contained in:
TwiN 2022-06-07 19:37:42 -04:00
parent ab52676f23
commit f899f41d16
3 changed files with 102 additions and 115 deletions

View file

@ -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, 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. then automatically roll it back.
The placeholders `[ALERT_DESCRIPTION]` and `[ENDPOINT_NAME]` are automatically substituted for the alert description and Furthermore, you may use the following placeholders in the body (`alerting.custom.body`) and in the url (`alerting.custom.url`):
the endpoint name. These placeholders can be used 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 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. `[ALERT_TRIGGERED_OR_RESOLVED]` placeholder to differentiate the notifications.
@ -819,7 +822,7 @@ alerting:
method: "POST" method: "POST"
body: | body: |
{ {
"text": "[ALERT_TRIGGERED_OR_RESOLVED]: [ENDPOINT_NAME] - [ALERT_DESCRIPTION]" "text": "[ALERT_TRIGGERED_OR_RESOLVED]: [ENDPOINT_GROUP] - [ENDPOINT_NAME] - [ALERT_DESCRIPTION]"
} }
endpoints: endpoints:
- name: website - name: website

View file

@ -50,48 +50,28 @@ func (provider *AlertProvider) GetAlertStatePlaceholderValue(resolved bool) stri
return status return status
} }
func (provider *AlertProvider) buildHTTPRequest(endpointName, alertDescription string, resolved bool) *http.Request { func (provider *AlertProvider) buildHTTPRequest(endpoint *core.Endpoint, alert *alert.Alert, resolved bool) *http.Request {
body := provider.Body body, url, method := provider.Body, provider.URL, provider.Method
providerURL := provider.URL body = strings.ReplaceAll(body, "[ALERT_DESCRIPTION]", alert.GetDescription())
method := provider.Method url = strings.ReplaceAll(url, "[ALERT_DESCRIPTION]", alert.GetDescription())
body = strings.ReplaceAll(body, "[ENDPOINT_NAME]", endpoint.Name)
if strings.Contains(body, "[ALERT_DESCRIPTION]") { url = strings.ReplaceAll(url, "[ENDPOINT_NAME]", endpoint.Name)
body = strings.ReplaceAll(body, "[ALERT_DESCRIPTION]", alertDescription) body = strings.ReplaceAll(body, "[ENDPOINT_GROUP]", endpoint.Group)
} url = strings.ReplaceAll(url, "[ENDPOINT_GROUP]", endpoint.Group)
if strings.Contains(body, "[SERVICE_NAME]") { // XXX: Remove this in v4.0.0 body = strings.ReplaceAll(body, "[ENDPOINT_URL]", endpoint.URL)
body = strings.ReplaceAll(body, "[SERVICE_NAME]", endpointName) url = strings.ReplaceAll(url, "[ENDPOINT_URL]", endpoint.URL)
}
if strings.Contains(body, "[ENDPOINT_NAME]") {
body = strings.ReplaceAll(body, "[ENDPOINT_NAME]", endpointName)
}
if strings.Contains(body, "[ALERT_TRIGGERED_OR_RESOLVED]") {
if resolved { if resolved {
body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true)) body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
url = strings.ReplaceAll(url, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
} else { } else {
body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(false)) body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(false))
} url = strings.ReplaceAll(url, "[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))
}
} }
if len(method) == 0 { if len(method) == 0 {
method = http.MethodGet method = http.MethodGet
} }
bodyBuffer := bytes.NewBuffer([]byte(body)) bodyBuffer := bytes.NewBuffer([]byte(body))
request, _ := http.NewRequest(method, providerURL, bodyBuffer) request, _ := http.NewRequest(method, url, bodyBuffer)
for k, v := range provider.Headers { for k, v := range provider.Headers {
request.Header.Set(k, v) 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 { 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) response, err := client.GetHTTPClient(provider.ClientConfig).Do(request)
if err != nil { if err != nil {
return err return err

View file

@ -1,6 +1,7 @@
package custom package custom
import ( import (
"fmt"
"io" "io"
"net/http" "net/http"
"testing" "testing"
@ -99,77 +100,103 @@ func TestAlertProvider_Send(t *testing.T) {
} }
} }
func TestAlertProvider_buildHTTPRequestWhenResolved(t *testing.T) { func TestAlertProvider_buildHTTPRequest(t *testing.T) {
const (
ExpectedURL = "https://example.com/endpoint-name?event=RESOLVED&description=alert-description"
ExpectedBody = "endpoint-name,alert-description,RESOLVED"
)
customAlertProvider := &AlertProvider{ customAlertProvider := &AlertProvider{
URL: "https://example.com/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]", URL: "https://example.com/[ENDPOINT_GROUP]/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]&url=[ENDPOINT_URL]",
Body: "[ENDPOINT_NAME],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]", Body: "[ENDPOINT_NAME],[ENDPOINT_GROUP],[ALERT_DESCRIPTION],[ENDPOINT_URL],[ALERT_TRIGGERED_OR_RESOLVED]",
Headers: nil,
} }
request := customAlertProvider.buildHTTPRequest("endpoint-name", "alert-description", true) alertDescription := "alert-description"
if request.URL.String() != ExpectedURL { scenarios := []struct {
t.Error("expected URL to be", ExpectedURL, "was", request.URL.String()) 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",
},
}
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) body, _ := io.ReadAll(request.Body)
if string(body) != ExpectedBody { if string(body) != scenario.ExpectedBody {
t.Error("expected body to be", ExpectedBody, "was", string(body)) t.Error("expected body to be", scenario.ExpectedBody, "got", 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))
} }
} }
func TestAlertProvider_buildHTTPRequestWithCustomPlaceholder(t *testing.T) { 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{ customAlertProvider := &AlertProvider{
URL: "https://example.com/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]", URL: "https://example.com/[ENDPOINT_GROUP]/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]",
Body: "[ENDPOINT_NAME],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]", Body: "[ENDPOINT_NAME],[ENDPOINT_GROUP],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]",
Headers: nil, Headers: nil,
Placeholders: map[string]map[string]string{ Placeholders: map[string]map[string]string{
"ALERT_TRIGGERED_OR_RESOLVED": { "ALERT_TRIGGERED_OR_RESOLVED": {
"RESOLVED": "test", "RESOLVED": "fixed",
"TRIGGERED": "boom",
}, },
}, },
} }
request := customAlertProvider.buildHTTPRequest("endpoint-name", "alert-description", true) alertDescription := "alert-description"
if request.URL.String() != ExpectedURL { scenarios := []struct {
t.Error("expected URL to be", ExpectedURL, "was", request.URL.String()) 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",
},
}
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) body, _ := io.ReadAll(request.Body)
if string(body) != ExpectedBody { if string(body) != scenario.ExpectedBody {
t.Error("expected body to be", ExpectedBody, "was", string(body)) t.Error("expected body to be", scenario.ExpectedBody, "got", string(body))
}
})
} }
} }
func TestAlertProvider_GetAlertStatePlaceholderValueDefaults(t *testing.T) { func TestAlertProvider_GetAlertStatePlaceholderValueDefaults(t *testing.T) {
customAlertProvider := &AlertProvider{ customAlertProvider := &AlertProvider{
URL: "https://example.com/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]", URL: "https://example.com/[ENDPOINT_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]",
Body: "[ENDPOINT_NAME],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]", Body: "[ENDPOINT_NAME],[ENDPOINT_GROUP],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]",
Headers: nil,
Placeholders: nil,
} }
if customAlertProvider.GetAlertStatePlaceholderValue(true) != "RESOLVED" { if customAlertProvider.GetAlertStatePlaceholderValue(true) != "RESOLVED" {
t.Error("expected RESOLVED, got", customAlertProvider.GetAlertStatePlaceholderValue(true)) 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") 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))
}
}