mirror of https://github.com/grafana/grafana
AlertingNG: Add Telegram notification channel (#32795)
Signed-off-by: Ganesh Vernekar <ganeshvern@gmail.com>pull/32979/head
parent
0a03d5c29e
commit
c9cd7ea701
@ -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() |
||||
} |
@ -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) |
||||
}) |
||||
} |
||||
} |
Loading…
Reference in new issue