| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299 |
- package notifiers
- import (
- "bytes"
- "fmt"
- "io"
- "mime/multipart"
- "os"
- "strconv"
- "github.com/grafana/grafana/pkg/bus"
- "github.com/grafana/grafana/pkg/infra/log"
- "github.com/grafana/grafana/pkg/models"
- "github.com/grafana/grafana/pkg/services/alerting"
- )
- const pushoverEndpoint = "https://api.pushover.net/1/messages.json"
- func init() {
- sounds := `
- 'default',
- 'pushover',
- 'bike',
- 'bugle',
- 'cashregister',
- 'classical',
- 'cosmic',
- 'falling',
- 'gamelan',
- 'incoming',
- 'intermission',
- 'magic',
- 'mechanical',
- 'pianobar',
- 'siren',
- 'spacealarm',
- 'tugboat',
- 'alien',
- 'climb',
- 'persistent',
- 'echo',
- 'updown',
- 'none'`
- 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">Alerting sound</span>
- <select class="gf-form-input max-width-14" ng-model="ctrl.model.settings.sound" ng-options="s for s in [
- ` + sounds + `
- ]" ng-init="ctrl.model.settings.sound=ctrl.model.settings.sound||'default'"></select>
- </div>
- <div class="gf-form">
- <span class="gf-form-label width-10">OK sound</span>
- <select class="gf-form-input max-width-14" ng-model="ctrl.model.settings.okSound" ng-options="s for s in [
- ` + sounds + `
- ]" ng-init="ctrl.model.settings.okSound=ctrl.model.settings.okSound||'default'"></select>
- </div>
- `,
- })
- }
- // NewPushoverNotifier is the constructor for the Pushover Notifier
- func NewPushoverNotifier(model *models.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())
- alertingSound := model.Settings.Get("sound").MustString()
- okSound := model.Settings.Get("okSound").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,
- AlertingSound: alertingSound,
- OkSound: okSound,
- Upload: uploadImage,
- log: log.New("alerting.notifier.pushover"),
- }, nil
- }
- // PushoverNotifier is responsible for sending
- // alert notifications to Pushover
- type PushoverNotifier struct {
- NotifierBase
- UserKey string
- APIToken string
- Priority int
- Retry int
- Expire int
- Device string
- AlertingSound string
- OkSound string
- Upload bool
- log log.Logger
- }
- // Notify sends a alert notification to Pushover
- func (pn *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
- ruleURL, err := evalContext.GetRuleURL()
- if err != nil {
- pn.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 := pn.genPushoverBody(evalContext, message, ruleURL)
- if err != nil {
- pn.log.Error("Failed to generate body for pushover", "error", err)
- return err
- }
- cmd := &models.SendWebhookSync{
- Url: pushoverEndpoint,
- HttpMethod: "POST",
- HttpHeader: headers,
- Body: uploadBody.String(),
- }
- if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
- pn.log.Error("Failed to send pushover notification", "error", err, "webhook", pn.Name)
- return err
- }
- return nil
- }
- func (pn *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 pn.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", pn.UserKey)
- if err != nil {
- return nil, b, err
- }
- // Add the api token
- err = w.WriteField("token", pn.APIToken)
- if err != nil {
- return nil, b, err
- }
- // Add priority
- err = w.WriteField("priority", strconv.Itoa(pn.Priority))
- if err != nil {
- return nil, b, err
- }
- if pn.Priority == 2 {
- err = w.WriteField("retry", strconv.Itoa(pn.Retry))
- if err != nil {
- return nil, b, err
- }
- err = w.WriteField("expire", strconv.Itoa(pn.Expire))
- if err != nil {
- return nil, b, err
- }
- }
- // Add device
- if pn.Device != "" {
- err = w.WriteField("device", pn.Device)
- if err != nil {
- return nil, b, err
- }
- }
- // Add sound
- sound := pn.AlertingSound
- if evalContext.Rule.State == models.AlertStateOK {
- sound = pn.OkSound
- }
- if sound != "default" {
- err = w.WriteField("sound", 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
- }
|