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/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/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
new file mode 100644
index 00000000000..dd3011a49ec
--- /dev/null
+++ b/pkg/services/alerting/notifiers/telegram.go
@@ -0,0 +1,113 @@
+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(&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 {
+ 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/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
+
+
+
+
+ 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 b5076efd792..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
-