mirror of
https://github.com/grafana/grafana.git
synced 2024-11-29 20:24:18 -06:00
ec0fe11939
Use native pushover native attachment support to deliver images
280 lines
7.8 KiB
Go
280 lines
7.8 KiB
Go
package notifiers
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"os"
|
|
"strconv"
|
|
|
|
"github.com/grafana/grafana/pkg/bus"
|
|
"github.com/grafana/grafana/pkg/log"
|
|
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()
|
|
uploadImage := model.Settings.Get("uploadImage").MustBool(true)
|
|
|
|
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),
|
|
UserKey: userKey,
|
|
ApiToken: apiToken,
|
|
Priority: priority,
|
|
Retry: retry,
|
|
Expire: expire,
|
|
Device: device,
|
|
Sound: sound,
|
|
Upload: uploadImage,
|
|
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
|
|
Upload bool
|
|
log log.Logger
|
|
}
|
|
|
|
func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
|
|
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())
|
|
}
|
|
|
|
if message == "" {
|
|
message = "Notification message missing (Set a notification message to replace this text.)"
|
|
}
|
|
|
|
headers, uploadBody, err := this.genPushoverBody(evalContext, message, ruleUrl)
|
|
if err != nil {
|
|
this.log.Error("Failed to generate body for pushover", "error", err)
|
|
return err
|
|
}
|
|
|
|
cmd := &m.SendWebhookSync{
|
|
Url: PUSHOVER_ENDPOINT,
|
|
HttpMethod: "POST",
|
|
HttpHeader: headers,
|
|
Body: uploadBody.String(),
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (this *PushoverNotifier) genPushoverBody(evalContext *alerting.EvalContext, message string, ruleUrl string) (map[string]string, bytes.Buffer, error) {
|
|
var b bytes.Buffer
|
|
var err error
|
|
w := multipart.NewWriter(&b)
|
|
|
|
// Add image only if requested and available
|
|
if this.Upload && evalContext.ImageOnDiskPath != "" {
|
|
f, err := os.Open(evalContext.ImageOnDiskPath)
|
|
if err != nil {
|
|
return nil, b, err
|
|
}
|
|
defer f.Close()
|
|
|
|
fw, err := w.CreateFormFile("attachment", evalContext.ImageOnDiskPath)
|
|
if err != nil {
|
|
return nil, b, err
|
|
}
|
|
|
|
_, err = io.Copy(fw, f)
|
|
if err != nil {
|
|
return nil, b, err
|
|
}
|
|
}
|
|
|
|
// Add the user token
|
|
err = w.WriteField("user", this.UserKey)
|
|
if err != nil {
|
|
return nil, b, err
|
|
}
|
|
|
|
// Add the api token
|
|
err = w.WriteField("token", this.ApiToken)
|
|
if err != nil {
|
|
return nil, b, err
|
|
}
|
|
|
|
// Add priority
|
|
err = w.WriteField("priority", strconv.Itoa(this.Priority))
|
|
if err != nil {
|
|
return nil, b, err
|
|
}
|
|
|
|
if this.Priority == 2 {
|
|
err = w.WriteField("retry", strconv.Itoa(this.Retry))
|
|
if err != nil {
|
|
return nil, b, err
|
|
}
|
|
|
|
err = w.WriteField("expire", strconv.Itoa(this.Expire))
|
|
if err != nil {
|
|
return nil, b, err
|
|
}
|
|
}
|
|
|
|
// Add device
|
|
if this.Device != "" {
|
|
err = w.WriteField("device", this.Device)
|
|
if err != nil {
|
|
return nil, b, err
|
|
}
|
|
}
|
|
|
|
// Add sound
|
|
if this.Sound != "default" {
|
|
err = w.WriteField("sound", this.Sound)
|
|
if err != nil {
|
|
return nil, b, err
|
|
}
|
|
}
|
|
|
|
// Add title
|
|
err = w.WriteField("title", evalContext.GetNotificationTitle())
|
|
if err != nil {
|
|
return nil, b, err
|
|
}
|
|
|
|
// Add URL
|
|
err = w.WriteField("url", ruleUrl)
|
|
if err != nil {
|
|
return nil, b, err
|
|
}
|
|
// Add URL title
|
|
err = w.WriteField("url_title", "Show dashboard with alert")
|
|
if err != nil {
|
|
return nil, b, err
|
|
}
|
|
|
|
// Add message
|
|
err = w.WriteField("message", message)
|
|
if err != nil {
|
|
return nil, b, err
|
|
}
|
|
|
|
// Mark as html message
|
|
err = w.WriteField("html", "1")
|
|
if err != nil {
|
|
return nil, b, err
|
|
}
|
|
|
|
w.Close()
|
|
headers := map[string]string{
|
|
"Content-Type": w.FormDataContentType(),
|
|
}
|
|
return headers, b, nil
|
|
}
|