The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/alerting/notifiers/pushover.go

416 lines
10 KiB

package notifiers
import (
"bytes"
"context"
"fmt"
"io"
"mime/multipart"
"os"
"strconv"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/notifications"
)
const pushoverEndpoint = "https://api.pushover.net/1/messages.json"
func init() {
soundOptions := []alerting.SelectOption{
{
Value: "default",
Label: "Default",
},
{
Value: "pushover",
Label: "Pushover",
}, {
Value: "bike",
Label: "Bike",
}, {
Value: "bugle",
Label: "Bugle",
}, {
Value: "cashregister",
Label: "Cashregister",
}, {
Value: "classical",
Label: "Classical",
}, {
Value: "cosmic",
Label: "Cosmic",
}, {
Value: "falling",
Label: "Falling",
}, {
Value: "gamelan",
Label: "Gamelan",
}, {
Value: "incoming",
Label: "Incoming",
}, {
Value: "intermission",
Label: "Intermission",
}, {
Value: "magic",
Label: "Magic",
}, {
Value: "mechanical",
Label: "Mechanical",
}, {
Value: "pianobar",
Label: "Pianobar",
}, {
Value: "siren",
Label: "Siren",
}, {
Value: "spacealarm",
Label: "Spacealarm",
}, {
Value: "tugboat",
Label: "Tugboat",
}, {
Value: "alien",
Label: "Alien",
}, {
Value: "climb",
Label: "Climb",
}, {
Value: "persistent",
Label: "Persistent",
}, {
Value: "echo",
Label: "Echo",
}, {
Value: "updown",
Label: "Updown",
}, {
Value: "none",
Label: "None",
},
}
priorityOptions := []alerting.SelectOption{
{
Value: "2",
Label: "Emergency",
},
{
Value: "1",
Label: "High",
},
{
Value: "0",
Label: "Normal",
},
{
Value: "-1",
Label: "Low",
},
{
Value: "-2",
Label: "Lowest",
},
}
alerting.RegisterNotifier(&alerting.NotifierPlugin{
Type: "pushover",
Name: "Pushover",
Description: "Sends HTTP POST request to the Pushover API",
Heading: "Pushover settings",
Factory: NewPushoverNotifier,
Options: []alerting.NotifierOption{
{
Label: "API Token",
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText,
Placeholder: "Application token",
PropertyName: "apiToken",
Required: true,
Secure: true,
},
{
Label: "User key(s)",
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText,
Placeholder: "comma-separated list",
PropertyName: "userKey",
Required: true,
Secure: true,
},
{
Label: "Device(s) (optional)",
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText,
Placeholder: "comma-separated list; leave empty to send to all devices",
PropertyName: "device",
},
{
Label: "Alerting priority",
Element: alerting.ElementTypeSelect,
SelectOptions: priorityOptions,
PropertyName: "priority",
},
{
Label: "OK priority",
Element: alerting.ElementTypeSelect,
SelectOptions: priorityOptions,
PropertyName: "okPriority",
},
{
Description: "How often (in seconds) the Pushover servers will send the same alerting or OK notification to the user.",
Label: "Retry (Only used for Emergency Priority)",
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText,
Placeholder: "minimum 30 seconds",
PropertyName: "retry",
},
{
Description: "How many seconds the alerting or OK notification will continue to be retried.",
Label: "Expire (Only used for Emergency Priority)",
Element: alerting.ElementTypeInput,
InputType: alerting.InputTypeText,
Placeholder: "maximum 86400 seconds",
PropertyName: "expire",
},
{
Label: "Alerting sound",
Element: alerting.ElementTypeSelect,
SelectOptions: soundOptions,
PropertyName: "sound",
},
{
Label: "OK sound",
Element: alerting.ElementTypeSelect,
SelectOptions: soundOptions,
PropertyName: "okSound",
},
},
})
}
// NewPushoverNotifier is the constructor for the Pushover Notifier
func NewPushoverNotifier(model *models.AlertNotification, fn alerting.GetDecryptedValueFn, ns notifications.Service) (alerting.Notifier, error) {
userKey := fn(context.Background(), model.SecureSettings, "userKey", model.Settings.Get("userKey").MustString(), setting.SecretKey)
APIToken := fn(context.Background(), model.SecureSettings, "apiToken", model.Settings.Get("apiToken").MustString(), setting.SecretKey)
device := model.Settings.Get("device").MustString()
alertingPriority, err := strconv.Atoi(model.Settings.Get("priority").MustString("0")) // default Normal
if err != nil {
return nil, fmt.Errorf("failed to convert alerting priority to integer: %w", err)
}
okPriority, err := strconv.Atoi(model.Settings.Get("okPriority").MustString("0")) // default Normal
if err != nil {
return nil, fmt.Errorf("failed to convert OK priority to integer: %w", err)
}
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, ns),
UserKey: userKey,
APIToken: APIToken,
AlertingPriority: alertingPriority,
OKPriority: okPriority,
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
AlertingPriority int
OKPriority 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 := pn.NotificationService.SendWebhookSync(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 func() {
if err := f.Close(); err != nil {
pn.log.Warn("Failed to close file", "path", evalContext.ImageOnDiskPath, "err", err)
}
}()
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
priority := pn.AlertingPriority
if evalContext.Rule.State == models.AlertStateOK {
priority = pn.OKPriority
}
err = w.WriteField("priority", strconv.Itoa(priority))
if err != nil {
return nil, b, err
}
if 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
}
if err := w.Close(); err != nil {
return nil, b, err
}
headers := map[string]string{
"Content-Type": w.FormDataContentType(),
}
return headers, b, nil
}