From e32a653cb61d3a4ecf1dbfb49556705c96feebfa Mon Sep 17 00:00:00 2001 From: Mikhail Leonov Date: Fri, 30 Dec 2016 15:57:12 +0300 Subject: [PATCH 1/2] Added Telegram Messenger notification destination --- pkg/metrics/metrics.go | 2 + pkg/services/alerting/notifiers/telegram.go | 86 +++++++++++++++++++ .../alerting/notifiers/telegram_test.go | 55 ++++++++++++ .../alerting/partials/notification_edit.html | 24 +++++- 4 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 pkg/services/alerting/notifiers/telegram.go create mode 100644 pkg/services/alerting/notifiers/telegram_test.go diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 6d6558f5271..5f937a5cbd8 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -47,6 +47,7 @@ var ( M_Alerting_Notification_Sent_PagerDuty Counter M_Alerting_Notification_Sent_Victorops Counter M_Alerting_Notification_Sent_OpsGenie Counter + M_Alerting_Notification_Sent_Telegram Counter M_Aws_CloudWatch_GetMetricStatistics Counter M_Aws_CloudWatch_ListMetrics Counter @@ -114,6 +115,7 @@ func initMetricVars(settings *MetricSettings) { M_Alerting_Notification_Sent_PagerDuty = RegCounter("alerting.notifications_sent", "type", "pagerduty") M_Alerting_Notification_Sent_Victorops = RegCounter("alerting.notifications_sent", "type", "victorops") M_Alerting_Notification_Sent_OpsGenie = RegCounter("alerting.notifications_sent", "type", "opsgenie") + M_Alerting_Notification_Sent_Telegram = RegCounter("alerting.notifications_sent", "type", "telegram") M_Aws_CloudWatch_GetMetricStatistics = RegCounter("aws.cloudwatch.get_metric_statistics") M_Aws_CloudWatch_ListMetrics = RegCounter("aws.cloudwatch.list_metrics") diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go new file mode 100644 index 00000000000..551464733d9 --- /dev/null +++ b/pkg/services/alerting/notifiers/telegram.go @@ -0,0 +1,86 @@ +package notifiers + +import ( + "fmt" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/metrics" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" +) + +var ( + telegeramApiUrl string = "https://api.telegram.org/bot%s/%s" +) + +func init() { + alerting.RegisterNotifier("telegram", NewTelegramNotifier) +} + +type TelegramNotifier struct { + NotifierBase + BotToken string + ChatID string + log log.Logger +} + +func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error) { + if model.Settings == nil { + return nil, alerting.ValidationError{Reason: "No Settings Supplied"} + } + + botToken := model.Settings.Get("bottoken").MustString() + chatId := model.Settings.Get("chatid").MustString() + + 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: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + BotToken: botToken, + ChatID: chatId, + log: log.New("alerting.notifier.telegram"), + }, nil +} + +func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error { + this.log.Info("Sending alert notification to", "bot_token", this.BotToken) + this.log.Info("Sending alert notification to", "chat_id", this.ChatID) + metrics.M_Alerting_Notification_Sent_Telegram.Inc(1) + + bodyJSON := simplejson.New() + + bodyJSON.Set("chat_id", this.ChatID) + bodyJSON.Set("parse_mode", "html") + + message := fmt.Sprintf("%s\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message) + + ruleUrl, err := evalContext.GetRuleUrl() + if err == nil { + message = message + fmt.Sprintf("URL: %s\n", ruleUrl) + } + bodyJSON.Set("text", message) + + url := fmt.Sprintf(telegeramApiUrl, this.BotToken, "sendMessage") + body, _ := bodyJSON.MarshalJSON() + + cmd := &m.SendWebhookSync{ + Url: url, + Body: string(body), + HttpMethod: "POST", + } + + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + this.log.Error("Failed to send webhook", "error", err, "webhook", this.Name) + return err + } + + return nil +} diff --git a/pkg/services/alerting/notifiers/telegram_test.go b/pkg/services/alerting/notifiers/telegram_test.go new file mode 100644 index 00000000000..3e8066e273b --- /dev/null +++ b/pkg/services/alerting/notifiers/telegram_test.go @@ -0,0 +1,55 @@ +package notifiers + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" + . "github.com/smartystreets/goconvey/convey" +) + +func TestTelegramNotifier(t *testing.T) { + Convey("Telegram notifier tests", t, func() { + + Convey("Parsing alert notification from settings", func() { + Convey("empty settings should return error", func() { + json := `{ }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "telegram_testing", + Type: "telegram", + Settings: settingsJSON, + } + + _, err := NewTelegramNotifier(model) + So(err, ShouldNotBeNil) + }) + + Convey("settings should trigger incident", func() { + json := ` + { + "bottoken": "abcdefgh0123456789", + "chatid": "-1234567890" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "telegram_testing", + Type: "telegram", + Settings: settingsJSON, + } + + not, err := NewTelegramNotifier(model) + telegramNotifier := not.(*TelegramNotifier) + + So(err, ShouldBeNil) + So(telegramNotifier.Name, ShouldEqual, "telegram_testing") + So(telegramNotifier.Type, ShouldEqual, "telegram") + So(telegramNotifier.BotToken, ShouldEqual, "abcdefgh0123456789") + So(telegramNotifier.ChatID, ShouldEqual, "-1234567890") + }) + + }) + }) +} diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index b5076efd792..33f8e1a0f87 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -19,7 +19,7 @@
Type
-
@@ -139,6 +139,28 @@ +
+

Telegram API settings

+
+ BOT API Token + +
+
+ Chat ID + + + + Integer Telegram Chat Identifier + +
+
+
From b8f559aecb49827402ff0a585823a94b8d14457e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 6 Jan 2017 12:04:25 +0100 Subject: [PATCH 2/2] feat(plugins): made notifiers more pluggable and easier to support many of them, new ones can now be added without modifying any existing file, #7162 --- pkg/api/alerting.go | 4 + pkg/api/api.go | 1 + pkg/api/index.go | 2 +- pkg/services/alerting/notifier.go | 28 +++- pkg/services/alerting/notifiers/email.go | 16 +- pkg/services/alerting/notifiers/opsgenie.go | 23 ++- pkg/services/alerting/notifiers/pagerduty.go | 23 ++- pkg/services/alerting/notifiers/slack.go | 37 ++++- pkg/services/alerting/notifiers/telegram.go | 29 +++- pkg/services/alerting/notifiers/victorops.go | 14 +- pkg/services/alerting/notifiers/webhook.go | 30 +++- .../alerting/notification_edit_ctrl.ts | 69 +++++---- .../alerting/partials/notification_edit.html | 139 +----------------- .../alerting/partials/notifications_list.html | 4 +- 14 files changed, 245 insertions(+), 174 deletions(-) diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 7b48a70dcf4..143c72106fe 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -172,6 +172,10 @@ func DelAlert(c *middleware.Context) Response { return Json(200, resp) } +func GetAlertNotifiers(c *middleware.Context) Response { + return Json(200, alerting.GetNotifiers()) +} + func GetAlertNotifications(c *middleware.Context) Response { query := &models.GetAllAlertNotificationsQuery{OrgId: c.OrgId} diff --git a/pkg/api/api.go b/pkg/api/api.go index e931c5de601..ef550838c56 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -262,6 +262,7 @@ func Register(r *macaron.Macaron) { }) r.Get("/alert-notifications", wrap(GetAlertNotifications)) + r.Get("/alert-notifiers", wrap(GetAlertNotifiers)) r.Group("/alert-notifications", func() { r.Post("/test", bind(dtos.NotificationTestCommand{}), wrap(NotificationTest)) diff --git a/pkg/api/index.go b/pkg/api/index.go index 5bc4344a8ba..715baefae78 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -106,7 +106,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR { alertChildNavs := []*dtos.NavLink{ {Text: "Alert List", Url: setting.AppSubUrl + "/alerting/list"}, - {Text: "Notifications", Url: setting.AppSubUrl + "/alerting/notifications"}, + {Text: "Notification channels", Url: setting.AppSubUrl + "/alerting/notifications"}, } data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 0775b48f2b0..7e213058cd0 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -13,6 +13,14 @@ import ( m "github.com/grafana/grafana/pkg/models" ) +type NotifierPlugin struct { + Type string `json:"type"` + Name string `json:"name"` + Description string `json:"description"` + OptionsTemplate string `json:"optionsTemplate"` + Factory NotifierFactory `json:"-"` +} + type RootNotifier struct { log log.Logger } @@ -130,12 +138,12 @@ func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64, contex } func (n *RootNotifier) createNotifierFor(model *m.AlertNotification) (Notifier, error) { - factory, found := notifierFactories[model.Type] + notifierPlugin, found := notifierFactories[model.Type] if !found { return nil, errors.New("Unsupported notification type") } - return factory(model) + return notifierPlugin.Factory(model) } func shouldUseNotification(notifier Notifier, context *EvalContext) bool { @@ -152,8 +160,18 @@ func shouldUseNotification(notifier Notifier, context *EvalContext) bool { type NotifierFactory func(notification *m.AlertNotification) (Notifier, error) -var notifierFactories map[string]NotifierFactory = make(map[string]NotifierFactory) +var notifierFactories map[string]*NotifierPlugin = make(map[string]*NotifierPlugin) -func RegisterNotifier(typeName string, factory NotifierFactory) { - notifierFactories[typeName] = factory +func RegisterNotifier(plugin *NotifierPlugin) { + notifierFactories[plugin.Type] = plugin +} + +func GetNotifiers() []*NotifierPlugin { + list := make([]*NotifierPlugin, 0) + + for _, value := range notifierFactories { + list = append(list, value) + } + + return list } diff --git a/pkg/services/alerting/notifiers/email.go b/pkg/services/alerting/notifiers/email.go index 8cd4273e6be..4058d3860b5 100644 --- a/pkg/services/alerting/notifiers/email.go +++ b/pkg/services/alerting/notifiers/email.go @@ -13,7 +13,21 @@ import ( ) func init() { - alerting.RegisterNotifier("email", NewEmailNotifier) + alerting.RegisterNotifier(&alerting.NotifierPlugin{ + Type: "email", + Name: "Email", + Description: "Sends notifications using Grafana server configured STMP settings", + Factory: NewEmailNotifier, + OptionsTemplate: ` +

Email addresses

+
+ +
+
+ You can enter multiple email addresses using a ";" separator +
+ `, + }) } type EmailNotifier struct { diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go index a93b650d494..742aeb922b6 100644 --- a/pkg/services/alerting/notifiers/opsgenie.go +++ b/pkg/services/alerting/notifiers/opsgenie.go @@ -13,7 +13,28 @@ import ( ) func init() { - alerting.RegisterNotifier("opsgenie", NewOpsGenieNotifier) + alerting.RegisterNotifier(&alerting.NotifierPlugin{ + Type: "opsgenie", + Name: "OpsGenie", + Description: "Sends notifications to OpsGenie", + Factory: NewOpsGenieNotifier, + OptionsTemplate: ` +

OpsGenie settings

+
+ API Key + +
+
+ + +
+ `, + }) } var ( diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go index e94bd5ebeaa..0c98ab00e20 100644 --- a/pkg/services/alerting/notifiers/pagerduty.go +++ b/pkg/services/alerting/notifiers/pagerduty.go @@ -12,7 +12,28 @@ import ( ) func init() { - alerting.RegisterNotifier("pagerduty", NewPagerdutyNotifier) + alerting.RegisterNotifier(&alerting.NotifierPlugin{ + Type: "pagerduty", + Name: "PagerDuty", + Description: "Sends notifications to PagerDuty", + Factory: NewPagerdutyNotifier, + OptionsTemplate: ` +

PagerDuty settings

+
+ Integration Key + +
+
+ + +
+ `, + }) } var ( diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index 2666662e32c..7b2bfd09c9a 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -13,7 +13,42 @@ import ( ) func init() { - alerting.RegisterNotifier("slack", NewSlackNotifier) + alerting.RegisterNotifier(&alerting.NotifierPlugin{ + Type: "slack", + Name: "Slack", + Description: "Sends notifications using Grafana server configured STMP settings", + Factory: NewSlackNotifier, + OptionsTemplate: ` +

Slack settings

+
+ Url + +
+
+ Recipient + + + + Override default channel or user, use #channel-name or @username + +
+
+ Mention + + + + Mention a user or a group using @ when notifying in a channel + +
+ `, + }) + } func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) { diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index 551464733d9..dd3011a49ec 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -16,7 +16,34 @@ var ( ) func init() { - alerting.RegisterNotifier("telegram", NewTelegramNotifier) + alerting.RegisterNotifier(&alerting.NotifierPlugin{ + Type: "telegram", + Name: "Telegram", + Description: "Sends notifications to Telegram", + Factory: NewOpsGenieNotifier, + OptionsTemplate: ` +

Telegram API settings

+
+ BOT API Token + +
+
+ Chat ID + + + + Integer Telegram Chat Identifier + +
+ `, + }) + } type TelegramNotifier struct { diff --git a/pkg/services/alerting/notifiers/victorops.go b/pkg/services/alerting/notifiers/victorops.go index 9afa1efb281..a4e34a40b8a 100644 --- a/pkg/services/alerting/notifiers/victorops.go +++ b/pkg/services/alerting/notifiers/victorops.go @@ -16,7 +16,19 @@ import ( const AlertStateCritical = "CRITICAL" func init() { - alerting.RegisterNotifier("victorops", NewVictoropsNotifier) + alerting.RegisterNotifier(&alerting.NotifierPlugin{ + Type: "victorops", + Name: "VictorOps", + Description: "Sends notifications to VictorOps", + Factory: NewVictoropsNotifier, + OptionsTemplate: ` +

VictorOps settings

+
+ Url + +
+ `, + }) } // NewVictoropsNotifier creates an instance of VictoropsNotifier that diff --git a/pkg/services/alerting/notifiers/webhook.go b/pkg/services/alerting/notifiers/webhook.go index 0603cf45084..25ed22a65f5 100644 --- a/pkg/services/alerting/notifiers/webhook.go +++ b/pkg/services/alerting/notifiers/webhook.go @@ -10,7 +10,35 @@ import ( ) func init() { - alerting.RegisterNotifier("webhook", NewWebHookNotifier) + alerting.RegisterNotifier(&alerting.NotifierPlugin{ + Type: "webhook", + Name: "webhook", + Description: "Sends HTTP POST request to a URL", + Factory: NewWebHookNotifier, + OptionsTemplate: ` +

Webhook settings

+
+ Url + +
+
+ Http Method +
+ +
+
+
+ Username + +
+
+ Password + +
+ `, + }) + } func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) { diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index 77e67d96d86..ed05264cdb3 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/public/app/features/alerting/notification_edit_ctrl.ts @@ -2,40 +2,48 @@ import angular from 'angular'; import _ from 'lodash'; -import coreModule from '../../core/core_module'; import config from 'app/core/config'; +import {appEvents, coreModule} from 'app/core/core'; export class AlertNotificationEditCtrl { - model: any; theForm: any; testSeverity: string = "critical"; + notifiers: any; + notifierTemplateId: string; + + model: any; + defaults: any = { + type: 'email', + settings: { + httpMethod: 'POST', + autoResolve: true, + }, + isDefault: false + }; /** @ngInject */ - constructor(private $routeParams, private backendSrv, private $scope, private $location) { - if ($routeParams.id) { - this.loadNotification($routeParams.id); - } else { - this.model = { - type: 'email', - settings: { - httpMethod: 'POST', - autoResolve: true, - }, - isDefault: false - }; - } - } + constructor(private $routeParams, private backendSrv, private $location, private $templateCache) { + this.backendSrv.get(`/api/alert-notifiers`).then(notifiers => { + this.notifiers = notifiers; - loadNotification(id) { - this.backendSrv.get(`/api/alert-notifications/${id}`).then(result => { - this.model = result; + // add option templates + for (let notifier of this.notifiers) { + this.$templateCache.put(this.getNotifierTemplateId(notifier.type), notifier.optionsTemplate); + } + + if (!this.$routeParams.id) { + return this.model; + } + + return this.backendSrv.get(`/api/alert-notifications/${this.$routeParams.id}`).then(result => { + return result; + }); + }).then(model => { + this.model = model; + this.notifierTemplateId = this.getNotifierTemplateId(this.model.type); }); } - isNew() { - return this.model.id === undefined; - } - save() { if (!this.theForm.$valid) { return; @@ -44,18 +52,23 @@ export class AlertNotificationEditCtrl { if (this.model.id) { this.backendSrv.put(`/api/alert-notifications/${this.model.id}`, this.model).then(res => { this.model = res; - this.$scope.appEvent('alert-success', ['Notification updated', '']); + appEvents.emit('alert-success', ['Notification updated', '']); }); } else { this.backendSrv.post(`/api/alert-notifications`, this.model).then(res => { - this.$scope.appEvent('alert-success', ['Notification created', '']); + appEvents.emit('alert-success', ['Notification created', '']); this.$location.path('alerting/notifications'); }); } } + getNotifierTemplateId(type) { + return `notifier-options-${type}`; + } + typeChanged() { this.model.settings = {}; + this.notifierTemplateId = this.getNotifierTemplateId(this.model.type); } testNotification() { @@ -70,9 +83,9 @@ export class AlertNotificationEditCtrl { }; this.backendSrv.post(`/api/alert-notifications/test`, payload) - .then(res => { - this.$scope.appEvent('alert-succes', ['Test notification sent', '']); - }); + .then(res => { + appEvents.emit('alert-succes', ['Test notification sent', '']); + }); } } diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index 33f8e1a0f87..dfdfb115f4b 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -1,16 +1,17 @@ - - Notifications + + Notification channels -
+
-
+
Name @@ -19,7 +20,7 @@
Type
-
@@ -34,131 +35,7 @@
-
-

Webhook settings

-
- Url - -
-
- Http Method -
- -
-
-
- Username - -
-
- Password - -
-
- -
-

Slack settings

-
- Url - -
-
- Recipient - - - - Override default channel or user, use #channel-name or @username - -
-
- Mention - - - - Mention a user or a group using @ when notifying in a channel - -
-
- -
-

VictorOps settings

-
- Url - -
-
- -
-

Email addresses

-
- -
-
- You can enter multiple email addresses using a ";" separator -
-
- -
-

Pagerduty settings

-
- Integration Key - -
-
- - -
-
- -
-

OpsGenie settings

-
- API Key - -
-
- - -
-
- -
-

Telegram API settings

-
- BOT API Token - -
-
- Chat ID - - - - Integer Telegram Chat Identifier - -
+
diff --git a/public/app/features/alerting/partials/notifications_list.html b/public/app/features/alerting/partials/notifications_list.html index 8777d6f6a1b..32ac4a44476 100644 --- a/public/app/features/alerting/partials/notifications_list.html +++ b/public/app/features/alerting/partials/notifications_list.html @@ -3,10 +3,10 @@