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:
		| @ -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 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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)) | ||||
| 	} | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user