Merge pull request #143 from zeylos/feature/teams_alert_provider

Add Microsoft Teams alerting provider
This commit is contained in:
Chris 2021-07-29 19:16:03 -04:00 committed by GitHub
commit 2ce02b0d7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 237 additions and 6 deletions

BIN
.github/assets/teams-alerts.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -42,6 +42,7 @@ For more details, see [Usage](#usage)
- [Configuring Twilio alerts](#configuring-twilio-alerts) - [Configuring Twilio alerts](#configuring-twilio-alerts)
- [Configuring Mattermost alerts](#configuring-mattermost-alerts) - [Configuring Mattermost alerts](#configuring-mattermost-alerts)
- [Configuring Messagebird alerts](#configuring-messagebird-alerts) - [Configuring Messagebird alerts](#configuring-messagebird-alerts)
- [Configuring Teams alerts](#configuring-teams-alerts)
- [Configuring Telegram alerts](#configuring-telegram-alerts) - [Configuring Telegram alerts](#configuring-telegram-alerts)
- [Configuring custom alerts](#configuring-custom-alerts) - [Configuring custom alerts](#configuring-custom-alerts)
- [Kubernetes (ALPHA)](#kubernetes-alpha) - [Kubernetes (ALPHA)](#kubernetes-alpha)
@ -94,7 +95,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. - **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. - **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. - **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, 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. - **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, Twilio and Teams 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** - **Metrics**
- **Low resource consumption**: As with most Go applications, the resource footprint that this application requires is negligibly small. - **Low resource consumption**: As with most Go applications, the resource footprint that this application requires is negligibly small.
- **GitHub uptime badges**: ![Uptime 1h](https://status.twinnation.org/api/v1/badges/uptime/1h/core_twinnation-external.svg) ![Uptime 24h](https://status.twinnation.org/api/v1/badges/uptime/24h/core_twinnation-external.svg) ![Uptime 7d](https://status.twinnation.org/api/v1/badges/uptime/7d/core_twinnation-external.svg) - **GitHub uptime badges**: ![Uptime 1h](https://status.twinnation.org/api/v1/badges/uptime/1h/core_twinnation-external.svg) ![Uptime 24h](https://status.twinnation.org/api/v1/badges/uptime/24h/core_twinnation-external.svg) ![Uptime 7d](https://status.twinnation.org/api/v1/badges/uptime/7d/core_twinnation-external.svg)
@ -152,7 +153,7 @@ If you want to test it locally, see [Docker](#docker).
| `services[].dns` | Configuration for a service of type DNS. See [Monitoring a service using DNS queries](#monitoring-a-service-using-dns-queries). | `""` | | `services[].dns` | Configuration for a service of type DNS. See [Monitoring a service using DNS queries](#monitoring-a-service-using-dns-queries). | `""` |
| `services[].dns.query-type` | Query type for DNS service. | `""` | | `services[].dns.query-type` | Query type for DNS service. | `""` |
| `services[].dns.query-name` | Query name for DNS service. | `""` | | `services[].dns.query-name` | Query name for DNS service. | `""` |
| `services[].alerts[].type` | Type of alert. Valid types: `slack`, `discord`, `pagerduty`, `twilio`, `mattermost`, `messagebird`, `custom`. | Required `""` | | `services[].alerts[].type` | Type of alert. Valid types: `slack`, `discord`, `pagerduty`, `twilio`, `mattermost`, `messagebird`, `teams` `custom`. | Required `""` |
| `services[].alerts[].enabled` | Whether to enable the alert. | `false` | | `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[].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` | | `services[].alerts[].success-threshold` | Number of successes in a row before an ongoing incident is marked as resolved. | `2` |
@ -303,6 +304,8 @@ ignored.
| `alerting.messagebird.access-key` | Messagebird access key | Required `""` | | `alerting.messagebird.access-key` | Messagebird access key | Required `""` |
| `alerting.messagebird.originator` | The sender of the message | Required `""` | | `alerting.messagebird.originator` | The sender of the message | Required `""` |
| `alerting.messagebird.recipients` | The recipients of the message | Required `""` | | `alerting.messagebird.recipients` | The recipients of the message | Required `""` |
| `alerting.teams` | Configuration for alerts of type `teams` | `{}` |
| `alerting.teams.webhook-url` | Teams Webhook URL | Required `""` |
| `alerting.telegram` | Configuration for alerts of type `telegram` | `{}` | | `alerting.telegram` | Configuration for alerts of type `telegram` | `{}` |
| `alerting.telegram.token` | Telegram Bot Token | Required `""` | | `alerting.telegram.token` | Telegram Bot Token | Required `""` |
| `alerting.telegram.id` | Telegram User ID | Required `""` | | `alerting.telegram.id` | Telegram User ID | Required `""` |
@ -481,6 +484,30 @@ services:
- "[RESPONSE_TIME] < 300" - "[RESPONSE_TIME] < 300"
``` ```
#### Configuring Teams alerts
```yaml
alerting:
teams:
webhook-url: "https://********.webhook.office.com/webhookb2/************"
services:
- name: twinnation
url: "https://twinnation.org/health"
interval: 30s
alerts:
- type: teams
enabled: true
description: "healthcheck failed"
send-on-resolved: true
conditions:
- "[STATUS] == 200"
- "[BODY].status == UP"
- "[RESPONSE_TIME] < 300"
```
Here's an example of what the notifications look like:
![Teams notifications](.github/assets/teams-alerts.png)
#### Configuring Telegram alerts #### Configuring Telegram alerts
```yaml ```yaml

View File

@ -28,4 +28,7 @@ const (
// TypeTwilio is the Type for the twilio alerting provider // TypeTwilio is the Type for the twilio alerting provider
TypeTwilio Type = "twilio" TypeTwilio Type = "twilio"
// Teams is the Type for the teams alerting provider
TypeTeams Type = "teams"
) )

View File

@ -11,6 +11,7 @@ import (
"github.com/TwinProduction/gatus/alerting/provider/slack" "github.com/TwinProduction/gatus/alerting/provider/slack"
"github.com/TwinProduction/gatus/alerting/provider/telegram" "github.com/TwinProduction/gatus/alerting/provider/telegram"
"github.com/TwinProduction/gatus/alerting/provider/twilio" "github.com/TwinProduction/gatus/alerting/provider/twilio"
"github.com/TwinProduction/gatus/alerting/provider/teams"
) )
// Config is the configuration for alerting providers // Config is the configuration for alerting providers
@ -38,6 +39,9 @@ type Config struct {
// Twilio is the configuration for the twilio alerting provider // Twilio is the configuration for the twilio alerting provider
Twilio *twilio.AlertProvider `yaml:"twilio"` Twilio *twilio.AlertProvider `yaml:"twilio"`
// Teams is the configuration for the teams alerting provider
Teams *teams.AlertProvider `yaml:"teams"`
} }
// GetAlertingProviderByAlertType returns an provider.AlertProvider by its corresponding alert.Type // GetAlertingProviderByAlertType returns an provider.AlertProvider by its corresponding alert.Type
@ -91,6 +95,12 @@ func (config Config) GetAlertingProviderByAlertType(alertType alert.Type) provid
return nil return nil
} }
return config.Twilio return config.Twilio
case alert.TypeTeams:
if config.Teams == nil {
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil
return nil
}
return config.Teams
} }
return nil return nil
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/TwinProduction/gatus/alerting/provider/slack" "github.com/TwinProduction/gatus/alerting/provider/slack"
"github.com/TwinProduction/gatus/alerting/provider/telegram" "github.com/TwinProduction/gatus/alerting/provider/telegram"
"github.com/TwinProduction/gatus/alerting/provider/twilio" "github.com/TwinProduction/gatus/alerting/provider/twilio"
"github.com/TwinProduction/gatus/alerting/provider/teams"
"github.com/TwinProduction/gatus/core" "github.com/TwinProduction/gatus/core"
) )
@ -57,4 +58,5 @@ var (
_ AlertProvider = (*slack.AlertProvider)(nil) _ AlertProvider = (*slack.AlertProvider)(nil)
_ AlertProvider = (*telegram.AlertProvider)(nil) _ AlertProvider = (*telegram.AlertProvider)(nil)
_ AlertProvider = (*twilio.AlertProvider)(nil) _ AlertProvider = (*twilio.AlertProvider)(nil)
_ AlertProvider = (*teams.AlertProvider)(nil)
) )

View File

@ -0,0 +1,73 @@
package teams
import (
"fmt"
"net/http"
"github.com/TwinProduction/gatus/alerting/alert"
"github.com/TwinProduction/gatus/alerting/provider/custom"
"github.com/TwinProduction/gatus/core"
)
// AlertProvider is the configuration necessary for sending an alert using Teams
type AlertProvider struct {
WebhookURL string `yaml:"webhook-url"`
// DefaultAlert is the default alert configuration to use for services with an alert of the appropriate type
DefaultAlert *alert.Alert `yaml:"default-alert"`
}
// 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 *alert.Alert, result *core.Result, resolved bool) *custom.AlertProvider {
var message string
var color string
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)
color = "#36A64F"
} 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)
color = "#DD0000"
}
var results string
for _, conditionResult := range result.ConditionResults {
var prefix string
if conditionResult.Success {
prefix = "&#x2705;"
} else {
prefix = "&#x274C;"
}
results += fmt.Sprintf("%s - `%s`<br/>", prefix, conditionResult.Condition)
}
return &custom.AlertProvider{
URL: provider.WebhookURL,
Method: http.MethodPost,
Body: fmt.Sprintf(`{
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
"themeColor": "%s",
"title": "&#x1F6A8; Gatus",
"text": "%s:\n> %s",
"sections": [
{
"activityTitle": "URL",
"text": "%s"
},
{
"activityTitle": "Condition results",
"text": "%s"
}
]
}`, color, message, alert.GetDescription(), service.URL, results),
Headers: map[string]string{"Content-Type": "application/json"},
}
}
// GetDefaultAlert returns the provider's default alert configuration
func (provider AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}

View File

@ -0,0 +1,66 @@
package teams
import (
"encoding/json"
"net/http"
"strings"
"testing"
"github.com/TwinProduction/gatus/alerting/alert"
"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.org"}
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &alert.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.org" {
t.Errorf("expected URL to be %s, got %s", "http://example.org", 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.org"}
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &alert.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.org" {
t.Errorf("expected URL to be %s, got %s", "http://example.org", 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())
}
}

View File

@ -272,6 +272,7 @@ func validateAlertingConfig(alertingConfig *alerting.Config, services []*core.Se
alert.TypeSlack, alert.TypeSlack,
alert.TypeTelegram, alert.TypeTelegram,
alert.TypeTwilio, alert.TypeTwilio,
alert.TypeTeams,
} }
var validProviders, invalidProviders []alert.Type var validProviders, invalidProviders []alert.Type
for _, alertType := range alertTypes { for _, alertType := range alertTypes {

View File

@ -16,6 +16,7 @@ import (
"github.com/TwinProduction/gatus/alerting/provider/slack" "github.com/TwinProduction/gatus/alerting/provider/slack"
"github.com/TwinProduction/gatus/alerting/provider/telegram" "github.com/TwinProduction/gatus/alerting/provider/telegram"
"github.com/TwinProduction/gatus/alerting/provider/twilio" "github.com/TwinProduction/gatus/alerting/provider/twilio"
"github.com/TwinProduction/gatus/alerting/provider/teams"
"github.com/TwinProduction/gatus/client" "github.com/TwinProduction/gatus/client"
"github.com/TwinProduction/gatus/core" "github.com/TwinProduction/gatus/core"
"github.com/TwinProduction/gatus/k8stest" "github.com/TwinProduction/gatus/k8stest"
@ -417,6 +418,8 @@ alerting:
token: "5678" token: "5678"
from: "+1-234-567-8901" from: "+1-234-567-8901"
to: "+1-234-567-8901" to: "+1-234-567-8901"
teams:
webhook-url: "http://example.com"
services: services:
- name: twinnation - name: twinnation
@ -441,6 +444,8 @@ services:
enabled: true enabled: true
failure-threshold: 12 failure-threshold: 12
success-threshold: 15 success-threshold: 15
- type: teams
enabled: true
conditions: conditions:
- "[STATUS] == 200" - "[STATUS] == 200"
`)) `))
@ -467,8 +472,8 @@ services:
if config.Services[0].Interval != 60*time.Second { if config.Services[0].Interval != 60*time.Second {
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second) t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
} }
if len(config.Services[0].Alerts) != 7 { if len(config.Services[0].Alerts) != 8 {
t.Fatal("There should've been 7 alerts configured") t.Fatal("There should've been 8 alerts configured")
} }
if config.Services[0].Alerts[0].Type != alert.TypeSlack { if config.Services[0].Alerts[0].Type != alert.TypeSlack {
@ -555,6 +560,19 @@ services:
if config.Services[0].Alerts[6].SuccessThreshold != 15 { if config.Services[0].Alerts[6].SuccessThreshold != 15 {
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 15, config.Services[0].Alerts[6].SuccessThreshold) t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 15, config.Services[0].Alerts[6].SuccessThreshold)
} }
if config.Services[0].Alerts[7].Type != alert.TypeTeams {
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeTeams, config.Services[0].Alerts[7].Type)
}
if !config.Services[0].Alerts[7].IsEnabled() {
t.Error("The alert should've been enabled")
}
if config.Services[0].Alerts[7].FailureThreshold != 3 {
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Services[0].Alerts[7].FailureThreshold)
}
if config.Services[0].Alerts[7].SuccessThreshold != 2 {
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Services[0].Alerts[7].SuccessThreshold)
}
} }
func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlert(t *testing.T) { func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlert(t *testing.T) {
@ -604,6 +622,10 @@ alerting:
enabled: true enabled: true
failure-threshold: 12 failure-threshold: 12
success-threshold: 15 success-threshold: 15
teams:
webhook-url: "http://example.com"
default-alert:
enabled: true
services: services:
- name: twinnation - name: twinnation
@ -617,6 +639,7 @@ services:
success-threshold: 2 # test service alert override success-threshold: 2 # test service alert override
- type: telegram - type: telegram
- type: twilio - type: twilio
- type: teams
conditions: conditions:
- "[STATUS] == 200" - "[STATUS] == 200"
`)) `))
@ -708,6 +731,14 @@ services:
if config.Alerting.Twilio.GetDefaultAlert() == nil { if config.Alerting.Twilio.GetDefaultAlert() == nil {
t.Fatal("Twilio.GetDefaultAlert() shouldn't have returned nil") t.Fatal("Twilio.GetDefaultAlert() shouldn't have returned nil")
} }
if config.Alerting.Teams == nil || !config.Alerting.Teams.IsValid() {
t.Fatal("Teams alerting config should've been valid")
}
if config.Alerting.Teams.GetDefaultAlert() == nil {
t.Fatal("Teams.GetDefaultAlert() shouldn't have returned nil")
}
// Services // Services
if len(config.Services) != 1 { if len(config.Services) != 1 {
t.Error("There should've been 1 service") t.Error("There should've been 1 service")
@ -718,8 +749,8 @@ services:
if config.Services[0].Interval != 60*time.Second { if config.Services[0].Interval != 60*time.Second {
t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second) t.Errorf("Interval should have been %s, because it is the default value", 60*time.Second)
} }
if len(config.Services[0].Alerts) != 7 { if len(config.Services[0].Alerts) != 8 {
t.Fatal("There should've been 7 alerts configured") t.Fatal("There should've been 8 alerts configured")
} }
if config.Services[0].Alerts[0].Type != alert.TypeSlack { if config.Services[0].Alerts[0].Type != alert.TypeSlack {
@ -809,6 +840,20 @@ services:
if config.Services[0].Alerts[6].SuccessThreshold != 15 { if config.Services[0].Alerts[6].SuccessThreshold != 15 {
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 15, config.Services[0].Alerts[6].SuccessThreshold) t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 15, config.Services[0].Alerts[6].SuccessThreshold)
} }
if config.Services[0].Alerts[7].Type != alert.TypeTeams {
t.Errorf("The type of the alert should've been %s, but it was %s", alert.TypeTeams, config.Services[0].Alerts[7].Type)
}
if !config.Services[0].Alerts[7].IsEnabled() {
t.Error("The alert should've been enabled")
}
if config.Services[0].Alerts[7].FailureThreshold != 3 {
t.Errorf("The default failure threshold of the alert should've been %d, but it was %d", 3, config.Services[0].Alerts[7].FailureThreshold)
}
if config.Services[0].Alerts[7].SuccessThreshold != 2 {
t.Errorf("The default success threshold of the alert should've been %d, but it was %d", 2, config.Services[0].Alerts[7].SuccessThreshold)
}
} }
func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlertAndMultipleAlertsOfSameTypeWithOverriddenParameters(t *testing.T) { func TestParseAndValidateConfigBytesWithAlertingAndDefaultAlertAndMultipleAlertsOfSameTypeWithOverriddenParameters(t *testing.T) {
@ -1318,6 +1363,7 @@ func TestGetAlertingProviderByAlertType(t *testing.T) {
Slack: &slack.AlertProvider{}, Slack: &slack.AlertProvider{},
Telegram: &telegram.AlertProvider{}, Telegram: &telegram.AlertProvider{},
Twilio: &twilio.AlertProvider{}, Twilio: &twilio.AlertProvider{},
Teams: &teams.AlertProvider{},
} }
if alertingConfig.GetAlertingProviderByAlertType(alert.TypeCustom) != alertingConfig.Custom { if alertingConfig.GetAlertingProviderByAlertType(alert.TypeCustom) != alertingConfig.Custom {
t.Error("expected Custom configuration") t.Error("expected Custom configuration")
@ -1343,4 +1389,7 @@ func TestGetAlertingProviderByAlertType(t *testing.T) {
if alertingConfig.GetAlertingProviderByAlertType(alert.TypeTwilio) != alertingConfig.Twilio { if alertingConfig.GetAlertingProviderByAlertType(alert.TypeTwilio) != alertingConfig.Twilio {
t.Error("expected Twilio configuration") t.Error("expected Twilio configuration")
} }
if alertingConfig.GetAlertingProviderByAlertType(alert.TypeTeams) != alertingConfig.Teams {
t.Error("expected Teams configuration")
}
} }