mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
[Alerting]: Add Pushover integration with the alert manager (#34371)
* [Alerting]: Add Pushover integration with the alert manager * lint * Set boundary only for tests * Remove title field * fix imports
This commit is contained in:
parent
1d2febfa85
commit
a79a4838b8
@ -412,6 +412,8 @@ func (am *Alertmanager) buildReceiverIntegrations(receiver *apimodels.PostableAp
|
|||||||
n, err = channels.NewEmailNotifier(cfg, tmpl) // Email notifier already has a default template.
|
n, err = channels.NewEmailNotifier(cfg, tmpl) // Email notifier already has a default template.
|
||||||
case "pagerduty":
|
case "pagerduty":
|
||||||
n, err = channels.NewPagerdutyNotifier(cfg, tmpl)
|
n, err = channels.NewPagerdutyNotifier(cfg, tmpl)
|
||||||
|
case "pushover":
|
||||||
|
n, err = channels.NewPushoverNotifier(cfg, tmpl)
|
||||||
case "slack":
|
case "slack":
|
||||||
n, err = channels.NewSlackNotifier(cfg, tmpl)
|
n, err = channels.NewSlackNotifier(cfg, tmpl)
|
||||||
case "telegram":
|
case "telegram":
|
||||||
|
@ -4,6 +4,103 @@ import "github.com/grafana/grafana/pkg/services/alerting"
|
|||||||
|
|
||||||
// GetAvailableNotifiers returns the metadata of all the notification channels that can be configured.
|
// GetAvailableNotifiers returns the metadata of all the notification channels that can be configured.
|
||||||
func GetAvailableNotifiers() []*alerting.NotifierPlugin {
|
func GetAvailableNotifiers() []*alerting.NotifierPlugin {
|
||||||
|
pushoverSoundOptions := []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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pushoverPriorityOptions := []alerting.SelectOption{
|
||||||
|
{
|
||||||
|
Value: "2",
|
||||||
|
Label: "Emergency",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value: "1",
|
||||||
|
Label: "High",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value: "0",
|
||||||
|
Label: "Normal",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value: "-1",
|
||||||
|
Label: "Low",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Value: "-2",
|
||||||
|
Label: "Lowest",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
return []*alerting.NotifierPlugin{
|
return []*alerting.NotifierPlugin{
|
||||||
{
|
{
|
||||||
Type: "dingding",
|
Type: "dingding",
|
||||||
@ -137,6 +234,85 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Type: "pushover",
|
||||||
|
Name: "Pushover",
|
||||||
|
Description: "Sends HTTP POST request to the Pushover API",
|
||||||
|
Heading: "Pushover settings",
|
||||||
|
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: pushoverPriorityOptions,
|
||||||
|
PropertyName: "priority",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "OK priority",
|
||||||
|
Element: alerting.ElementTypeSelect,
|
||||||
|
SelectOptions: pushoverPriorityOptions,
|
||||||
|
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: pushoverSoundOptions,
|
||||||
|
PropertyName: "sound",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "OK sound",
|
||||||
|
Element: alerting.ElementTypeSelect,
|
||||||
|
SelectOptions: pushoverSoundOptions,
|
||||||
|
PropertyName: "okSound",
|
||||||
|
},
|
||||||
|
{ // New in 8.0.
|
||||||
|
Label: "Message",
|
||||||
|
Element: alerting.ElementTypeTextArea,
|
||||||
|
Placeholder: `{{ template "default.message" . }}`,
|
||||||
|
PropertyName: "message",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Type: "slack",
|
Type: "slack",
|
||||||
Name: "Slack",
|
Name: "Slack",
|
||||||
|
250
pkg/services/ngalert/notifier/channels/pushover.go
Normal file
250
pkg/services/ngalert/notifier/channels/pushover.go
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
package channels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
gokit_log "github.com/go-kit/kit/log"
|
||||||
|
"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"
|
||||||
|
old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/prometheus/alertmanager/notify"
|
||||||
|
"github.com/prometheus/alertmanager/template"
|
||||||
|
"github.com/prometheus/alertmanager/types"
|
||||||
|
"github.com/prometheus/common/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PUSHOVERENDPOINT = "https://api.pushover.net/1/messages.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getBoundary is used for overriding the behaviour for tests
|
||||||
|
// and set a boundary
|
||||||
|
var getBoundary = func() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushoverNotifier is responsible for sending
|
||||||
|
// alert notifications to Pushover
|
||||||
|
type PushoverNotifier struct {
|
||||||
|
old_notifiers.NotifierBase
|
||||||
|
UserKey string
|
||||||
|
APIToken string
|
||||||
|
AlertingPriority int
|
||||||
|
OKPriority int
|
||||||
|
Retry int
|
||||||
|
Expire int
|
||||||
|
Device string
|
||||||
|
AlertingSound string
|
||||||
|
OKSound string
|
||||||
|
Upload bool
|
||||||
|
Message string
|
||||||
|
tmpl *template.Template
|
||||||
|
log log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSlackNotifier is the constructor for the Slack notifier
|
||||||
|
func NewPushoverNotifier(model *NotificationChannelConfig, t *template.Template) (*PushoverNotifier, error) {
|
||||||
|
userKey := model.DecryptedValue("userKey", model.Settings.Get("userKey").MustString())
|
||||||
|
APIToken := model.DecryptedValue("apiToken", model.Settings.Get("apiToken").MustString())
|
||||||
|
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 found"}
|
||||||
|
}
|
||||||
|
if APIToken == "" {
|
||||||
|
return nil, alerting.ValidationError{Reason: "API token not found"}
|
||||||
|
}
|
||||||
|
return &PushoverNotifier{
|
||||||
|
NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{
|
||||||
|
Uid: model.UID,
|
||||||
|
Name: model.Name,
|
||||||
|
Type: model.Type,
|
||||||
|
DisableResolveMessage: model.DisableResolveMessage,
|
||||||
|
Settings: model.Settings,
|
||||||
|
SecureSettings: model.SecureSettings,
|
||||||
|
}),
|
||||||
|
UserKey: userKey,
|
||||||
|
APIToken: APIToken,
|
||||||
|
AlertingPriority: alertingPriority,
|
||||||
|
OKPriority: okPriority,
|
||||||
|
Retry: retry,
|
||||||
|
Expire: expire,
|
||||||
|
Device: device,
|
||||||
|
AlertingSound: alertingSound,
|
||||||
|
OKSound: okSound,
|
||||||
|
Upload: uploadImage,
|
||||||
|
Message: model.Settings.Get("message").MustString(`{{ template "default.message" .}}`),
|
||||||
|
tmpl: t,
|
||||||
|
log: log.New("alerting.notifier.pushover"),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify sends an alert notification to Slack.
|
||||||
|
func (pn *PushoverNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
|
||||||
|
headers, uploadBody, err := pn.genPushoverBody(ctx, as...)
|
||||||
|
if err != nil {
|
||||||
|
pn.log.Error("Failed to generate body for pushover", "error", err)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := &models.SendWebhookSync{
|
||||||
|
Url: PUSHOVERENDPOINT,
|
||||||
|
HttpMethod: "POST",
|
||||||
|
HttpHeader: headers,
|
||||||
|
Body: uploadBody.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bus.DispatchCtx(ctx, cmd); err != nil {
|
||||||
|
pn.log.Error("Failed to send pushover notification", "error", err, "webhook", pn.Name)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
func (pn *PushoverNotifier) SendResolved() bool {
|
||||||
|
return !pn.GetDisableResolveMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pn *PushoverNotifier) genPushoverBody(ctx context.Context, as ...*types.Alert) (map[string]string, bytes.Buffer, error) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
|
||||||
|
u, err := url.Parse(pn.tmpl.ExternalURL.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, b, fmt.Errorf("failed to parse ")
|
||||||
|
}
|
||||||
|
u.Path = path.Join(u.Path, "/alerting/list")
|
||||||
|
ruleURL := u.String()
|
||||||
|
|
||||||
|
alerts := types.Alerts(as...)
|
||||||
|
|
||||||
|
var tmplErr error
|
||||||
|
data := notify.GetTemplateData(ctx, pn.tmpl, as, gokit_log.NewNopLogger())
|
||||||
|
tmpl := notify.TmplText(pn.tmpl, data, &tmplErr)
|
||||||
|
|
||||||
|
w := multipart.NewWriter(&b)
|
||||||
|
boundary := getBoundary()
|
||||||
|
if boundary != "" {
|
||||||
|
err = w.SetBoundary(boundary)
|
||||||
|
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 alerts.Status() == model.AlertResolved {
|
||||||
|
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 alerts.Status() == model.AlertResolved {
|
||||||
|
sound = pn.OKSound
|
||||||
|
}
|
||||||
|
if sound != "default" {
|
||||||
|
err = w.WriteField("sound", sound)
|
||||||
|
if err != nil {
|
||||||
|
return nil, b, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add title
|
||||||
|
err = w.WriteField("title", tmpl(`{{ template "default.title" . }}`))
|
||||||
|
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 alert rule")
|
||||||
|
if err != nil {
|
||||||
|
return nil, b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add message
|
||||||
|
err = w.WriteField("message", tmpl(pn.Message))
|
||||||
|
if err != nil {
|
||||||
|
return nil, b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tmplErr != nil {
|
||||||
|
return nil, b, errors.Wrap(tmplErr, "failed to template pushover message")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
203
pkg/services/ngalert/notifier/channels/pushover_test.go
Normal file
203
pkg/services/ngalert/notifier/channels/pushover_test.go
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
package channels
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
|
"github.com/prometheus/alertmanager/notify"
|
||||||
|
"github.com/prometheus/alertmanager/types"
|
||||||
|
"github.com/prometheus/common/model"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPushoverNotifier(t *testing.T) {
|
||||||
|
tmpl := templateForTests(t)
|
||||||
|
|
||||||
|
externalURL, err := url.Parse("http://localhost")
|
||||||
|
require.NoError(t, err)
|
||||||
|
tmpl.ExternalURL = externalURL
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
settings string
|
||||||
|
alerts []*types.Alert
|
||||||
|
expMsg map[string]string
|
||||||
|
expInitError error
|
||||||
|
expMsgError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Correct config with one alert",
|
||||||
|
settings: `{
|
||||||
|
"userKey": "<userKey>",
|
||||||
|
"apiToken": "<apiToken>"
|
||||||
|
}`,
|
||||||
|
alerts: []*types.Alert{
|
||||||
|
{
|
||||||
|
Alert: model.Alert{
|
||||||
|
Labels: model.LabelSet{"__alert_rule_uid__": "rule uid", "alertname": "alert1", "lbl1": "val1"},
|
||||||
|
Annotations: model.LabelSet{"ann1": "annv1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expMsg: map[string]string{
|
||||||
|
"user": "<userKey>",
|
||||||
|
"token": "<apiToken>",
|
||||||
|
"priority": "0",
|
||||||
|
"sound": "",
|
||||||
|
"title": "[FIRING:1] (rule uid val1)",
|
||||||
|
"url": "http://localhost/alerting/list",
|
||||||
|
"url_title": "Show alert rule",
|
||||||
|
"message": "\n**Firing**\nLabels:\n - alertname = alert1\n - __alert_rule_uid__ = rule uid\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: \n\n\n\n\n",
|
||||||
|
"html": "1",
|
||||||
|
},
|
||||||
|
expInitError: nil,
|
||||||
|
expMsgError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Custom config with multiple alerts",
|
||||||
|
settings: `{
|
||||||
|
"userKey": "<userKey>",
|
||||||
|
"apiToken": "<apiToken>",
|
||||||
|
"device": "device",
|
||||||
|
"priority": "2",
|
||||||
|
"okpriority": "0",
|
||||||
|
"retry": "30",
|
||||||
|
"expire": "86400",
|
||||||
|
"sound": "echo",
|
||||||
|
"oksound": "magic",
|
||||||
|
"message": "{{ len .Alerts.Firing }} alerts are firing, {{ len .Alerts.Resolved }} are resolved"
|
||||||
|
}`,
|
||||||
|
alerts: []*types.Alert{
|
||||||
|
{
|
||||||
|
Alert: model.Alert{
|
||||||
|
Labels: model.LabelSet{"__alert_rule_uid__": "rule uid", "alertname": "alert1", "lbl1": "val1"},
|
||||||
|
Annotations: model.LabelSet{"ann1": "annv1"},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
Alert: model.Alert{
|
||||||
|
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val2"},
|
||||||
|
Annotations: model.LabelSet{"ann1": "annv2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expMsg: map[string]string{
|
||||||
|
"user": "<userKey>",
|
||||||
|
"token": "<apiToken>",
|
||||||
|
"priority": "2",
|
||||||
|
"sound": "echo",
|
||||||
|
"title": "[FIRING:2] ",
|
||||||
|
"url": "http://localhost/alerting/list",
|
||||||
|
"url_title": "Show alert rule",
|
||||||
|
"message": "2 alerts are firing, 0 are resolved",
|
||||||
|
"html": "1",
|
||||||
|
"retry": "30",
|
||||||
|
"expire": "86400",
|
||||||
|
"device": "device",
|
||||||
|
},
|
||||||
|
expInitError: nil,
|
||||||
|
expMsgError: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Missing user key",
|
||||||
|
settings: `{
|
||||||
|
"apiToken": "<apiToken>"
|
||||||
|
}`,
|
||||||
|
expInitError: alerting.ValidationError{Reason: "user key not found"},
|
||||||
|
}, {
|
||||||
|
name: "Missing api key",
|
||||||
|
settings: `{
|
||||||
|
"userKey": "<userKey>"
|
||||||
|
}`,
|
||||||
|
expInitError: alerting.ValidationError{Reason: "API token not found"},
|
||||||
|
}, {
|
||||||
|
name: "Error in building message",
|
||||||
|
settings: `{
|
||||||
|
"apiToken": "<apiToken>",
|
||||||
|
"userKey": "<userKey>",
|
||||||
|
"message": "{{ .BrokenTemplate }"
|
||||||
|
}`,
|
||||||
|
expMsgError: errors.New("failed to template pushover message: template: :1: unexpected \"}\" in operand"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
origGetBoundary := getBoundary
|
||||||
|
boundary := "abcd"
|
||||||
|
getBoundary = func() string {
|
||||||
|
return boundary
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
getBoundary = origGetBoundary
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
settingsJSON, err := simplejson.NewJson([]byte(c.settings))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
m := &NotificationChannelConfig{
|
||||||
|
Name: "pushover_testing",
|
||||||
|
Type: "pushover",
|
||||||
|
Settings: settingsJSON,
|
||||||
|
}
|
||||||
|
|
||||||
|
pn, err := NewPushoverNotifier(m, tmpl)
|
||||||
|
if c.expInitError != nil {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, c.expInitError.Error(), err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
body := ""
|
||||||
|
bus.AddHandlerCtx("test", func(ctx context.Context, webhook *models.SendWebhookSync) error {
|
||||||
|
body = webhook.Body
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := notify.WithGroupKey(context.Background(), "alertname")
|
||||||
|
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
|
||||||
|
ok, err := pn.Notify(ctx, c.alerts...)
|
||||||
|
if c.expMsgError != nil {
|
||||||
|
require.Error(t, err)
|
||||||
|
require.False(t, ok)
|
||||||
|
require.Equal(t, c.expMsgError.Error(), err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
bodyReader := multipart.NewReader(strings.NewReader(body), boundary)
|
||||||
|
for {
|
||||||
|
part, err := bodyReader.NextPart()
|
||||||
|
if part == nil || errors.Is(err, io.EOF) {
|
||||||
|
assert.Empty(t, c.expMsg, fmt.Sprintf("expected fields %v", c.expMsg))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
formField := part.FormName()
|
||||||
|
expected, ok := c.expMsg[formField]
|
||||||
|
assert.True(t, ok, fmt.Sprintf("unexpected field %s", formField))
|
||||||
|
actual := []byte("")
|
||||||
|
if expected != "" {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
_, err := buf.ReadFrom(part)
|
||||||
|
require.NoError(t, err)
|
||||||
|
actual = buf.Bytes()
|
||||||
|
}
|
||||||
|
assert.Equal(t, expected, string(actual))
|
||||||
|
delete(c.expMsg, formField)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -287,6 +287,403 @@ var expAvailableChannelJsonOutput = `
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "pushover",
|
||||||
|
"name": "Pushover",
|
||||||
|
"description": "Sends HTTP POST request to the Pushover API",
|
||||||
|
"heading": "Pushover settings",
|
||||||
|
"info": "",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"element": "input",
|
||||||
|
"inputType": "text",
|
||||||
|
"label": "API Token",
|
||||||
|
"description": "",
|
||||||
|
"placeholder": "Application token",
|
||||||
|
"propertyName": "apiToken",
|
||||||
|
"selectOptions": null,
|
||||||
|
"showWhen": {
|
||||||
|
"field": "",
|
||||||
|
"is": ""
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"validationRule": "",
|
||||||
|
"secure": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"element": "input",
|
||||||
|
"inputType": "text",
|
||||||
|
"label": "User key(s)",
|
||||||
|
"description": "",
|
||||||
|
"placeholder": "comma-separated list",
|
||||||
|
"propertyName": "userKey",
|
||||||
|
"selectOptions": null,
|
||||||
|
"showWhen": {
|
||||||
|
"field": "",
|
||||||
|
"is": ""
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"validationRule": "",
|
||||||
|
"secure": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"element": "input",
|
||||||
|
"inputType": "text",
|
||||||
|
"label": "Device(s) (optional)",
|
||||||
|
"description": "",
|
||||||
|
"placeholder": "comma-separated list; leave empty to send to all devices",
|
||||||
|
"propertyName": "device",
|
||||||
|
"selectOptions": null,
|
||||||
|
"showWhen": {
|
||||||
|
"field": "",
|
||||||
|
"is": ""
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"validationRule": "",
|
||||||
|
"secure": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"element": "select",
|
||||||
|
"inputType": "",
|
||||||
|
"label": "Alerting priority",
|
||||||
|
"description": "",
|
||||||
|
"placeholder": "",
|
||||||
|
"propertyName": "priority",
|
||||||
|
"selectOptions": [
|
||||||
|
{
|
||||||
|
"value": "2",
|
||||||
|
"label": "Emergency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "1",
|
||||||
|
"label": "High"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "0",
|
||||||
|
"label": "Normal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "-1",
|
||||||
|
"label": "Low"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "-2",
|
||||||
|
"label": "Lowest"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"showWhen": {
|
||||||
|
"field": "",
|
||||||
|
"is": ""
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"validationRule": "",
|
||||||
|
"secure": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"element": "select",
|
||||||
|
"inputType": "",
|
||||||
|
"label": "OK priority",
|
||||||
|
"description": "",
|
||||||
|
"placeholder": "",
|
||||||
|
"propertyName": "okPriority",
|
||||||
|
"selectOptions": [
|
||||||
|
{
|
||||||
|
"value": "2",
|
||||||
|
"label": "Emergency"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "1",
|
||||||
|
"label": "High"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "0",
|
||||||
|
"label": "Normal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "-1",
|
||||||
|
"label": "Low"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "-2",
|
||||||
|
"label": "Lowest"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"showWhen": {
|
||||||
|
"field": "",
|
||||||
|
"is": ""
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"validationRule": "",
|
||||||
|
"secure": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"element": "input",
|
||||||
|
"inputType": "text",
|
||||||
|
"label": "Retry (Only used for Emergency Priority)",
|
||||||
|
"description": "How often (in seconds) the Pushover servers will send the same alerting or OK notification to the user.",
|
||||||
|
"placeholder": "minimum 30 seconds",
|
||||||
|
"propertyName": "retry",
|
||||||
|
"selectOptions": null,
|
||||||
|
"showWhen": {
|
||||||
|
"field": "",
|
||||||
|
"is": ""
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"validationRule": "",
|
||||||
|
"secure": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"element": "input",
|
||||||
|
"inputType": "text",
|
||||||
|
"label": "Expire (Only used for Emergency Priority)",
|
||||||
|
"description": "How many seconds the alerting or OK notification will continue to be retried.",
|
||||||
|
"placeholder": "maximum 86400 seconds",
|
||||||
|
"propertyName": "expire",
|
||||||
|
"selectOptions": null,
|
||||||
|
"showWhen": {
|
||||||
|
"field": "",
|
||||||
|
"is": ""
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"validationRule": "",
|
||||||
|
"secure": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"element": "select",
|
||||||
|
"inputType": "",
|
||||||
|
"label": "Alerting sound",
|
||||||
|
"description": "",
|
||||||
|
"placeholder": "",
|
||||||
|
"propertyName": "sound",
|
||||||
|
"selectOptions": [
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"showWhen": {
|
||||||
|
"field": "",
|
||||||
|
"is": ""
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"validationRule": "",
|
||||||
|
"secure": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"element": "select",
|
||||||
|
"inputType": "",
|
||||||
|
"label": "OK sound",
|
||||||
|
"description": "",
|
||||||
|
"placeholder": "",
|
||||||
|
"propertyName": "okSound",
|
||||||
|
"selectOptions": [
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"showWhen": {
|
||||||
|
"field": "",
|
||||||
|
"is": ""
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"validationRule": "",
|
||||||
|
"secure": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"element": "textarea",
|
||||||
|
"inputType": "",
|
||||||
|
"label": "Message",
|
||||||
|
"description": "",
|
||||||
|
"placeholder": "{{ template \"default.message\" . }}",
|
||||||
|
"propertyName": "message",
|
||||||
|
"selectOptions": null,
|
||||||
|
"showWhen": {
|
||||||
|
"field": "",
|
||||||
|
"is": ""
|
||||||
|
},
|
||||||
|
"required": false,
|
||||||
|
"validationRule": "",
|
||||||
|
"secure": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "slack",
|
"type": "slack",
|
||||||
"name": "Slack",
|
"name": "Slack",
|
||||||
|
Loading…
Reference in New Issue
Block a user