Add Telegram Alerting (#102)

This commit is contained in:
Jonah 2021-03-31 01:38:34 +02:00 committed by GitHub
parent 4e5a86031f
commit 24da853820
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 212 additions and 9 deletions

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

3
.gitignore vendored
View File

@ -1,4 +1,5 @@
.idea .idea
.vscode .vscode
gatus gatus
db.db db.db
config/config.yml

View File

@ -31,6 +31,7 @@ core applications: https://status.twinnation.org/
- [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 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)
- [Auto Discovery](#auto-discovery) - [Auto Discovery](#auto-discovery)
@ -220,6 +221,9 @@ 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.telegram` | Configuration for alerts of type `telegram` | `{}` |
| `alerting.telegram.token` | Telegram Bot Token | Required `""` |
| `alerting.telegram.id` | Telegram User ID | Required `""` |
| `alerting.custom` | Configuration for custom actions on failure or alerts | `{}` | | `alerting.custom` | Configuration for custom actions on failure or alerts | `{}` |
| `alerting.custom.url` | Custom alerting request url | Required `""` | | `alerting.custom.url` | Custom alerting request url | Required `""` |
| `alerting.custom.method` | Request method | `GET` | | `alerting.custom.method` | Request method | `GET` |
@ -395,6 +399,31 @@ services:
``` ```
#### Configuring Telegram alerts
```yaml
alerting:
telegram:
token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
id: "0123456789"
services:
- name: twinnation
url: "https://twinnation.org/health"
interval: 30s
alerts:
- type: telegram
enabled: true
send-on-resolved: true
conditions:
- "[STATUS] == 200"
- "[BODY].status == UP"
```
Here's an example of what the notifications look like:
![Telegram notifications](.github/assets/telegram-alerts.png)
#### Configuring custom alerts #### Configuring custom alerts
While they're called alerts, you can use this feature to call anything. While they're called alerts, you can use this feature to call anything.

View File

@ -7,6 +7,7 @@ import (
"github.com/TwinProduction/gatus/alerting/provider/messagebird" "github.com/TwinProduction/gatus/alerting/provider/messagebird"
"github.com/TwinProduction/gatus/alerting/provider/pagerduty" "github.com/TwinProduction/gatus/alerting/provider/pagerduty"
"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/twilio" "github.com/TwinProduction/gatus/alerting/provider/twilio"
) )
@ -30,6 +31,9 @@ type Config struct {
// Slack is the configuration for the slack alerting provider // Slack is the configuration for the slack alerting provider
Slack *slack.AlertProvider `yaml:"slack"` Slack *slack.AlertProvider `yaml:"slack"`
// Telegram is the configuration for the telegram alerting provider
Telegram *telegram.AlertProvider `yaml:"telegram"`
// 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"`
} }

View File

@ -7,6 +7,7 @@ import (
"github.com/TwinProduction/gatus/alerting/provider/messagebird" "github.com/TwinProduction/gatus/alerting/provider/messagebird"
"github.com/TwinProduction/gatus/alerting/provider/pagerduty" "github.com/TwinProduction/gatus/alerting/provider/pagerduty"
"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/twilio" "github.com/TwinProduction/gatus/alerting/provider/twilio"
"github.com/TwinProduction/gatus/core" "github.com/TwinProduction/gatus/core"
) )
@ -28,5 +29,6 @@ var (
_ AlertProvider = (*messagebird.AlertProvider)(nil) _ AlertProvider = (*messagebird.AlertProvider)(nil)
_ AlertProvider = (*pagerduty.AlertProvider)(nil) _ AlertProvider = (*pagerduty.AlertProvider)(nil)
_ AlertProvider = (*slack.AlertProvider)(nil) _ AlertProvider = (*slack.AlertProvider)(nil)
_ AlertProvider = (*telegram.AlertProvider)(nil)
_ AlertProvider = (*twilio.AlertProvider)(nil) _ AlertProvider = (*twilio.AlertProvider)(nil)
) )

View File

@ -0,0 +1,52 @@
package telegram
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 Telegram
type AlertProvider struct {
Token string `yaml:"token"`
ID string `yaml:"id"`
}
// IsValid returns whether the provider's configuration is valid
func (provider *AlertProvider) IsValid() bool {
return len(provider.Token) > 0 && len(provider.ID) > 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
if resolved {
message = fmt.Sprintf("An alert for *%s* has been resolved:\\n—\\n _healthcheck passing successfully %d time(s) in a row_\\n— ", service.Name, alert.FailureThreshold)
} else {
message = fmt.Sprintf("An alert for *%s* has been triggered:\\n—\\n _healthcheck failed %d time(s) in a row_\\n— ", service.Name, alert.FailureThreshold)
}
for _, conditionResult := range result.ConditionResults {
var prefix string
if conditionResult.Success {
prefix = "✅"
} else {
prefix = "❌"
}
results += fmt.Sprintf("%s - `%s`\\n", prefix, conditionResult.Condition)
}
var text string
if len(alert.Description) > 0 {
text = fmt.Sprintf("⛑ *Gatus* \\n%s \\n*Description* \\n_%s_ \\n\\n*Condition results*\\n%s", message, alert.Description, results)
} else {
text = fmt.Sprintf("⛑ *Gatus* \\n%s \\n*Condition results*\\n%s", message, results)
}
return &custom.AlertProvider{
URL: fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", provider.Token),
Method: http.MethodPost,
Body: fmt.Sprintf(`{"chat_id": "%s", "text": "%s", "parse_mode": "MARKDOWN" }`, provider.ID, text),
Headers: map[string]string{"Content-Type": "application/json"},
}
}

View File

@ -0,0 +1,89 @@
package telegram
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"
"github.com/TwinProduction/gatus/core"
)
func TestAlertProvider_IsValid(t *testing.T) {
invalidProvider := AlertProvider{Token: "", ID: ""}
if invalidProvider.IsValid() {
t.Error("provider shouldn't have been valid")
}
validProvider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678"}
if !validProvider.IsValid() {
t.Error("provider should've been valid")
}
}
func TestAlertProvider_ToCustomAlertProviderWithResolvedAlert(t *testing.T) {
provider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678"}
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 != fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", provider.Token) {
t.Errorf("expected URL to be %s, got %s", fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", provider.Token), 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)
//_, err := json.Marshal(customAlertProvider.Body)
if err != nil {
t.Error("expected body to be valid JSON, got error:", err.Error())
}
}
func TestAlertProvider_ToCustomAlertProviderWithTriggeredAlert(t *testing.T) {
provider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "0123456789"}
customAlertProvider := provider.ToCustomAlertProvider(&core.Service{}, &core.Alert{Description: "Healthcheck Successful"}, &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 != fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", provider.Token) {
t.Errorf("expected URL to be %s, got %s", fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", provider.Token), 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_ToCustomAlertProviderWithDescription(t *testing.T) {
provider := AlertProvider{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "0123456789"}
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 != fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", provider.Token) {
t.Errorf("expected URL to be %s, got %s", fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", provider.Token), 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

@ -234,6 +234,7 @@ func validateAlertingConfig(config *Config) {
core.MessagebirdAlert, core.MessagebirdAlert,
core.PagerDutyAlert, core.PagerDutyAlert,
core.SlackAlert, core.SlackAlert,
core.TelegramAlert,
core.TwilioAlert, core.TwilioAlert,
} }
var validProviders, invalidProviders []core.AlertType var validProviders, invalidProviders []core.AlertType
@ -292,6 +293,12 @@ func GetAlertingProviderByAlertType(config *Config, alertType core.AlertType) pr
return nil return nil
} }
return config.Alerting.Slack return config.Alerting.Slack
case core.TelegramAlert:
if config.Alerting.Telegram == 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.Telegram
case core.TwilioAlert: case core.TwilioAlert:
if config.Alerting.Twilio == nil { if config.Alerting.Twilio == nil {
// Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil // Since we're returning an interface, we need to explicitly return nil, even if the provider itself is nil

View File

@ -13,6 +13,7 @@ import (
"github.com/TwinProduction/gatus/alerting/provider/messagebird" "github.com/TwinProduction/gatus/alerting/provider/messagebird"
"github.com/TwinProduction/gatus/alerting/provider/pagerduty" "github.com/TwinProduction/gatus/alerting/provider/pagerduty"
"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/twilio" "github.com/TwinProduction/gatus/alerting/provider/twilio"
"github.com/TwinProduction/gatus/core" "github.com/TwinProduction/gatus/core"
"github.com/TwinProduction/gatus/k8stest" "github.com/TwinProduction/gatus/k8stest"
@ -354,6 +355,9 @@ alerting:
access-key: "1" access-key: "1"
originator: "31619191918" originator: "31619191918"
recipients: "31619191919" recipients: "31619191919"
telegram:
token: 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11
id: 0123456789
services: services:
- name: twinnation - name: twinnation
url: https://twinnation.org/health url: https://twinnation.org/health
@ -369,6 +373,8 @@ services:
- type: discord - type: discord
enabled: true enabled: true
failure-threshold: 10 failure-threshold: 10
- type: telegram
enabled: true
conditions: conditions:
- "[STATUS] == 200" - "[STATUS] == 200"
`)) `))
@ -394,12 +400,13 @@ services:
if config.Alerting.PagerDuty == nil || !config.Alerting.PagerDuty.IsValid() { if config.Alerting.PagerDuty == nil || !config.Alerting.PagerDuty.IsValid() {
t.Fatal("PagerDuty alerting config should've been valid") 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" { if config.Alerting.PagerDuty.IntegrationKey != "00000000000000000000000000000000" {
t.Errorf("PagerDuty integration key should've been %s, but was %s", "00000000000000000000000000000000", config.Alerting.PagerDuty.IntegrationKey) t.Errorf("PagerDuty integration key should've been %s, but was %s", "00000000000000000000000000000000", config.Alerting.PagerDuty.IntegrationKey)
} }
if config.Alerting.Messagebird == nil || !config.Alerting.Messagebird.IsValid() {
t.Fatal("Messagebird alerting config should've been valid")
}
if config.Alerting.Messagebird.AccessKey != "1" { if config.Alerting.Messagebird.AccessKey != "1" {
t.Errorf("Messagebird access key should've been %s, but was %s", "1", config.Alerting.Messagebird.AccessKey) t.Errorf("Messagebird access key should've been %s, but was %s", "1", config.Alerting.Messagebird.AccessKey)
} }
@ -409,12 +416,20 @@ services:
if config.Alerting.Messagebird.Recipients != "31619191919" { if config.Alerting.Messagebird.Recipients != "31619191919" {
t.Errorf("Messagebird to recipients should've been %s, but was %s", "31619191919", config.Alerting.Messagebird.Recipients) 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() { if config.Alerting.Discord == nil || !config.Alerting.Discord.IsValid() {
t.Fatal("Discord alerting config should've been valid") t.Fatal("Discord alerting config should've been valid")
} }
if config.Alerting.Discord.WebhookURL != "http://example.org" { 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) t.Errorf("Discord webhook should've been %s, but was %s", "http://example.org", config.Alerting.Discord.WebhookURL)
} }
if config.Alerting.Telegram.Token != "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" {
t.Errorf("Telegram token should've been %s, but was %s", "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", config.Alerting.Telegram.Token)
}
if config.Alerting.Telegram.ID != "0123456789" {
t.Errorf("Telegram ID should've been %s, but was %s", "012345689", config.Alerting.Telegram.ID)
}
if GetAlertingProviderByAlertType(config, core.DiscordAlert) != config.Alerting.Discord { if GetAlertingProviderByAlertType(config, core.DiscordAlert) != config.Alerting.Discord {
t.Error("expected discord configuration") t.Error("expected discord configuration")
} }
@ -428,8 +443,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) != 4 { if len(config.Services[0].Alerts) != 5 {
t.Fatal("There should've been 4 alerts configured") t.Fatal("There should've been 5 alerts configured")
} }
if config.Services[0].Alerts[0].Type != core.SlackAlert { if config.Services[0].Alerts[0].Type != core.SlackAlert {
@ -451,9 +466,6 @@ services:
if config.Services[0].Alerts[1].Description != "Healthcheck failed 7 times in a row" { 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) 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 { 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) t.Errorf("The success threshold of the alert should've been %d, but it was %d", 5, config.Services[0].Alerts[1].SuccessThreshold)
} }
@ -855,6 +867,7 @@ func TestGetAlertingProviderByAlertType(t *testing.T) {
Messagebird: &messagebird.AlertProvider{}, Messagebird: &messagebird.AlertProvider{},
PagerDuty: &pagerduty.AlertProvider{}, PagerDuty: &pagerduty.AlertProvider{},
Slack: &slack.AlertProvider{}, Slack: &slack.AlertProvider{},
Telegram: &telegram.AlertProvider{},
Twilio: &twilio.AlertProvider{}, Twilio: &twilio.AlertProvider{},
}, },
} }
@ -876,6 +889,9 @@ func TestGetAlertingProviderByAlertType(t *testing.T) {
if GetAlertingProviderByAlertType(cfg, core.SlackAlert) != cfg.Alerting.Slack { if GetAlertingProviderByAlertType(cfg, core.SlackAlert) != cfg.Alerting.Slack {
t.Error("expected Slack configuration") t.Error("expected Slack configuration")
} }
if GetAlertingProviderByAlertType(cfg, core.TelegramAlert) != cfg.Alerting.Telegram {
t.Error("expected Telegram configuration")
}
if GetAlertingProviderByAlertType(cfg, core.TwilioAlert) != cfg.Alerting.Twilio { if GetAlertingProviderByAlertType(cfg, core.TwilioAlert) != cfg.Alerting.Twilio {
t.Error("expected Twilio configuration") t.Error("expected Twilio configuration")
} }

View File

@ -58,6 +58,9 @@ const (
// SlackAlert is the AlertType for the slack alerting provider // SlackAlert is the AlertType for the slack alerting provider
SlackAlert AlertType = "slack" SlackAlert AlertType = "slack"
// TelegramAlert is the AlertType for the telegram alerting provider
TelegramAlert AlertType = "telegram"
// TwilioAlert is the AlertType for the twilio alerting provider // TwilioAlert is the AlertType for the twilio alerting provider
TwilioAlert AlertType = "twilio" TwilioAlert AlertType = "twilio"
) )