2016-12-25 14:57:08 -06:00
package notifiers
import (
2019-01-12 21:09:51 -06:00
"bytes"
2016-12-25 14:57:08 -06:00
"fmt"
2019-01-12 21:09:51 -06:00
"io"
"mime/multipart"
"os"
2016-12-25 14:57:08 -06:00
"strconv"
"github.com/grafana/grafana/pkg/bus"
2019-05-13 01:45:54 -05:00
"github.com/grafana/grafana/pkg/infra/log"
2019-05-14 01:15:05 -05:00
"github.com/grafana/grafana/pkg/models"
2016-12-25 14:57:08 -06:00
"github.com/grafana/grafana/pkg/services/alerting"
)
2019-05-20 08:23:06 -05:00
const pushoverEndpoint = "https://api.pushover.net/1/messages.json"
2016-12-25 14:57:08 -06:00
func init ( ) {
2019-04-12 07:57:17 -05:00
sounds := `
' default ' ,
' pushover ' ,
' bike ' ,
' bugle ' ,
' cashregister ' ,
' classical ' ,
' cosmic ' ,
' falling ' ,
' gamelan ' ,
' incoming ' ,
' intermission ' ,
' magic ' ,
' mechanical ' ,
' pianobar ' ,
' siren ' ,
' spacealarm ' ,
' tugboat ' ,
' alien ' ,
' climb ' ,
' persistent ' ,
' echo ' ,
' updown ' ,
' none ' `
2016-12-25 14:57:08 -06:00
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" >
2019-04-12 07:57:17 -05:00
< span class = "gf-form-label width-10" > Alerting sound < / span >
2016-12-25 14:57:08 -06:00
< select class = "gf-form-input max-width-14" ng - model = "ctrl.model.settings.sound" ng - options = " s for s in [
2019-04-12 07:57:17 -05:00
` + sounds + `
2016-12-25 14:57:08 -06:00
] " ng-init=" ctrl . model . settings . sound = ctrl . model . settings . sound || ' default ' " > < / select >
< / div >
2019-04-12 07:57:17 -05:00
< 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 >
2016-12-25 14:57:08 -06:00
` ,
} )
}
2019-05-20 08:23:06 -05:00
// NewPushoverNotifier is the constructor for the Pushover Notifier
2019-05-14 01:15:05 -05:00
func NewPushoverNotifier ( model * models . AlertNotification ) ( alerting . Notifier , error ) {
2016-12-25 14:57:08 -06:00
userKey := model . Settings . Get ( "userKey" ) . MustString ( )
2019-05-20 08:23:06 -05:00
APIToken := model . Settings . Get ( "apiToken" ) . MustString ( )
2016-12-25 14:57:08 -06:00
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 ( ) )
2019-04-12 07:57:17 -05:00
alertingSound := model . Settings . Get ( "sound" ) . MustString ( )
okSound := model . Settings . Get ( "okSound" ) . MustString ( )
2019-01-12 21:09:51 -06:00
uploadImage := model . Settings . Get ( "uploadImage" ) . MustBool ( true )
2016-12-25 14:57:08 -06:00
if userKey == "" {
return nil , alerting . ValidationError { Reason : "User key not given" }
}
2019-05-20 08:23:06 -05:00
if APIToken == "" {
2016-12-25 14:57:08 -06:00
return nil , alerting . ValidationError { Reason : "API token not given" }
}
return & PushoverNotifier {
2019-04-12 07:57:17 -05:00
NotifierBase : NewNotifierBase ( model ) ,
UserKey : userKey ,
2019-05-20 08:23:06 -05:00
APIToken : APIToken ,
2019-04-12 07:57:17 -05:00
Priority : priority ,
Retry : retry ,
Expire : expire ,
Device : device ,
AlertingSound : alertingSound ,
OkSound : okSound ,
Upload : uploadImage ,
log : log . New ( "alerting.notifier.pushover" ) ,
2016-12-25 14:57:08 -06:00
} , nil
}
2019-05-20 08:23:06 -05:00
// PushoverNotifier is responsible for sending
// alert notifications to Pushover
2016-12-25 14:57:08 -06:00
type PushoverNotifier struct {
NotifierBase
2019-04-12 07:57:17 -05:00
UserKey string
2019-05-20 08:23:06 -05:00
APIToken string
2019-04-12 07:57:17 -05:00
Priority int
Retry int
Expire int
Device string
AlertingSound string
OkSound string
Upload bool
log log . Logger
2016-12-25 14:57:08 -06:00
}
2019-05-20 08:23:06 -05:00
// Notify sends a alert notification to Pushover
func ( pn * PushoverNotifier ) Notify ( evalContext * alerting . EvalContext ) error {
2019-06-03 03:25:58 -05:00
ruleURL , err := evalContext . GetRuleURL ( )
2016-12-25 14:57:08 -06:00
if err != nil {
2019-05-20 08:23:06 -05:00
pn . log . Error ( "Failed get rule link" , "error" , err )
2016-12-25 14:57:08 -06:00
return err
}
2017-12-11 14:51:46 -06:00
2016-12-25 14:57:08 -06:00
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 {
2017-04-10 02:17:37 -05:00
message += fmt . Sprintf ( "\n<b>Error message:</b> %s" , evalContext . Error . Error ( ) )
2016-12-25 14:57:08 -06:00
}
2019-01-12 21:09:51 -06:00
2017-12-11 14:51:46 -06:00
if message == "" {
2017-12-19 07:31:14 -06:00
message = "Notification message missing (Set a notification message to replace this text.)"
2017-12-11 14:51:46 -06:00
}
2017-04-10 02:17:37 -05:00
2019-05-20 08:23:06 -05:00
headers , uploadBody , err := pn . genPushoverBody ( evalContext , message , ruleURL )
2019-01-12 21:09:51 -06:00
if err != nil {
2019-05-20 08:23:06 -05:00
pn . log . Error ( "Failed to generate body for pushover" , "error" , err )
2019-01-12 21:09:51 -06:00
return err
2016-12-25 14:57:08 -06:00
}
2019-05-14 01:15:05 -05:00
cmd := & models . SendWebhookSync {
2019-05-20 08:23:06 -05:00
Url : pushoverEndpoint ,
2016-12-25 14:57:08 -06:00
HttpMethod : "POST" ,
2019-01-12 21:09:51 -06:00
HttpHeader : headers ,
Body : uploadBody . String ( ) ,
2016-12-25 14:57:08 -06:00
}
if err := bus . DispatchCtx ( evalContext . Ctx , cmd ) ; err != nil {
2019-05-20 08:23:06 -05:00
pn . log . Error ( "Failed to send pushover notification" , "error" , err , "webhook" , pn . Name )
2016-12-25 14:57:08 -06:00
return err
}
return nil
}
2019-01-12 21:09:51 -06:00
2019-05-20 08:23:06 -05:00
func ( pn * PushoverNotifier ) genPushoverBody ( evalContext * alerting . EvalContext , message string , ruleURL string ) ( map [ string ] string , bytes . Buffer , error ) {
2019-01-12 21:09:51 -06:00
var b bytes . Buffer
var err error
w := multipart . NewWriter ( & b )
// Add image only if requested and available
2019-05-20 08:23:06 -05:00
if pn . Upload && evalContext . ImageOnDiskPath != "" {
2019-01-12 21:09:51 -06:00
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
2019-05-20 08:23:06 -05:00
err = w . WriteField ( "user" , pn . UserKey )
2019-01-12 21:09:51 -06:00
if err != nil {
return nil , b , err
}
// Add the api token
2019-05-20 08:23:06 -05:00
err = w . WriteField ( "token" , pn . APIToken )
2019-01-12 21:09:51 -06:00
if err != nil {
return nil , b , err
}
// Add priority
2019-05-20 08:23:06 -05:00
err = w . WriteField ( "priority" , strconv . Itoa ( pn . Priority ) )
2019-01-12 21:09:51 -06:00
if err != nil {
return nil , b , err
}
2019-05-20 08:23:06 -05:00
if pn . Priority == 2 {
err = w . WriteField ( "retry" , strconv . Itoa ( pn . Retry ) )
2019-01-12 21:09:51 -06:00
if err != nil {
return nil , b , err
}
2019-05-20 08:23:06 -05:00
err = w . WriteField ( "expire" , strconv . Itoa ( pn . Expire ) )
2019-01-12 21:09:51 -06:00
if err != nil {
return nil , b , err
}
}
// Add device
2019-05-20 08:23:06 -05:00
if pn . Device != "" {
err = w . WriteField ( "device" , pn . Device )
2019-01-12 21:09:51 -06:00
if err != nil {
return nil , b , err
}
}
// Add sound
2019-05-20 08:23:06 -05:00
sound := pn . AlertingSound
2019-05-14 01:15:05 -05:00
if evalContext . Rule . State == models . AlertStateOK {
2019-05-20 08:23:06 -05:00
sound = pn . OkSound
2019-04-12 07:57:17 -05:00
}
if sound != "default" {
err = w . WriteField ( "sound" , sound )
2019-01-12 21:09:51 -06:00
if err != nil {
return nil , b , err
}
}
// Add title
err = w . WriteField ( "title" , evalContext . GetNotificationTitle ( ) )
if err != nil {
return nil , b , err
}
// Add URL
2019-05-20 08:23:06 -05:00
err = w . WriteField ( "url" , ruleURL )
2019-01-12 21:09:51 -06:00
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
}