From 2873d96b9f3278b04b45d3ee1bcba51bb242f735 Mon Sep 17 00:00:00 2001 From: Robert Hoppe Date: Wed, 17 Feb 2021 12:39:17 +0100 Subject: [PATCH 1/4] Introduce configureable place holders for alerting --- alerting/provider/custom/custom.go | 44 ++++++++++++++++++++----- alerting/provider/custom/custom_test.go | 25 ++++++++++++++ config/config_test.go | 4 +++ 3 files changed, 64 insertions(+), 9 deletions(-) diff --git a/alerting/provider/custom/custom.go b/alerting/provider/custom/custom.go index ad942138..78c625e7 100644 --- a/alerting/provider/custom/custom.go +++ b/alerting/provider/custom/custom.go @@ -16,11 +16,19 @@ import ( // AlertProvider is the configuration necessary for sending an alert using a custom HTTP request // Technically, all alert providers should be reachable using the custom alert provider type AlertProvider struct { - URL string `yaml:"url"` - Method string `yaml:"method,omitempty"` - Insecure bool `yaml:"insecure,omitempty"` - Body string `yaml:"body,omitempty"` - Headers map[string]string `yaml:"headers,omitempty"` + URL string `yaml:"url"` + Method string `yaml:"method,omitempty"` + Insecure bool `yaml:"insecure,omitempty"` + Body string `yaml:"body,omitempty"` + Headers map[string]string `yaml:"headers,omitempty"` + Placeholders map[string]map[string]string `yaml:"placeholders,omitempty"` +} + +var DefaultPlaceholderValues = map[string]map[string]string{ + "ALERT_TRIGGERED_OR_RESOLVED": map[string]string{ + "triggered": "TRIGGERED", + "resolved": "RESOLVED", + }, } // IsValid returns whether the provider's configuration is valid @@ -33,10 +41,28 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler return provider } +// GetPlaceholderValue returns the Placeholder value +func (provider *AlertProvider) GetPlaceholderValue(name, status string) string { + if _, ok := provider.Placeholders[name]; ok { + if val, ok := provider.Placeholders[name][status]; ok { + return val + } + } else { + if _, ok := DefaultPlaceholderValues[name]; ok { + if val, ok := DefaultPlaceholderValues[name][status]; ok { + return val + } + } + } + + return "" +} + func (provider *AlertProvider) buildHTTPRequest(serviceName, 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) } @@ -45,9 +71,9 @@ func (provider *AlertProvider) buildHTTPRequest(serviceName, alertDescription st } if strings.Contains(body, "[ALERT_TRIGGERED_OR_RESOLVED]") { if resolved { - body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", "RESOLVED") + body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetPlaceholderValue("ALERT_TRIGGERED_OR_RESOLVED", "resolved")) } else { - body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", "TRIGGERED") + body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetPlaceholderValue("ALERT_TRIGGERED_OR_RESOLVED", "triggered")) } } if strings.Contains(providerURL, "[ALERT_DESCRIPTION]") { @@ -58,9 +84,9 @@ func (provider *AlertProvider) buildHTTPRequest(serviceName, alertDescription st } if strings.Contains(providerURL, "[ALERT_TRIGGERED_OR_RESOLVED]") { if resolved { - providerURL = strings.ReplaceAll(providerURL, "[ALERT_TRIGGERED_OR_RESOLVED]", "RESOLVED") + providerURL = strings.ReplaceAll(providerURL, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetPlaceholderValue("ALERT_TRIGGERED_OR_RESOLVED", "resolved")) } else { - providerURL = strings.ReplaceAll(providerURL, "[ALERT_TRIGGERED_OR_RESOLVED]", "TRIGGERED") + providerURL = strings.ReplaceAll(providerURL, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetPlaceholderValue("ALERT_TRIGGERED_OR_RESOLVED", "triggered")) } } if len(method) == 0 { diff --git a/alerting/provider/custom/custom_test.go b/alerting/provider/custom/custom_test.go index bcd5f669..8118fad8 100644 --- a/alerting/provider/custom/custom_test.go +++ b/alerting/provider/custom/custom_test.go @@ -68,3 +68,28 @@ func TestAlertProvider_ToCustomAlertProvider(t *testing.T) { t.Error("customAlertProvider should've been equal to customAlertProvider") } } + +func TestAlertProvider_buildHTTPRequestWithCustomPlaceholder(t *testing.T) { + const ( + ExpectedURL = "http://example.com/service-name?event=MYTEST&description=alert-description" + ExpectedBody = "service-name,alert-description,MYTEST" + ) + customAlertProvider := &AlertProvider{ + URL: "http://example.com/[SERVICE_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]", + Body: "[SERVICE_NAME],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]", + Headers: nil, + Placeholders: map[string]map[string]string{ + "ALERT_TRIGGERED_OR_RESOLVED": { + "resolved": "MYTEST", + }, + }, + } + request := customAlertProvider.buildHTTPRequest("service-name", "alert-description", true) + if request.URL.String() != ExpectedURL { + t.Error("expected URL to be", ExpectedURL, "was", request.URL.String()) + } + body, _ := ioutil.ReadAll(request.Body) + if string(body) != ExpectedBody { + t.Error("expected body to be", ExpectedBody, "was", string(body)) + } +} diff --git a/config/config_test.go b/config/config_test.go index 5f5f295a..d948f800 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -344,6 +344,10 @@ alerting: access-key: "1" originator: "31619191918" recipients: "31619191919" + placeholders: + ALERT_TRIGGERED_OR_RESOLVED: + triggered: "partial_outage" + resolved: "operational" services: - name: twinnation url: https://twinnation.org/health From 739089551416bff9635b1adffa2a7df90ccb2f80 Mon Sep 17 00:00:00 2001 From: Robert Hoppe Date: Wed, 17 Feb 2021 13:27:11 +0100 Subject: [PATCH 2/4] Extend also tests on config --- config/config_test.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/config/config_test.go b/config/config_test.go index d948f800..538309fe 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -344,10 +344,11 @@ alerting: access-key: "1" originator: "31619191918" recipients: "31619191919" - placeholders: - ALERT_TRIGGERED_OR_RESOLVED: - triggered: "partial_outage" - resolved: "operational" + custom: + placeholders: + ALERT_TRIGGERED_OR_RESOLVED: + triggered: "partial_outage" + resolved: "operational" services: - name: twinnation url: https://twinnation.org/health @@ -400,6 +401,12 @@ services: if config.Alerting.Messagebird.Recipients != "31619191919" { t.Errorf("Messagebird to recipients should've been %s, but was %s", "31619191919", config.Alerting.Messagebird.Recipients) } + if config.Alerting.Custom == nil { + t.Fatal("config.Alerting. Custom shouldn't have been nil") + } + if config.Alerting.Custom.Placeholders == nil { + t.Fatal("config.Alerting.Custom.Placeholders shouldn't have been nil") + } if len(config.Services) != 1 { t.Error("There should've been 1 service") } From d7d904ae5fbeee881b28c1e5612e38c3bedfc943 Mon Sep 17 00:00:00 2001 From: Robert Hoppe Date: Wed, 17 Feb 2021 13:45:22 +0100 Subject: [PATCH 3/4] Bump up test coverage --- alerting/provider/custom/custom_test.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/alerting/provider/custom/custom_test.go b/alerting/provider/custom/custom_test.go index 8118fad8..5587255d 100644 --- a/alerting/provider/custom/custom_test.go +++ b/alerting/provider/custom/custom_test.go @@ -93,3 +93,24 @@ func TestAlertProvider_buildHTTPRequestWithCustomPlaceholder(t *testing.T) { t.Error("expected body to be", ExpectedBody, "was", string(body)) } } + +func TestAlertProvider_GetPlaceholderValue(t *testing.T) { + customAlertProvider := &AlertProvider{ + URL: "http://example.com/[SERVICE_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]", + Body: "[SERVICE_NAME],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]", + Headers: nil, + Placeholders: nil, + } + + if customAlertProvider.GetPlaceholderValue("I_DO_NOT_EXIST", "i_do_also_no_exist") != "" { + t.Error("expected empty response from a non existing placeholder") + } + + if customAlertProvider.GetPlaceholderValue("ALERT_TRIGGERED_OR_RESOLVED", "I_DO_NOT_EXIST") != "" { + t.Error("expected empty response from a non existing subvalue") + } + + if customAlertProvider.GetPlaceholderValue("ALERT_TRIGGERED_OR_RESOLVED", "triggered") != "TRIGGERED" { + t.Error("expected 'triggered' as a result") + } +} From 1d21f5889d40366176efe6c456ac503ed11973d8 Mon Sep 17 00:00:00 2001 From: Robert Hoppe Date: Thu, 18 Feb 2021 19:03:12 +0100 Subject: [PATCH 4/4] Move away from generic solution to a fixed one --- alerting/provider/custom/custom.go | 40 +++++++++++-------------- alerting/provider/custom/custom_test.go | 14 ++++----- 2 files changed, 23 insertions(+), 31 deletions(-) diff --git a/alerting/provider/custom/custom.go b/alerting/provider/custom/custom.go index 78c625e7..d2c8e768 100644 --- a/alerting/provider/custom/custom.go +++ b/alerting/provider/custom/custom.go @@ -24,13 +24,6 @@ type AlertProvider struct { Placeholders map[string]map[string]string `yaml:"placeholders,omitempty"` } -var DefaultPlaceholderValues = map[string]map[string]string{ - "ALERT_TRIGGERED_OR_RESOLVED": map[string]string{ - "triggered": "TRIGGERED", - "resolved": "RESOLVED", - }, -} - // IsValid returns whether the provider's configuration is valid func (provider *AlertProvider) IsValid() bool { return len(provider.URL) > 0 @@ -41,21 +34,24 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler return provider } -// GetPlaceholderValue returns the Placeholder value -func (provider *AlertProvider) GetPlaceholderValue(name, status string) string { - if _, ok := provider.Placeholders[name]; ok { - if val, ok := provider.Placeholders[name][status]; ok { +// GetPlaceholderValue returns the Placeholder value for ALERT_TRIGGERED_OR_RESOLVED if configured +func (provider *AlertProvider) GetAlertStatePlaceholderValue(resolved bool) string { + status := "triggered" + if resolved { + status = "resolved" + } + + if _, ok := provider.Placeholders["ALERT_TRIGGERED_OR_RESOLVED"]; ok { + if val, ok := provider.Placeholders["ALERT_TRIGGERED_OR_RESOLVED"][status]; ok { return val } - } else { - if _, ok := DefaultPlaceholderValues[name]; ok { - if val, ok := DefaultPlaceholderValues[name][status]; ok { - return val - } - } } - return "" + if resolved { + return "RESOLVED" + } + + return "TRIGGERED" } func (provider *AlertProvider) buildHTTPRequest(serviceName, alertDescription string, resolved bool) *http.Request { @@ -71,9 +67,9 @@ func (provider *AlertProvider) buildHTTPRequest(serviceName, alertDescription st } if strings.Contains(body, "[ALERT_TRIGGERED_OR_RESOLVED]") { if resolved { - body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetPlaceholderValue("ALERT_TRIGGERED_OR_RESOLVED", "resolved")) + body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true)) } else { - body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetPlaceholderValue("ALERT_TRIGGERED_OR_RESOLVED", "triggered")) + body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(false)) } } if strings.Contains(providerURL, "[ALERT_DESCRIPTION]") { @@ -84,9 +80,9 @@ func (provider *AlertProvider) buildHTTPRequest(serviceName, alertDescription st } if strings.Contains(providerURL, "[ALERT_TRIGGERED_OR_RESOLVED]") { if resolved { - providerURL = strings.ReplaceAll(providerURL, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetPlaceholderValue("ALERT_TRIGGERED_OR_RESOLVED", "resolved")) + providerURL = strings.ReplaceAll(providerURL, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true)) } else { - providerURL = strings.ReplaceAll(providerURL, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetPlaceholderValue("ALERT_TRIGGERED_OR_RESOLVED", "triggered")) + providerURL = strings.ReplaceAll(providerURL, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(false)) } } if len(method) == 0 { diff --git a/alerting/provider/custom/custom_test.go b/alerting/provider/custom/custom_test.go index 5587255d..4c09b282 100644 --- a/alerting/provider/custom/custom_test.go +++ b/alerting/provider/custom/custom_test.go @@ -94,7 +94,7 @@ func TestAlertProvider_buildHTTPRequestWithCustomPlaceholder(t *testing.T) { } } -func TestAlertProvider_GetPlaceholderValue(t *testing.T) { +func TestAlertProvider_GetAlertStatePlaceholderValueDefaults(t *testing.T) { customAlertProvider := &AlertProvider{ URL: "http://example.com/[SERVICE_NAME]?event=[ALERT_TRIGGERED_OR_RESOLVED]&description=[ALERT_DESCRIPTION]", Body: "[SERVICE_NAME],[ALERT_DESCRIPTION],[ALERT_TRIGGERED_OR_RESOLVED]", @@ -102,15 +102,11 @@ func TestAlertProvider_GetPlaceholderValue(t *testing.T) { Placeholders: nil, } - if customAlertProvider.GetPlaceholderValue("I_DO_NOT_EXIST", "i_do_also_no_exist") != "" { - t.Error("expected empty response from a non existing placeholder") + if customAlertProvider.GetAlertStatePlaceholderValue(true) != "RESOLVED" { + t.Error("expected here actually RESOLVED") } - if customAlertProvider.GetPlaceholderValue("ALERT_TRIGGERED_OR_RESOLVED", "I_DO_NOT_EXIST") != "" { - t.Error("expected empty response from a non existing subvalue") - } - - if customAlertProvider.GetPlaceholderValue("ALERT_TRIGGERED_OR_RESOLVED", "triggered") != "TRIGGERED" { - t.Error("expected 'triggered' as a result") + if customAlertProvider.GetAlertStatePlaceholderValue(false) != "TRIGGERED" { + t.Error("expected here actually TRIGGERED") } }