mirror of
https://github.com/TwiN/gatus.git
synced 2024-12-15 17:51:09 +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:
parent
ab52676f23
commit
f899f41d16
3 changed files with 102 additions and 115 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue