From a870d3e43f912f7bdc107b4f280163a0df2d59c1 Mon Sep 17 00:00:00 2001 From: cemturker Date: Mon, 23 Nov 2020 22:20:06 +0100 Subject: [PATCH] Add Messagebird as an alerting provider --- README.md | 35 +++++++++++- alerting/config.go | 6 ++- alerting/provider/custom/custom.go | 2 +- alerting/provider/mattermost/mattermost.go | 6 ++- alerting/provider/messagebird/messagebird.go | 47 ++++++++++++++++ .../provider/messagebird/messagebird_test.go | 53 +++++++++++++++++++ alerting/provider/pagerduty/pagerduty.go | 6 ++- alerting/provider/provider.go | 4 +- alerting/provider/slack/slack.go | 3 +- alerting/provider/twilio/twilio.go | 8 +-- config/config.go | 7 +++ config/config_test.go | 25 ++++++++- core/alert.go | 3 ++ 13 files changed, 190 insertions(+), 15 deletions(-) create mode 100644 alerting/provider/messagebird/messagebird.go create mode 100644 alerting/provider/messagebird/messagebird_test.go diff --git a/README.md b/README.md index f9ed7b01..8532e806 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ core applications: https://status.twinnation.org/ - [Configuring PagerDuty alerts](#configuring-pagerduty-alerts) - [Configuring Twilio alerts](#configuring-twilio-alerts) - [Configuring Mattermost alerts](#configuring-mattermost-alerts) + - [Configuring Messagebird alerts](#configuring-messagebird-alerts) - [Configuring custom alerts](#configuring-custom-alerts) - [Kubernetes (ALPHA)](#kubernetes-alpha) - [Auto Discovery](#auto-discovery) @@ -50,7 +51,7 @@ The main features of Gatus are: - **Highly flexible health check conditions**: While checking the response status may be enough for some use cases, Gatus goes much further and allows you to add conditions on the response time, the response body and even the IP address. - **Ability to use Gatus for user acceptance tests**: Thanks to the point above, you can leverage this application to create automated user acceptance tests. - **Very easy to configure**: Not only is the configuration designed to be as readable as possible, it's also extremely easy to add a new service or a new endpoint to monitor. -- **Alerting**: While having a pretty visual dashboard is useful to keep track of the state of your application(s), you probably don't want to stare at it all day. Thus, notifications via Slack, Mattermost, PagerDuty and Twilio are supported out of the box with the ability to configure a custom alerting provider for any needs you might have, whether it be a different provider or a custom application that manages automated rollbacks. +- **Alerting**: While having a pretty visual dashboard is useful to keep track of the state of your application(s), you probably don't want to stare at it all day. Thus, notifications via Slack, Mattermost, Messagebird, PagerDuty and Twilio are supported out of the box with the ability to configure a custom alerting provider for any needs you might have, whether it be a different provider or a custom application that manages automated rollbacks. - **Metrics** - **Low resource consumption**: As with most Go applications, the resource footprint that this application requires is negligibly small. - **Service auto discovery in Kubernetes** (ALPHA) @@ -107,7 +108,7 @@ Note that you can also add environment variables in the configuration file (i.e. | `services[].dns` | Configuration for a service of type DNS. See [Monitoring using DNS queries](#monitoring-using-dns-queries) | `""` | | `services[].dns.query-type` | Query type for DNS service | `""` | | `services[].dns.query-name` | Query name for DNS service | `""` | -| `services[].alerts[].type` | Type of alert. Valid types: `slack`, `pagerduty`, `twilio`, `mattermost`, `custom` | Required `""` | +| `services[].alerts[].type` | Type of alert. Valid types: `slack`, `pagerduty`, `twilio`, `mattermost`, `messagebird`, `custom` | Required `""` | | `services[].alerts[].enabled` | Whether to enable the alert | `false` | | `services[].alerts[].failure-threshold` | Number of failures in a row needed before triggering the alert | `3` | | `services[].alerts[].success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved | `2` | @@ -126,6 +127,10 @@ Note that you can also add environment variables in the configuration file (i.e. | `alerting.mattermost` | Configuration for alerts of type `mattermost` | `{}` | | `alerting.mattermost.webhook-url` | Mattermost Webhook URL | Required `""` | | `alerting.mattermost.insecure` | Whether to skip verifying the server's certificate chain and host name | `false` | +| `alerting.messagebird` | Settings for alerts of type `messagebird` | `{}` | +| `alerting.messagebird.access-key` | Messagebird access key | Required `""` | +| `alerting.messagebird.originator` | The sender of the message | Required `""` | +| `alerting.messagebird.recipients` | The recipients of the message | Required `""` | | `alerting.custom` | Configuration for custom actions on failure or alerts | `{}` | | `alerting.custom.url` | Custom alerting request url | Required `""` | | `alerting.custom.method` | Request method | `GET` | @@ -306,6 +311,32 @@ Here's an example of what the notifications look like: ![Mattermost notifications](.github/assets/mattermost-alerts.png) +#### Configuring Messagebird alerts + +Example of sending **sms** message alert by using Messagebird + +```yaml +alerting: + messagebird: + access-key: "..." + originator: "31619191918" + recipients: "31619191919" +services: + - name: twinnation + interval: 30s + url: "https://twinnation.org/health" + alerts: + - type: messagebird + enabled: true + failure-threshold: 3 + send-on-resolved: true + description: "healthcheck failed 3 times in a row" + conditions: + - "[STATUS] == 200" + - "[BODY].status == UP" + - "[RESPONSE_TIME] < 300" +``` + #### Configuring custom alerts While they're called alerts, you can use this feature to call anything. diff --git a/alerting/config.go b/alerting/config.go index 8ebda379..599bd94b 100644 --- a/alerting/config.go +++ b/alerting/config.go @@ -2,9 +2,10 @@ package alerting import ( "github.com/TwinProduction/gatus/alerting/provider/custom" + "github.com/TwinProduction/gatus/alerting/provider/mattermost" + "github.com/TwinProduction/gatus/alerting/provider/messagebird" "github.com/TwinProduction/gatus/alerting/provider/pagerduty" "github.com/TwinProduction/gatus/alerting/provider/slack" - "github.com/TwinProduction/gatus/alerting/provider/mattermost" "github.com/TwinProduction/gatus/alerting/provider/twilio" ) @@ -16,6 +17,9 @@ type Config struct { // Mattermost is the configuration for the mattermost alerting provider Mattermost *mattermost.AlertProvider `yaml:"mattermost"` + // Messagebird is the configuration for the messagebird alerting provider + Messagebird *messagebird.AlertProvider `yaml:"messagebird"` + // Pagerduty is the configuration for the pagerduty alerting provider PagerDuty *pagerduty.AlertProvider `yaml:"pagerduty"` diff --git a/alerting/provider/custom/custom.go b/alerting/provider/custom/custom.go index a2980b31..65b49260 100644 --- a/alerting/provider/custom/custom.go +++ b/alerting/provider/custom/custom.go @@ -62,7 +62,7 @@ func (provider *AlertProvider) buildRequest(serviceName, alertDescription string } } if len(method) == 0 { - method = "GET" + method = http.MethodGet } bodyBuffer := bytes.NewBuffer([]byte(body)) request, _ := http.NewRequest(method, providerURL, bodyBuffer) diff --git a/alerting/provider/mattermost/mattermost.go b/alerting/provider/mattermost/mattermost.go index 49db2f82..7f4f470c 100644 --- a/alerting/provider/mattermost/mattermost.go +++ b/alerting/provider/mattermost/mattermost.go @@ -2,6 +2,8 @@ package mattermost import ( "fmt" + "net/http" + "github.com/TwinProduction/gatus/alerting/provider/custom" "github.com/TwinProduction/gatus/core" ) @@ -39,8 +41,8 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler results += fmt.Sprintf("%s - `%s`\n", prefix, conditionResult.Condition) } return &custom.AlertProvider{ - URL: provider.WebhookURL, - Method: "POST", + URL: provider.WebhookURL, + Method: http.MethodPost, Insecure: provider.Insecure, Body: fmt.Sprintf(`{ "text": "", diff --git a/alerting/provider/messagebird/messagebird.go b/alerting/provider/messagebird/messagebird.go new file mode 100644 index 00000000..1f243fcc --- /dev/null +++ b/alerting/provider/messagebird/messagebird.go @@ -0,0 +1,47 @@ +package messagebird + +import ( + "fmt" + "net/http" + + "github.com/TwinProduction/gatus/alerting/provider/custom" + "github.com/TwinProduction/gatus/core" +) + +const ( + restAPIURL = "https://rest.messagebird.com/messages" +) + +// AlertProvider is the configuration necessary for sending an alert using Messagebird +type AlertProvider struct { + AccessKey string `yaml:"access-key"` + Originator string `yaml:"originator"` + Recipients string `yaml:"recipients"` +} + +// IsValid returns whether the provider's configuration is valid +func (provider *AlertProvider) IsValid() bool { + return len(provider.AccessKey) > 0 && len(provider.Originator) > 0 && len(provider.Recipients) > 0 +} + +// ToCustomAlertProvider converts the provider into a custom.AlertProvider +// Reference doc for messagebird https://developers.messagebird.com/api/sms-messaging/#send-outbound-sms +func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, alert *core.Alert, _ *core.Result, resolved bool) *custom.AlertProvider { + var message string + if resolved { + message = fmt.Sprintf("RESOLVED: %s - %s", service.Name, alert.Description) + } else { + message = fmt.Sprintf("TRIGGERED: %s - %s", service.Name, alert.Description) + } + + return &custom.AlertProvider{ + URL: restAPIURL, + Method: http.MethodPost, + Body: fmt.Sprintf(`{ "originator": "%s", "recipients": "%s", "body": "%s"}`, + provider.Originator, provider.Recipients, message), + Headers: map[string]string{ + "Content-Type": "application/json", + "Authorization": fmt.Sprintf("AccessKey %s", provider.AccessKey), + }, + } +} diff --git a/alerting/provider/messagebird/messagebird_test.go b/alerting/provider/messagebird/messagebird_test.go new file mode 100644 index 00000000..9a9d4b10 --- /dev/null +++ b/alerting/provider/messagebird/messagebird_test.go @@ -0,0 +1,53 @@ +package messagebird + +import ( + "strings" + "testing" + + "github.com/TwinProduction/gatus/core" +) + +func TestMessagebirdAlertProvider_IsValid(t *testing.T) { + invalidProvider := AlertProvider{} + if invalidProvider.IsValid() { + t.Error("provider shouldn't have been valid") + } + validProvider := AlertProvider{ + AccessKey: "1", + Originator: "1", + Recipients: "1", + } + if !validProvider.IsValid() { + t.Error("provider should've been valid") + } +} + +func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) { + provider := AlertProvider{ + AccessKey: "1", + Originator: "1", + Recipients: "1", + } + customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &core.Alert{}, &core.Result{}, true) + if customAlertProvider == nil { + t.Error("customAlertProvider shouldn't have been nil") + } + if !strings.Contains(customAlertProvider.Body, "RESOLVED") { + t.Error("customAlertProvider.Body should've contained the substring RESOLVED") + } +} + +func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) { + provider := AlertProvider{ + AccessKey: "1", + Originator: "1", + Recipients: "1", + } + customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &core.Alert{}, &core.Result{}, false) + if customAlertProvider == nil { + t.Error("customAlertProvider shouldn't have been nil") + } + if !strings.Contains(customAlertProvider.Body, "TRIGGERED") { + t.Error("customAlertProvider.Body should've contained the substring TRIGGERED") + } +} diff --git a/alerting/provider/pagerduty/pagerduty.go b/alerting/provider/pagerduty/pagerduty.go index 50abc31c..a835bba5 100644 --- a/alerting/provider/pagerduty/pagerduty.go +++ b/alerting/provider/pagerduty/pagerduty.go @@ -2,6 +2,8 @@ package pagerduty import ( "fmt" + "net/http" + "github.com/TwinProduction/gatus/alerting/provider/custom" "github.com/TwinProduction/gatus/core" ) @@ -19,7 +21,7 @@ func (provider *AlertProvider) IsValid() bool { // ToCustomAlertProvider converts the provider into a custom.AlertProvider // // relevant: https://developer.pagerduty.com/docs/events-api-v2/trigger-events/ -func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, alert *core.Alert, result *core.Result, resolved bool) *custom.AlertProvider { +func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, alert *core.Alert, _ *core.Result, resolved bool) *custom.AlertProvider { var message, eventAction, resolveKey string if resolved { message = fmt.Sprintf("RESOLVED: %s - %s", service.Name, alert.Description) @@ -32,7 +34,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler } return &custom.AlertProvider{ URL: "https://events.pagerduty.com/v2/enqueue", - Method: "POST", + Method: http.MethodPost, Body: fmt.Sprintf(`{ "routing_key": "%s", "dedup_key": "%s", diff --git a/alerting/provider/provider.go b/alerting/provider/provider.go index 68d4596d..74a25eaf 100644 --- a/alerting/provider/provider.go +++ b/alerting/provider/provider.go @@ -2,9 +2,10 @@ package provider import ( "github.com/TwinProduction/gatus/alerting/provider/custom" + "github.com/TwinProduction/gatus/alerting/provider/mattermost" + "github.com/TwinProduction/gatus/alerting/provider/messagebird" "github.com/TwinProduction/gatus/alerting/provider/pagerduty" "github.com/TwinProduction/gatus/alerting/provider/slack" - "github.com/TwinProduction/gatus/alerting/provider/mattermost" "github.com/TwinProduction/gatus/alerting/provider/twilio" "github.com/TwinProduction/gatus/core" ) @@ -24,5 +25,6 @@ var ( _ AlertProvider = (*twilio.AlertProvider)(nil) _ AlertProvider = (*slack.AlertProvider)(nil) _ AlertProvider = (*mattermost.AlertProvider)(nil) + _ AlertProvider = (*messagebird.AlertProvider)(nil) _ AlertProvider = (*pagerduty.AlertProvider)(nil) ) diff --git a/alerting/provider/slack/slack.go b/alerting/provider/slack/slack.go index 2c613a58..d7ca66fb 100644 --- a/alerting/provider/slack/slack.go +++ b/alerting/provider/slack/slack.go @@ -2,6 +2,7 @@ package slack import ( "fmt" + "net/http" "github.com/TwinProduction/gatus/alerting/provider/custom" "github.com/TwinProduction/gatus/core" @@ -38,7 +39,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler } return &custom.AlertProvider{ URL: provider.WebhookURL, - Method: "POST", + Method: http.MethodPost, Body: fmt.Sprintf(`{ "text": "", "attachments": [ diff --git a/alerting/provider/twilio/twilio.go b/alerting/provider/twilio/twilio.go index 94772739..d1e9fc1b 100644 --- a/alerting/provider/twilio/twilio.go +++ b/alerting/provider/twilio/twilio.go @@ -3,9 +3,11 @@ package twilio import ( "encoding/base64" "fmt" + "net/http" + "net/url" + "github.com/TwinProduction/gatus/alerting/provider/custom" "github.com/TwinProduction/gatus/core" - "net/url" ) // AlertProvider is the configuration necessary for sending an alert using Twilio @@ -22,7 +24,7 @@ func (provider *AlertProvider) IsValid() bool { } // ToCustomAlertProvider converts the provider into a custom.AlertProvider -func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, alert *core.Alert, result *core.Result, resolved bool) *custom.AlertProvider { +func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, alert *core.Alert, _ *core.Result, resolved bool) *custom.AlertProvider { var message string if resolved { message = fmt.Sprintf("RESOLVED: %s - %s", service.Name, alert.Description) @@ -31,7 +33,7 @@ func (provider *AlertProvider) ToCustomAlertProvider(service *core.Service, aler } return &custom.AlertProvider{ URL: fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json", provider.SID), - Method: "POST", + Method: http.MethodPost, Body: url.Values{ "To": {provider.To}, "From": {provider.From}, diff --git a/config/config.go b/config/config.go index a9c044aa..33831891 100644 --- a/config/config.go +++ b/config/config.go @@ -202,6 +202,7 @@ func validateAlertingConfig(config *Config) { alertTypes := []core.AlertType{ core.SlackAlert, core.MattermostAlert, + core.MessagebirdAlert, core.TwilioAlert, core.PagerDutyAlert, core.CustomAlert, @@ -238,6 +239,12 @@ func GetAlertingProviderByAlertType(config *Config, alertType core.AlertType) pr return nil } return config.Alerting.Mattermost + case core.MessagebirdAlert: + if config.Alerting.Messagebird == nil { + // Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil + return nil + } + return config.Alerting.Messagebird case core.TwilioAlert: if config.Alerting.Twilio == nil { // Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil diff --git a/config/config_test.go b/config/config_test.go index c0a8fe92..04c13065 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -366,6 +366,10 @@ alerting: webhook-url: "http://example.com" pagerduty: integration-key: "00000000000000000000000000000000" + messagebird: + token: "1" + originator: "31619191918" + recipients: "31619191919" services: - name: twinnation url: https://twinnation.org/health @@ -377,6 +381,8 @@ services: failure-threshold: 7 success-threshold: 5 description: "Healthcheck failed 7 times in a row" + - type: messagebird + enabled: true conditions: - "[STATUS] == 200" `)) @@ -401,9 +407,21 @@ services: if config.Alerting.PagerDuty == nil || !config.Alerting.PagerDuty.IsValid() { t.Fatal("PagerDuty alerting config should've been valid") } + if config.Alerting.Messagebird == nil || !config.Alerting.Messagebird.IsValid() { + t.Fatal("Messagebird alerting config should've been valid") + } if config.Alerting.PagerDuty.IntegrationKey != "00000000000000000000000000000000" { t.Errorf("PagerDuty integration key should've been %s, but was %s", "00000000000000000000000000000000", config.Alerting.PagerDuty.IntegrationKey) } + if config.Alerting.Messagebird.AccessKey != "1" { + t.Errorf("Messagebird access key should've been %s, but was %s", "1", config.Alerting.Messagebird.AccessKey) + } + if config.Alerting.Messagebird.Originator != "31619191918" { + t.Errorf("Messagebird originator should've been %s, but was %s", "31619191918", config.Alerting.Messagebird.Originator) + } + if config.Alerting.Messagebird.Recipients != "31619191919" { + t.Errorf("Messagebird recipients should've been %s, but was %s", "31619191919", config.Alerting.Messagebird.Recipients) + } if len(config.Services) != 1 { t.Error("There should've been 1 service") } @@ -416,8 +434,8 @@ services: if config.Services[0].Alerts == nil { t.Fatal("The service alerts shouldn't have been nil") } - if len(config.Services[0].Alerts) != 2 { - t.Fatal("There should've been 2 alert configured") + if len(config.Services[0].Alerts) != 3 { + t.Fatal("There should've been 3 alert configured") } if !config.Services[0].Alerts[0].Enabled { t.Error("The alert should've been enabled") @@ -443,6 +461,9 @@ services: if config.Services[0].Alerts[1].Description != "Healthcheck failed 7 times in a row" { t.Errorf("The description of the alert should've been %s, but it was %s", "Healthcheck failed 7 times in a row", config.Services[0].Alerts[0].Description) } + if config.Services[0].Alerts[2].Type != core.MessagebirdAlert { + t.Errorf("The type of the alert should've been %s, but it was %s", core.MessagebirdAlert, config.Services[0].Alerts[1].Type) + } } func TestParseAndValidateConfigBytesWithInvalidPagerDutyAlertingConfig(t *testing.T) { diff --git a/core/alert.go b/core/alert.go index 2ba882fa..d28d8ad8 100644 --- a/core/alert.go +++ b/core/alert.go @@ -40,6 +40,9 @@ const ( // MattermostAlert is the AlertType for the mattermost alerting provider MattermostAlert AlertType = "mattermost" + // MessagebirdAlert is the AlertType for the messagebird alerting provider + MessagebirdAlert AlertType = "messagebird" + // PagerDutyAlert is the AlertType for the pagerduty alerting provider PagerDutyAlert AlertType = "pagerduty"