From 8fe9d013b5df1e43ee6d1a54e1de4e3bebb626d7 Mon Sep 17 00:00:00 2001 From: TwinProduction Date: Thu, 4 Mar 2021 21:26:17 -0500 Subject: [PATCH] Close #48: Implement Discord alerting providers --- README.md | 42 ++++++++-- alerting/config.go | 16 ++-- alerting/provider/discord/discord.go | 63 +++++++++++++++ alerting/provider/discord/discord_test.go | 65 +++++++++++++++ alerting/provider/provider.go | 6 +- config/config.go | 37 +++++---- config/config_test.go | 99 +++++++++++++++++++---- core/alert.go | 13 +-- 8 files changed, 293 insertions(+), 48 deletions(-) create mode 100644 alerting/provider/discord/discord.go create mode 100644 alerting/provider/discord/discord_test.go diff --git a/README.md b/README.md index 1cc6b715..b8b7f7e0 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ core applications: https://status.twinnation.org/ - [Functions](#functions) - [Alerting](#alerting) - [Configuring Slack alerts](#configuring-slack-alerts) + - [Configuring Discord alerts](#configuring-discord-alerts) - [Configuring PagerDuty alerts](#configuring-pagerduty-alerts) - [Configuring Twilio alerts](#configuring-twilio-alerts) - [Configuring Mattermost alerts](#configuring-mattermost-alerts) @@ -118,7 +119,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`, `messagebird`, `custom` | Required `""` | +| `services[].alerts[].type` | Type of alert. Valid types: `slack`, `discord`m `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` | @@ -127,6 +128,8 @@ Note that you can also add environment variables in the configuration file (i.e. | `alerting` | Configuration for alerting | `{}` | | `alerting.slack` | Configuration for alerts of type `slack` | `{}` | | `alerting.slack.webhook-url` | Slack Webhook URL | Required `""` | +| `alerting.discord` | Configuration for alerts of type `discord` | `{}` | +| `alerting.discord.webhook-url` | Discord Webhook URL | Required `""` | | `alerting.pagerduty` | Configuration for alerts of type `pagerduty` | `{}` | | `alerting.pagerduty.integration-key` | PagerDuty Events API v2 integration key. | Required `""` | | `alerting.twilio` | Settings for alerts of type `twilio` | `{}` | @@ -223,6 +226,7 @@ ignored. alerting: slack: webhook-url: "https://hooks.slack.com/services/**********/**********/**********" + services: - name: twinnation url: "https://twinnation.org/health" @@ -248,6 +252,29 @@ Here's an example of what the notifications look like: ![Slack notifications](.github/assets/slack-alerts.png) +#### Configuring Discord alerts + +```yaml +alerting: + discord: + webhook-url: "https://discord.com/api/webhooks/**********/**********" + +services: + - name: twinnation + url: "https://twinnation.org/health" + interval: 30s + alerts: + - type: discord + enabled: true + description: "healthcheck failed" + send-on-resolved: true + conditions: + - "[STATUS] == 200" + - "[BODY].status == UP" + - "[RESPONSE_TIME] < 300" +``` + + #### Configuring PagerDuty alerts It is highly recommended to set `services[].alerts[].send-on-resolved` to `true` for alerts @@ -259,6 +286,7 @@ PagerDuty instead. alerting: pagerduty: integration-key: "********************************" + services: - name: twinnation url: "https://twinnation.org/health" @@ -269,7 +297,7 @@ services: failure-threshold: 3 success-threshold: 5 send-on-resolved: true - description: "healthcheck failed 3 times in a row" + description: "healthcheck failed" conditions: - "[STATUS] == 200" - "[BODY].status == UP" @@ -286,6 +314,7 @@ alerting: token: "..." from: "+1-234-567-8901" to: "+1-234-567-8901" + services: - name: twinnation interval: 30s @@ -295,7 +324,7 @@ services: enabled: true failure-threshold: 5 send-on-resolved: true - description: "healthcheck failed 5 times in a row" + description: "healthcheck failed" conditions: - "[STATUS] == 200" - "[BODY].status == UP" @@ -310,6 +339,7 @@ alerting: mattermost: webhook-url: "http://**********/hooks/**********" insecure: true + services: - name: twinnation url: "https://twinnation.org/health" @@ -317,7 +347,7 @@ services: alerts: - type: mattermost enabled: true - description: "healthcheck failed 3 times in a row" + description: "healthcheck failed" send-on-resolved: true conditions: - "[STATUS] == 200" @@ -349,7 +379,7 @@ services: enabled: true failure-threshold: 3 send-on-resolved: true - description: "healthcheck failed 3 times in a row" + description: "healthcheck failed" conditions: - "[STATUS] == 200" - "[BODY].status == UP" @@ -395,7 +425,7 @@ services: failure-threshold: 10 success-threshold: 3 send-on-resolved: true - description: "healthcheck failed 10 times in a row" + description: "healthcheck failed" conditions: - "[STATUS] == 200" - "[BODY].status == UP" diff --git a/alerting/config.go b/alerting/config.go index 599bd94b..c99456ab 100644 --- a/alerting/config.go +++ b/alerting/config.go @@ -2,6 +2,7 @@ package alerting import ( "github.com/TwinProduction/gatus/alerting/provider/custom" + "github.com/TwinProduction/gatus/alerting/provider/discord" "github.com/TwinProduction/gatus/alerting/provider/mattermost" "github.com/TwinProduction/gatus/alerting/provider/messagebird" "github.com/TwinProduction/gatus/alerting/provider/pagerduty" @@ -11,8 +12,11 @@ import ( // Config is the configuration for alerting providers type Config struct { - // Slack is the configuration for the slack alerting provider - Slack *slack.AlertProvider `yaml:"slack"` + // Custom is the configuration for the custom alerting provider + Custom *custom.AlertProvider `yaml:"custom"` + + // Discord is the configuration for the discord alerting provider + Discord *discord.AlertProvider `yaml:"discord"` // Mattermost is the configuration for the mattermost alerting provider Mattermost *mattermost.AlertProvider `yaml:"mattermost"` @@ -20,12 +24,12 @@ type Config struct { // Messagebird is the configuration for the messagebird alerting provider Messagebird *messagebird.AlertProvider `yaml:"messagebird"` - // Pagerduty is the configuration for the pagerduty alerting provider + // PagerDuty is the configuration for the pagerduty alerting provider PagerDuty *pagerduty.AlertProvider `yaml:"pagerduty"` + // Slack is the configuration for the slack alerting provider + Slack *slack.AlertProvider `yaml:"slack"` + // Twilio is the configuration for the twilio alerting provider Twilio *twilio.AlertProvider `yaml:"twilio"` - - // Custom is the configuration for the custom alerting provider - Custom *custom.AlertProvider `yaml:"custom"` } diff --git a/alerting/provider/discord/discord.go b/alerting/provider/discord/discord.go new file mode 100644 index 00000000..060e7c69 --- /dev/null +++ b/alerting/provider/discord/discord.go @@ -0,0 +1,63 @@ +package discord + +import ( + "fmt" + "net/http" + + "github.com/TwinProduction/gatus/alerting/provider/custom" + "github.com/TwinProduction/gatus/core" +) + +// AlertProvider is the configuration necessary for sending an alert using Discord +type AlertProvider struct { + WebhookURL string `yaml:"webhook-url"` +} + +// IsValid returns whether the provider's configuration is valid +func (provider *AlertProvider) IsValid() bool { + return len(provider.WebhookURL) > 0 +} + +// 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 { + var message, results string + var colorCode int + if resolved { + message = fmt.Sprintf("An alert for **%s** has been resolved after passing successfully %d time(s) in a row", service.Name, alert.SuccessThreshold) + colorCode = 3066993 + } else { + message = fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", service.Name, alert.FailureThreshold) + colorCode = 15158332 + } + for _, conditionResult := range result.ConditionResults { + var prefix string + if conditionResult.Success { + prefix = ":white_check_mark:" + } else { + prefix = ":x:" + } + results += fmt.Sprintf("%s - `%s`\\n", prefix, conditionResult.Condition) + } + return &custom.AlertProvider{ + URL: provider.WebhookURL, + Method: http.MethodPost, + Body: fmt.Sprintf(`{ + "content": "", + "embeds": [ + { + "title": ":helmet_with_white_cross: Gatus", + "description": "%s:\n> %s", + "color": %d, + "fields": [ + { + "name": "Condition results", + "value": "%s", + "inline": false + } + ] + } + ] +}`, message, alert.Description, colorCode, results), + Headers: map[string]string{"Content-Type": "application/json"}, + } +} diff --git a/alerting/provider/discord/discord_test.go b/alerting/provider/discord/discord_test.go new file mode 100644 index 00000000..84abd070 --- /dev/null +++ b/alerting/provider/discord/discord_test.go @@ -0,0 +1,65 @@ +package discord + +import ( + "encoding/json" + "net/http" + "strings" + "testing" + + "github.com/TwinProduction/gatus/core" +) + +func TestAlertProvider_IsValid(t *testing.T) { + invalidProvider := AlertProvider{WebhookURL: ""} + if invalidProvider.IsValid() { + t.Error("provider shouldn't have been valid") + } + validProvider := AlertProvider{WebhookURL: "http://example.com"} + if !validProvider.IsValid() { + t.Error("provider should've been valid") + } +} + +func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) { + provider := AlertProvider{WebhookURL: "http://example.com"} + customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &core.Alert{}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "SUCCESSFUL_CONDITION", Success: true}}}, true) + if customAlertProvider == nil { + t.Fatal("customAlertProvider shouldn't have been nil") + } + if !strings.Contains(customAlertProvider.Body, "resolved") { + t.Error("customAlertProvider.Body should've contained the substring resolved") + } + if customAlertProvider.URL != "http://example.com" { + t.Errorf("expected URL to be %s, got %s", "http://example.com", customAlertProvider.URL) + } + if customAlertProvider.Method != http.MethodPost { + t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method) + } + body := make(map[string]interface{}) + err := json.Unmarshal([]byte(customAlertProvider.Body), &body) + if err != nil { + t.Error("expected body to be valid JSON, got error:", err.Error()) + } +} + +func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) { + provider := AlertProvider{WebhookURL: "http://example.com"} + customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &core.Alert{}, &core.Result{ConditionResults: []*core.ConditionResult{{Condition: "UNSUCCESSFUL_CONDITION", Success: false}}}, false) + if customAlertProvider == nil { + t.Fatal("customAlertProvider shouldn't have been nil") + } + if !strings.Contains(customAlertProvider.Body, "triggered") { + t.Error("customAlertProvider.Body should've contained the substring triggered") + } + if customAlertProvider.URL != "http://example.com" { + t.Errorf("expected URL to be %s, got %s", "http://example.com", customAlertProvider.URL) + } + if customAlertProvider.Method != http.MethodPost { + t.Errorf("expected method to be %s, got %s", http.MethodPost, customAlertProvider.Method) + } + body := make(map[string]interface{}) + err := json.Unmarshal([]byte(customAlertProvider.Body), &body) + if err != nil { + t.Error("expected body to be valid JSON, got error:", err.Error()) + } +} diff --git a/alerting/provider/provider.go b/alerting/provider/provider.go index 74a25eaf..eeba3523 100644 --- a/alerting/provider/provider.go +++ b/alerting/provider/provider.go @@ -2,6 +2,7 @@ package provider import ( "github.com/TwinProduction/gatus/alerting/provider/custom" + "github.com/TwinProduction/gatus/alerting/provider/discord" "github.com/TwinProduction/gatus/alerting/provider/mattermost" "github.com/TwinProduction/gatus/alerting/provider/messagebird" "github.com/TwinProduction/gatus/alerting/provider/pagerduty" @@ -22,9 +23,10 @@ type AlertProvider interface { var ( // Validate interface implementation on compile _ AlertProvider = (*custom.AlertProvider)(nil) - _ AlertProvider = (*twilio.AlertProvider)(nil) - _ AlertProvider = (*slack.AlertProvider)(nil) + _ AlertProvider = (*discord.AlertProvider)(nil) _ AlertProvider = (*mattermost.AlertProvider)(nil) _ AlertProvider = (*messagebird.AlertProvider)(nil) _ AlertProvider = (*pagerduty.AlertProvider)(nil) + _ AlertProvider = (*slack.AlertProvider)(nil) + _ AlertProvider = (*twilio.AlertProvider)(nil) ) diff --git a/config/config.go b/config/config.go index e5174bf9..51036194 100644 --- a/config/config.go +++ b/config/config.go @@ -228,12 +228,13 @@ func validateAlertingConfig(config *Config) { return } alertTypes := []core.AlertType{ - core.SlackAlert, + core.CustomAlert, + core.DiscordAlert, core.MattermostAlert, core.MessagebirdAlert, - core.TwilioAlert, core.PagerDutyAlert, - core.CustomAlert, + core.SlackAlert, + core.TwilioAlert, } var validProviders, invalidProviders []core.AlertType for _, alertType := range alertTypes { @@ -255,12 +256,18 @@ func validateAlertingConfig(config *Config) { // GetAlertingProviderByAlertType returns an provider.AlertProvider by its corresponding core.AlertType func GetAlertingProviderByAlertType(config *Config, alertType core.AlertType) provider.AlertProvider { switch alertType { - case core.SlackAlert: - if config.Alerting.Slack == nil { + case core.CustomAlert: + if config.Alerting.Custom == 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.Slack + return config.Alerting.Custom + case core.DiscordAlert: + if config.Alerting.Discord == 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.Discord case core.MattermostAlert: if config.Alerting.Mattermost == nil { // Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil @@ -273,24 +280,24 @@ func GetAlertingProviderByAlertType(config *Config, alertType core.AlertType) pr 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 - return nil - } - return config.Alerting.Twilio case core.PagerDutyAlert: if config.Alerting.PagerDuty == 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.PagerDuty - case core.CustomAlert: - if config.Alerting.Custom == nil { + case core.SlackAlert: + if config.Alerting.Slack == 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.Custom + return config.Alerting.Slack + 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 + return nil + } + return config.Alerting.Twilio } return nil } diff --git a/config/config_test.go b/config/config_test.go index a53fd44d..1ee72654 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -6,6 +6,14 @@ import ( "testing" "time" + "github.com/TwinProduction/gatus/alerting" + "github.com/TwinProduction/gatus/alerting/provider/custom" + "github.com/TwinProduction/gatus/alerting/provider/discord" + "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/twilio" "github.com/TwinProduction/gatus/core" "github.com/TwinProduction/gatus/k8stest" v1 "k8s.io/api/core/v1" @@ -338,6 +346,8 @@ debug: true alerting: slack: webhook-url: "http://example.com" + discord: + webhook-url: "http://example.org" pagerduty: integration-key: "00000000000000000000000000000000" messagebird: @@ -356,7 +366,9 @@ services: success-threshold: 5 description: "Healthcheck failed 7 times in a row" - type: messagebird + - type: discord enabled: true + failure-threshold: 10 conditions: - "[STATUS] == 200" `)) @@ -369,6 +381,7 @@ services: if config.Metrics { t.Error("Metrics should've been false by default") } + // Alerting providers if config.Alerting == nil { t.Fatal("config.Alerting shouldn't have been nil") } @@ -396,6 +409,16 @@ 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.Discord == nil || !config.Alerting.Discord.IsValid() { + t.Fatal("Discord alerting config should've been valid") + } + if config.Alerting.Discord.WebhookURL != "http://example.org" { + t.Errorf("Discord webhook should've been %s, but was %s", "http://example.org", config.Alerting.Discord.WebhookURL) + } + if GetAlertingProviderByAlertType(config, core.DiscordAlert) != config.Alerting.Discord { + t.Error("expected discord configuration") + } + // Services if len(config.Services) != 1 { t.Error("There should've been 1 service") } @@ -405,11 +428,12 @@ services: if config.Services[0].Interval != 60*time.Second { t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second) } - if config.Services[0].Alerts == nil { - t.Fatal("The service alerts shouldn't have been nil") + if len(config.Services[0].Alerts) != 4 { + t.Fatal("There should've been 4 alerts configured") } - if len(config.Services[0].Alerts) != 3 { - t.Fatal("There should've been 3 alert configured") + + if config.Services[0].Alerts[0].Type != core.SlackAlert { + t.Errorf("The type of the alert should've been %s, but it was %s", core.SlackAlert, config.Services[0].Alerts[0].Type) } if !config.Services[0].Alerts[0].Enabled { t.Error("The alert should've been enabled") @@ -420,23 +444,35 @@ services: if config.Services[0].Alerts[0].SuccessThreshold != 2 { t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Services[0].Alerts[0].SuccessThreshold) } + + if config.Services[0].Alerts[1].Type != core.PagerDutyAlert { + t.Errorf("The type of the alert should've been %s, but it was %s", core.PagerDutyAlert, config.Services[0].Alerts[1].Type) + } + 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[1].Description) + } if config.Services[0].Alerts[1].FailureThreshold != 7 { t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 7, config.Services[0].Alerts[1].FailureThreshold) } if config.Services[0].Alerts[1].SuccessThreshold != 5 { t.Errorf("The success threshold of the alert should've been %d, but it was %d", 5, config.Services[0].Alerts[1].SuccessThreshold) } - if config.Services[0].Alerts[0].Type != core.SlackAlert { - t.Errorf("The type of the alert should've been %s, but it was %s", core.SlackAlert, config.Services[0].Alerts[0].Type) - } - if config.Services[0].Alerts[1].Type != core.PagerDutyAlert { - t.Errorf("The type of the alert should've been %s, but it was %s", core.PagerDutyAlert, config.Services[0].Alerts[1].Type) - } - 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) + t.Errorf("The type of the alert should've been %s, but it was %s", core.MessagebirdAlert, config.Services[0].Alerts[2].Type) + } + if config.Services[0].Alerts[2].Enabled { + t.Error("The alert should've been disabled") + } + + if config.Services[0].Alerts[3].Type != core.DiscordAlert { + t.Errorf("The type of the alert should've been %s, but it was %s", core.DiscordAlert, config.Services[0].Alerts[3].Type) + } + if config.Services[0].Alerts[3].FailureThreshold != 10 { + t.Errorf("The failure threshold of the alert should've been %d, but it was %d", 10, config.Services[0].Alerts[3].FailureThreshold) + } + if config.Services[0].Alerts[3].SuccessThreshold != 2 { + t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Services[0].Alerts[3].SuccessThreshold) } } @@ -809,3 +845,38 @@ kubernetes: // TODO: find a way to test this? t.Error("Function should've panicked because testing with ClusterModeIn isn't supported") } + +func TestGetAlertingProviderByAlertType(t *testing.T) { + cfg := &Config{ + Alerting: &alerting.Config{ + Custom: &custom.AlertProvider{}, + Discord: &discord.AlertProvider{}, + Mattermost: &mattermost.AlertProvider{}, + Messagebird: &messagebird.AlertProvider{}, + PagerDuty: &pagerduty.AlertProvider{}, + Slack: &slack.AlertProvider{}, + Twilio: &twilio.AlertProvider{}, + }, + } + if GetAlertingProviderByAlertType(cfg, core.CustomAlert) != cfg.Alerting.Custom { + t.Error("expected Custom configuration") + } + if GetAlertingProviderByAlertType(cfg, core.DiscordAlert) != cfg.Alerting.Discord { + t.Error("expected Discord configuration") + } + if GetAlertingProviderByAlertType(cfg, core.MattermostAlert) != cfg.Alerting.Mattermost { + t.Error("expected Mattermost configuration") + } + if GetAlertingProviderByAlertType(cfg, core.MessagebirdAlert) != cfg.Alerting.Messagebird { + t.Error("expected Messagebird configuration") + } + if GetAlertingProviderByAlertType(cfg, core.PagerDutyAlert) != cfg.Alerting.PagerDuty { + t.Error("expected PagerDuty configuration") + } + if GetAlertingProviderByAlertType(cfg, core.SlackAlert) != cfg.Alerting.Slack { + t.Error("expected Slack configuration") + } + if GetAlertingProviderByAlertType(cfg, core.TwilioAlert) != cfg.Alerting.Twilio { + t.Error("expected Twilio configuration") + } +} diff --git a/core/alert.go b/core/alert.go index 1ad26829..254c09ef 100644 --- a/core/alert.go +++ b/core/alert.go @@ -40,8 +40,11 @@ type Alert struct { type AlertType string const ( - // SlackAlert is the AlertType for the slack alerting provider - SlackAlert AlertType = "slack" + // CustomAlert is the AlertType for the custom alerting provider + CustomAlert AlertType = "custom" + + // DiscordAlert is the AlertType for the discord alerting provider + DiscordAlert AlertType = "discord" // MattermostAlert is the AlertType for the mattermost alerting provider MattermostAlert AlertType = "mattermost" @@ -52,9 +55,9 @@ const ( // PagerDutyAlert is the AlertType for the pagerduty alerting provider PagerDutyAlert AlertType = "pagerduty" + // SlackAlert is the AlertType for the slack alerting provider + SlackAlert AlertType = "slack" + // TwilioAlert is the AlertType for the twilio alerting provider TwilioAlert AlertType = "twilio" - - // CustomAlert is the AlertType for the custom alerting provider - CustomAlert AlertType = "custom" )