Prechádzať zdrojové kódy

Add Pushover alert notifications

Pushover is a service for getting real-time
notifications on your mobile devices and desktop
computers: https://pushover.net
Jörg Thalheim 9 rokov pred
rodič
commit
f069aae576

+ 2 - 0
pkg/metrics/metrics.go

@@ -50,6 +50,7 @@ var (
 	M_Alerting_Notification_Sent_OpsGenie  Counter
 	M_Alerting_Notification_Sent_Telegram  Counter
 	M_Alerting_Notification_Sent_Sensu     Counter
+	M_Alerting_Notification_Sent_Pushover  Counter
 	M_Aws_CloudWatch_GetMetricStatistics   Counter
 	M_Aws_CloudWatch_ListMetrics           Counter
 
@@ -120,6 +121,7 @@ func initMetricVars(settings *MetricSettings) {
 	M_Alerting_Notification_Sent_Telegram = RegCounter("alerting.notifications_sent", "type", "telegram")
 	M_Alerting_Notification_Sent_Sensu = RegCounter("alerting.notifications_sent", "type", "sensu")
 	M_Alerting_Notification_Sent_LINE = RegCounter("alerting.notifications_sent", "type", "LINE")
+	M_Alerting_Notification_Sent_Pushover = RegCounter("alerting.notifications_sent", "type", "pushover")
 
 	M_Aws_CloudWatch_GetMetricStatistics = RegCounter("aws.cloudwatch.get_metric_statistics")
 	M_Aws_CloudWatch_ListMetrics = RegCounter("aws.cloudwatch.list_metrics")

+ 177 - 0
pkg/services/alerting/notifiers/pushover.go

@@ -0,0 +1,177 @@
+package notifiers
+
+import (
+	"fmt"
+	"net/url"
+	"strconv"
+
+	"github.com/grafana/grafana/pkg/bus"
+	"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"
+)
+
+const PUSHOVER_ENDPOINT = "https://api.pushover.net/1/messages.json"
+
+func init() {
+	alerting.RegisterNotifier(&alerting.NotifierPlugin{
+		Type:        "pushover",
+		Name:        "Pushover",
+		Description: "Sends HTTP POST request to the Pushover API",
+		Factory:     NewPushoverNotifier,
+		OptionsTemplate: `
+      <h3 class="page-heading">Pushover settings</h3>
+      <div class="gf-form">
+        <span class="gf-form-label width-10">API Token</span>
+        <input type="text" class="gf-form-input" required placeholder="Application token" ng-model="ctrl.model.settings.apiToken"></input>
+      </div>
+      <div class="gf-form">
+        <span class="gf-form-label width-10">User key(s)</span>
+        <input type="text" class="gf-form-input" required placeholder="comma-separated list" ng-model="ctrl.model.settings.userKey"></input>
+      </div>
+      <div class="gf-form">
+        <span class="gf-form-label width-10">Device(s) (optional)</span>
+        <input type="text" class="gf-form-input" placeholder="comma-separated list; leave empty to send to all devices" ng-model="ctrl.model.settings.device"></input>
+      </div>
+      <div class="gf-form">
+        <span class="gf-form-label width-10">Priority</span>
+        <select class="gf-form-input max-width-14" ng-model="ctrl.model.settings.priority" ng-options="v as k for (k, v) in {
+          Emergency: '2',
+          High:      '1',
+          Normal:    '0',
+          Low:      '-1',
+          Lowest:   '-2'
+        }" ng-init="ctrl.model.settings.priority=ctrl.model.settings.priority||'0'"></select>
+      </div>
+      <div class="gf-form" ng-show="ctrl.model.settings.priority == '2'">
+        <span class="gf-form-label width-10">Retry</span>
+        <input type="text" class="gf-form-input max-width-14" ng-required="ctrl.model.settings.priority == '2'" placeholder="minimum 30 seconds" ng-model="ctrl.model.settings.retry" ng-init="ctrl.model.settings.retry=ctrl.model.settings.retry||'60'></input>
+      </div>
+      <div class="gf-form" ng-show="ctrl.model.settings.priority == '2'">
+        <span class="gf-form-label width-10">Expire</span>
+        <input type="text" class="gf-form-input max-width-14" ng-required="ctrl.model.settings.priority == '2'" placeholder="maximum 86400 seconds" ng-model="ctrl.model.settings.expire" ng-init="ctrl.model.settings.expire=ctrl.model.settings.expire||'3600'"></input>
+      </div>
+      <div class="gf-form">
+        <span class="gf-form-label width-10">Sound</span>
+        <select class="gf-form-input max-width-14" ng-model="ctrl.model.settings.sound" ng-options="s for s in [
+          'default',
+          'pushover',
+          'bike',
+          'bugle',
+          'cashregister',
+          'classical',
+          'cosmic',
+          'falling',
+          'gamelan',
+          'incoming',
+          'intermission',
+          'magic',
+          'mechanical',
+          'pianobar',
+          'siren',
+          'spacealarm',
+          'tugboat',
+          'alien',
+          'climb',
+          'persistent',
+          'echo',
+          'updown',
+          'none'
+        ]" ng-init="ctrl.model.settings.sound=ctrl.model.settings.sound||'default'"></select>
+      </div>
+    `,
+	})
+}
+
+func NewPushoverNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
+	userKey := model.Settings.Get("userKey").MustString()
+	apiToken := model.Settings.Get("apiToken").MustString()
+	device := model.Settings.Get("device").MustString()
+	priority, _ := strconv.Atoi(model.Settings.Get("priority").MustString())
+	retry, _ := strconv.Atoi(model.Settings.Get("retry").MustString())
+	expire, _ := strconv.Atoi(model.Settings.Get("expire").MustString())
+	sound := model.Settings.Get("sound").MustString()
+
+	if userKey == "" {
+		return nil, alerting.ValidationError{Reason: "User key not given"}
+	}
+	if apiToken == "" {
+		return nil, alerting.ValidationError{Reason: "API token not given"}
+	}
+	return &PushoverNotifier{
+		NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
+		UserKey:      userKey,
+		ApiToken:     apiToken,
+		Priority:     priority,
+		Retry:        retry,
+		Expire:       expire,
+		Device:       device,
+		Sound:        sound,
+		log:          log.New("alerting.notifier.pushover"),
+	}, nil
+}
+
+type PushoverNotifier struct {
+	NotifierBase
+	UserKey  string
+	ApiToken string
+	Priority int
+	Retry    int
+	Expire   int
+	Device   string
+	Sound    string
+	log      log.Logger
+}
+
+func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
+	metrics.M_Alerting_Notification_Sent_Pushover.Inc(1)
+	ruleUrl, err := evalContext.GetRuleUrl()
+	if err != nil {
+		this.log.Error("Failed get rule link", "error", err)
+		return err
+	}
+	message := evalContext.Rule.Message
+	for idx, evt := range evalContext.EvalMatches {
+		message += fmt.Sprintf("\n<b>%s</b>: %v", evt.Metric, evt.Value)
+		if idx > 4 {
+			break
+		}
+	}
+	if evalContext.Error != nil {
+		message += fmt.Sprintf("\n<b>Error message</b> %s", evalContext.Error.Error())
+	}
+	q := url.Values{}
+	q.Add("user", this.UserKey)
+	q.Add("token", this.ApiToken)
+	q.Add("priority", strconv.Itoa(this.Priority))
+	if this.Priority == 2 {
+		q.Add("retry", strconv.Itoa(this.Retry))
+		q.Add("expire", strconv.Itoa(this.Expire))
+	}
+	if this.Device != "" {
+		q.Add("device", this.Device)
+	}
+	if this.Sound != "default" {
+		q.Add("sound", this.Sound)
+	}
+	q.Add("title", evalContext.GetNotificationTitle())
+	q.Add("url", ruleUrl)
+	q.Add("url_title", "Show dashboard with alert")
+	q.Add("message", message)
+	q.Add("html", "1")
+
+	cmd := &m.SendWebhookSync{
+		Url:        PUSHOVER_ENDPOINT,
+		HttpMethod: "POST",
+		HttpHeader: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
+		Body:       q.Encode(),
+	}
+
+	if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
+		this.log.Error("Failed to send pushover notification", "error", err, "webhook", this.Name)
+		return err
+	}
+
+	return nil
+}

+ 58 - 0
pkg/services/alerting/notifiers/pushover_test.go

@@ -0,0 +1,58 @@
+package notifiers
+
+import (
+	"testing"
+
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	m "github.com/grafana/grafana/pkg/models"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestPushoverNotifier(t *testing.T) {
+	Convey("Pushover 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:     "Pushover",
+					Type:     "pushover",
+					Settings: settingsJSON,
+				}
+
+				_, err := NewPushoverNotifier(model)
+				So(err, ShouldNotBeNil)
+			})
+
+			Convey("from settings", func() {
+				json := `
+				{
+					"apiToken": "4SrUFQL4A5V5TQ1z5Pg9nxHXPXSTve",
+					"userKey": "tzNZYf36y0ohWwXo4XoUrB61rz1A4o",
+					"priority": "1",
+					"sound": "pushover"
+				}`
+
+				settingsJSON, _ := simplejson.NewJson([]byte(json))
+				model := &m.AlertNotification{
+					Name:     "Pushover",
+					Type:     "pushover",
+					Settings: settingsJSON,
+				}
+
+				not, err := NewPushoverNotifier(model)
+				pushoverNotifier := not.(*PushoverNotifier)
+
+				So(err, ShouldBeNil)
+				So(pushoverNotifier.Name, ShouldEqual, "Pushover")
+				So(pushoverNotifier.Type, ShouldEqual, "pushover")
+				So(pushoverNotifier.ApiToken, ShouldEqual, "4SrUFQL4A5V5TQ1z5Pg9nxHXPXSTve")
+				So(pushoverNotifier.UserKey, ShouldEqual, "tzNZYf36y0ohWwXo4XoUrB61rz1A4o")
+				So(pushoverNotifier.Priority, ShouldEqual, 1)
+				So(pushoverNotifier.Sound, ShouldEqual, "pushover")
+			})
+		})
+	})
+}

+ 1 - 0
public/app/features/alerting/alert_tab_ctrl.ts

@@ -105,6 +105,7 @@ export class AlertTabCtrl {
       case "pagerduty": return "fa fa-bullhorn";
       case "opsgenie": return "fa fa-bell";
       case "hipchat": return "fa fa-mail-forward";
+      case "pushover": return "fa fa-mobile";
     }
   }