mirror of https://github.com/grafana/grafana
Pushover is a service for getting real-time notifications on your mobile devices and desktop computers: https://pushover.netpull/7526/head
parent
900230890f
commit
f069aae576
@ -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 |
||||||
|
} |
@ -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") |
||||||
|
}) |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
Loading…
Reference in new issue