From c9cd7ea7014ed1deac7b9cfef3f3a40a7293822e Mon Sep 17 00:00:00 2001 From: Ganesh Vernekar <15064823+codesome@users.noreply.github.com> Date: Thu, 22 Apr 2021 20:54:59 +0530 Subject: [PATCH] AlertingNG: Add Telegram notification channel (#32795) Signed-off-by: Ganesh Vernekar --- pkg/services/ngalert/notifier/alertmanager.go | 5 +- .../ngalert/notifier/channels/telegram.go | 144 ++++++++++++++++++ .../notifier/channels/telegram_test.go | 131 ++++++++++++++++ .../notifier/channels/templates/default.tmpl | 11 ++ 4 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 pkg/services/ngalert/notifier/channels/telegram.go create mode 100644 pkg/services/ngalert/notifier/channels/telegram_test.go diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index f95602da55e..45c99f72778 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -4,14 +4,13 @@ import ( "context" "crypto/md5" "encoding/json" + "errors" "fmt" "net/url" "path/filepath" "sync" "time" - "errors" - gokit_log "github.com/go-kit/kit/log" amv2 "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/alertmanager/dispatch" @@ -367,6 +366,8 @@ func (am *Alertmanager) buildReceiverIntegrations(receiver *apimodels.PostableAp n, err = channels.NewPagerdutyNotifier(cfg, tmpl, externalURL) case "slack": n, err = channels.NewSlackNotifier(cfg, tmpl, externalURL) + case "telegram": + n, err = channels.NewTelegramNotifier(cfg, tmpl, externalURL) } if err != nil { return nil, err diff --git a/pkg/services/ngalert/notifier/channels/telegram.go b/pkg/services/ngalert/notifier/channels/telegram.go new file mode 100644 index 00000000000..3723f46345e --- /dev/null +++ b/pkg/services/ngalert/notifier/channels/telegram.go @@ -0,0 +1,144 @@ +package channels + +import ( + "bytes" + "context" + "fmt" + "mime/multipart" + "net/url" + + gokit_log "github.com/go-kit/kit/log" + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/template" + "github.com/prometheus/alertmanager/types" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" + old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers" +) + +const ( + telegramAPIURL = "https://api.telegram.org/bot%s/sendMessage" +) + +// TelegramNotifier is responsible for sending +// alert notifications to Telegram. +type TelegramNotifier struct { + old_notifiers.NotifierBase + BotToken string + ChatID string + Message string + log log.Logger + tmpl *template.Template + externalUrl *url.URL +} + +// NewTelegramNotifier is the constructor for the Telegram notifier +func NewTelegramNotifier(model *models.AlertNotification, t *template.Template, externalUrl *url.URL) (*TelegramNotifier, error) { + if model.Settings == nil { + return nil, alerting.ValidationError{Reason: "No Settings Supplied"} + } + + botToken := model.DecryptedValue("bottoken", model.Settings.Get("bottoken").MustString()) + chatID := model.Settings.Get("chatid").MustString() + message := model.Settings.Get("message").MustString(`{{ template "default.message" . }}`) + + if botToken == "" { + return nil, alerting.ValidationError{Reason: "Could not find Bot Token in settings"} + } + + if chatID == "" { + return nil, alerting.ValidationError{Reason: "Could not find Chat Id in settings"} + } + + return &TelegramNotifier{ + NotifierBase: old_notifiers.NewNotifierBase(model), + BotToken: botToken, + ChatID: chatID, + Message: message, + tmpl: t, + log: log.New("alerting.notifier.telegram"), + externalUrl: externalUrl, + }, nil +} + +// Notify send an alert notification to Telegram. +func (tn *TelegramNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { + msg, err := tn.buildTelegramMessage(ctx, as) + if err != nil { + return false, err + } + + var body bytes.Buffer + w := multipart.NewWriter(&body) + defer func() { + if err := w.Close(); err != nil { + tn.log.Warn("Failed to close writer", "err", err) + } + }() + + for k, v := range msg { + if err := writeField(w, k, v); err != nil { + return false, err + } + } + + // We need to close it before using so that the last part + // is added to the writer along with the boundary. + if err := w.Close(); err != nil { + return false, err + } + + tn.log.Info("sending telegram notification", "chat_id", tn.ChatID) + cmd := &models.SendWebhookSync{ + Url: fmt.Sprintf(telegramAPIURL, tn.BotToken), + Body: body.String(), + HttpMethod: "POST", + HttpHeader: map[string]string{ + "Content-Type": w.FormDataContentType(), + }, + } + + if err := bus.DispatchCtx(ctx, cmd); err != nil { + tn.log.Error("Failed to send webhook", "error", err, "webhook", tn.Name) + return false, err + } + + return true, nil +} + +func (tn *TelegramNotifier) buildTelegramMessage(ctx context.Context, as []*types.Alert) (map[string]string, error) { + msg := map[string]string{} + msg["chat_id"] = tn.ChatID + msg["parse_mode"] = "html" + + data := notify.GetTemplateData(ctx, &template.Template{ExternalURL: tn.externalUrl}, as, gokit_log.NewNopLogger()) + var tmplErr error + tmpl := notify.TmplText(tn.tmpl, data, &tmplErr) + + message := tmpl(tn.Message) + if tmplErr != nil { + return nil, tmplErr + } + + msg["text"] = message + + return msg, nil +} + +func writeField(w *multipart.Writer, name, value string) error { + fw, err := w.CreateFormField(name) + if err != nil { + return err + } + if _, err := fw.Write([]byte(value)); err != nil { + return err + } + return nil +} + +func (tn *TelegramNotifier) SendResolved() bool { + return !tn.GetDisableResolveMessage() +} diff --git a/pkg/services/ngalert/notifier/channels/telegram_test.go b/pkg/services/ngalert/notifier/channels/telegram_test.go new file mode 100644 index 00000000000..df4f161157f --- /dev/null +++ b/pkg/services/ngalert/notifier/channels/telegram_test.go @@ -0,0 +1,131 @@ +package channels + +import ( + "context" + "errors" + "net/url" + "testing" + + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/template" + "github.com/prometheus/alertmanager/types" + "github.com/prometheus/common/model" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" +) + +func TestTelegramNotifier(t *testing.T) { + tmpl, err := template.FromGlobs("templates/default.tmpl") + require.NoError(t, err) + + cases := []struct { + name string + settings string + alerts []*types.Alert + expMsg map[string]string + expInitError error + expMsgError error + }{ + { + name: "Default template with one alert", + settings: `{ + "bottoken": "abcdefgh0123456789", + "chatid": "someid" + }`, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1"}, + GeneratorURL: "a URL", + }, + }, + }, + expMsg: map[string]string{ + "chat_id": "someid", + "parse_mode": "html", + "text": "\n**Firing**\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: a URL\n\n\n\n\n", + }, + expInitError: nil, + expMsgError: nil, + }, { + name: "Custom template with multiple alerts", + settings: `{ + "bottoken": "abcdefgh0123456789", + "chatid": "someid", + "message": "__Custom Firing__\n{{len .Alerts.Firing}} Firing\n{{ template \"__text_alert_list\" .Alerts.Firing }}" + }`, + alerts: []*types.Alert{ + { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, + Annotations: model.LabelSet{"ann1": "annv1"}, + GeneratorURL: "a URL", + }, + }, { + Alert: model.Alert{ + Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val2"}, + Annotations: model.LabelSet{"ann1": "annv2"}, + }, + }, + }, + expMsg: map[string]string{ + "chat_id": "someid", + "parse_mode": "html", + "text": "__Custom Firing__\n2 Firing\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: a URL\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSource: \n", + }, + expInitError: nil, + expMsgError: nil, + }, { + name: "Error in initing", + settings: `{}`, + expInitError: alerting.ValidationError{Reason: "Could not find Bot Token in settings"}, + }, { + name: "Error in building message", + settings: `{ + "bottoken": "abcdefgh0123456789", + "chatid": "someid", + "message": "{{ .BrokenTemplate }" + }`, + expMsgError: errors.New("template: :1: unexpected \"}\" in operand"), + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + settingsJSON, err := simplejson.NewJson([]byte(c.settings)) + require.NoError(t, err) + + m := &models.AlertNotification{ + Name: "telegram_testing", + Type: "telegram", + Settings: settingsJSON, + } + + externalURL, err := url.Parse("http://localhost") + require.NoError(t, err) + pn, err := NewTelegramNotifier(m, tmpl, externalURL) + if c.expInitError != nil { + require.Error(t, err) + require.Equal(t, c.expInitError.Error(), err.Error()) + return + } + require.NoError(t, err) + + ctx := notify.WithGroupKey(context.Background(), "alertname") + ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) + msg, err := pn.buildTelegramMessage(ctx, c.alerts) + if c.expMsgError != nil { + require.Error(t, err) + require.Equal(t, c.expMsgError.Error(), err.Error()) + return + } + require.NoError(t, err) + + require.Equal(t, c.expMsg, msg) + }) + } +} diff --git a/pkg/services/ngalert/notifier/channels/templates/default.tmpl b/pkg/services/ngalert/notifier/channels/templates/default.tmpl index 64e95b49abb..1eb8d77204e 100644 --- a/pkg/services/ngalert/notifier/channels/templates/default.tmpl +++ b/pkg/services/ngalert/notifier/channels/templates/default.tmpl @@ -11,6 +11,17 @@ {{ end }}Source: {{ .GeneratorURL }} {{ end }}{{ end }} +{{ define "default.title" }}{{ template "__subject" . }}{{ end }} +{{ define "default.message" }}{{ if gt (len .Alerts.Firing) 0 }} +**Firing** +{{ template "__text_alert_list" .Alerts.Firing }} + +{{ end }} +{{ if gt (len .Alerts.Resolved) 0 }} +**Resolved** +{{ template "__text_alert_list" .Alerts.Resolved }} +{{ end }} +{{ end }} {{ define "slack.default.title" }}{{ template "__subject" . }}{{ end }} {{ define "slack.default.username" }}{{ template "__alertmanager" . }}{{ end }}