mirror of
https://github.com/grafana/grafana.git
synced 2025-02-14 09:33:34 -06:00
Alerting: Add support for images in Pushover alerts (#51372)
This commit is contained in:
parent
5053468c65
commit
f04dfc589c
@ -5,7 +5,9 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/prometheus/alertmanager/template"
|
||||
@ -14,9 +16,14 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/notifications"
|
||||
)
|
||||
|
||||
const (
|
||||
pushoverMaxFileSize = 1 << 21 // 2MB
|
||||
)
|
||||
|
||||
var (
|
||||
PushoverEndpoint = "https://api.pushover.net/1/messages.json"
|
||||
)
|
||||
@ -38,6 +45,7 @@ type PushoverNotifier struct {
|
||||
Message string
|
||||
tmpl *template.Template
|
||||
log log.Logger
|
||||
images ImageStore
|
||||
ns notifications.WebhookSender
|
||||
}
|
||||
|
||||
@ -64,7 +72,7 @@ func PushoverFactory(fc FactoryConfig) (NotificationChannel, error) {
|
||||
Cfg: *fc.Config,
|
||||
}
|
||||
}
|
||||
return NewPushoverNotifier(cfg, fc.NotificationService, fc.Template), nil
|
||||
return NewPushoverNotifier(cfg, fc.ImageStore, fc.NotificationService, fc.Template), nil
|
||||
}
|
||||
|
||||
func NewPushoverConfig(config *NotificationChannelConfig, decryptFunc GetDecryptedValueFn) (*PushoverConfig, error) {
|
||||
@ -103,7 +111,8 @@ func NewPushoverConfig(config *NotificationChannelConfig, decryptFunc GetDecrypt
|
||||
}
|
||||
|
||||
// NewSlackNotifier is the constructor for the Slack notifier
|
||||
func NewPushoverNotifier(config *PushoverConfig, ns notifications.WebhookSender, t *template.Template) *PushoverNotifier {
|
||||
func NewPushoverNotifier(config *PushoverConfig, images ImageStore,
|
||||
ns notifications.WebhookSender, t *template.Template) *PushoverNotifier {
|
||||
return &PushoverNotifier{
|
||||
Base: NewBase(&models.AlertNotification{
|
||||
Uid: config.UID,
|
||||
@ -126,6 +135,7 @@ func NewPushoverNotifier(config *PushoverConfig, ns notifications.WebhookSender,
|
||||
Message: config.Message,
|
||||
tmpl: t,
|
||||
log: log.New("alerting.notifier.pushover"),
|
||||
images: images,
|
||||
ns: ns,
|
||||
}
|
||||
}
|
||||
@ -157,114 +167,131 @@ func (pn *PushoverNotifier) SendResolved() bool {
|
||||
}
|
||||
|
||||
func (pn *PushoverNotifier) genPushoverBody(ctx context.Context, as ...*types.Alert) (map[string]string, bytes.Buffer, error) {
|
||||
var b bytes.Buffer
|
||||
|
||||
ruleURL := joinUrlPath(pn.tmpl.ExternalURL.String(), "/alerting/list", pn.log)
|
||||
|
||||
alerts := types.Alerts(as...)
|
||||
|
||||
var tmplErr error
|
||||
tmpl, _ := TmplText(ctx, pn.tmpl, as, pn.log, &tmplErr)
|
||||
|
||||
b := bytes.Buffer{}
|
||||
w := multipart.NewWriter(&b)
|
||||
boundary := GetBoundary()
|
||||
if boundary != "" {
|
||||
|
||||
// tests use a non-random boundary separator
|
||||
if boundary := GetBoundary(); boundary != "" {
|
||||
err := w.SetBoundary(boundary)
|
||||
if err != nil {
|
||||
return nil, b, err
|
||||
}
|
||||
}
|
||||
|
||||
// Add the user token
|
||||
err := w.WriteField("user", tmpl(pn.UserKey))
|
||||
if err != nil {
|
||||
return nil, b, err
|
||||
var tmplErr error
|
||||
tmpl, _ := TmplText(ctx, pn.tmpl, as, pn.log, &tmplErr)
|
||||
|
||||
if err := w.WriteField("user", tmpl(pn.UserKey)); err != nil {
|
||||
return nil, b, fmt.Errorf("failed to write the user: %w", err)
|
||||
}
|
||||
|
||||
// Add the api token
|
||||
err = w.WriteField("token", pn.APIToken)
|
||||
if err != nil {
|
||||
return nil, b, err
|
||||
if err := w.WriteField("token", pn.APIToken); err != nil {
|
||||
return nil, b, fmt.Errorf("failed to write the token: %w", err)
|
||||
}
|
||||
|
||||
// Add priority
|
||||
status := types.Alerts(as...).Status()
|
||||
priority := pn.AlertingPriority
|
||||
if alerts.Status() == model.AlertResolved {
|
||||
if status == model.AlertResolved {
|
||||
priority = pn.OKPriority
|
||||
}
|
||||
err = w.WriteField("priority", strconv.Itoa(priority))
|
||||
if err != nil {
|
||||
return nil, b, err
|
||||
if err := w.WriteField("priority", strconv.Itoa(priority)); err != nil {
|
||||
return nil, b, fmt.Errorf("failed to write the priority: %w", err)
|
||||
}
|
||||
|
||||
if priority == 2 {
|
||||
err = w.WriteField("retry", strconv.Itoa(pn.Retry))
|
||||
if err != nil {
|
||||
return nil, b, err
|
||||
if err := w.WriteField("retry", strconv.Itoa(pn.Retry)); err != nil {
|
||||
return nil, b, fmt.Errorf("failed to write retry: %w", err)
|
||||
}
|
||||
|
||||
err = w.WriteField("expire", strconv.Itoa(pn.Expire))
|
||||
if err != nil {
|
||||
return nil, b, err
|
||||
if err := w.WriteField("expire", strconv.Itoa(pn.Expire)); err != nil {
|
||||
return nil, b, fmt.Errorf("failed to write expire: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add device
|
||||
if pn.Device != "" {
|
||||
err = w.WriteField("device", tmpl(pn.Device))
|
||||
if err != nil {
|
||||
return nil, b, err
|
||||
if err := w.WriteField("device", tmpl(pn.Device)); err != nil {
|
||||
return nil, b, fmt.Errorf("failed to write the device: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add sound
|
||||
sound := tmpl(pn.AlertingSound)
|
||||
if alerts.Status() == model.AlertResolved {
|
||||
if err := w.WriteField("title", tmpl(DefaultMessageTitleEmbed)); err != nil {
|
||||
return nil, b, fmt.Errorf("failed to write the title: %w", err)
|
||||
}
|
||||
|
||||
ruleURL := joinUrlPath(pn.tmpl.ExternalURL.String(), "/alerting/list", pn.log)
|
||||
if err := w.WriteField("url", ruleURL); err != nil {
|
||||
return nil, b, fmt.Errorf("failed to write the URL: %w", err)
|
||||
}
|
||||
|
||||
if err := w.WriteField("url_title", "Show alert rule"); err != nil {
|
||||
return nil, b, fmt.Errorf("failed to write the URL title: %w", err)
|
||||
}
|
||||
|
||||
if err := w.WriteField("message", tmpl(pn.Message)); err != nil {
|
||||
return nil, b, fmt.Errorf("failed write the message: %w", err)
|
||||
}
|
||||
|
||||
// Pushover supports at most one image attachment with a maximum size of pushoverMaxFileSize.
|
||||
// If the image is larger than pushoverMaxFileSize then return an error.
|
||||
_ = withStoredImages(ctx, pn.log, pn.images, func(index int, image *ngmodels.Image) error {
|
||||
if image != nil {
|
||||
f, err := os.Open(image.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open the image: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
pn.log.Error("failed to close the image", "file", image.Path)
|
||||
}
|
||||
}()
|
||||
|
||||
fileInfo, err := f.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat the image: %w", err)
|
||||
}
|
||||
|
||||
if fileInfo.Size() > pushoverMaxFileSize {
|
||||
return fmt.Errorf("image would exceeded maximum file size: %d", fileInfo.Size())
|
||||
}
|
||||
|
||||
fw, err := w.CreateFormFile("attachment", image.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create form file for the image: %w", err)
|
||||
}
|
||||
|
||||
if _, err = io.Copy(fw, f); err != nil {
|
||||
return fmt.Errorf("failed to copy the image to the form file: %w", err)
|
||||
}
|
||||
|
||||
return ErrImagesDone
|
||||
}
|
||||
return nil
|
||||
}, as...)
|
||||
|
||||
var sound string
|
||||
if status == model.AlertResolved {
|
||||
sound = tmpl(pn.OKSound)
|
||||
} else {
|
||||
sound = tmpl(pn.AlertingSound)
|
||||
}
|
||||
if sound != "default" {
|
||||
err = w.WriteField("sound", sound)
|
||||
if err != nil {
|
||||
return nil, b, err
|
||||
if err := w.WriteField("sound", sound); err != nil {
|
||||
return nil, b, fmt.Errorf("failed to write the sound: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add title
|
||||
err = w.WriteField("title", tmpl(DefaultMessageTitleEmbed))
|
||||
if err != nil {
|
||||
return nil, b, err
|
||||
// Mark the message as HTML
|
||||
if err := w.WriteField("html", "1"); err != nil {
|
||||
return nil, b, fmt.Errorf("failed to mark the message as HTML: %w", 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 err := w.Close(); err != nil {
|
||||
return nil, b, fmt.Errorf("failed to close the multipart request: %w", err)
|
||||
}
|
||||
|
||||
if tmplErr != nil {
|
||||
pn.log.Warn("failed to template pushover message", "err", tmplErr.Error())
|
||||
}
|
||||
|
||||
// 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(),
|
||||
}
|
||||
|
@ -25,6 +25,8 @@ import (
|
||||
func TestPushoverNotifier(t *testing.T) {
|
||||
tmpl := templateForTests(t)
|
||||
|
||||
images := newFakeImageStoreWithFile(t, 2)
|
||||
|
||||
externalURL, err := url.Parse("http://localhost")
|
||||
require.NoError(t, err)
|
||||
tmpl.ExternalURL = externalURL
|
||||
@ -38,7 +40,7 @@ func TestPushoverNotifier(t *testing.T) {
|
||||
expMsgError error
|
||||
}{
|
||||
{
|
||||
name: "Correct config with one alert",
|
||||
name: "Correct config with single alert",
|
||||
settings: `{
|
||||
"userKey": "<userKey>",
|
||||
"apiToken": "<apiToken>"
|
||||
@ -47,20 +49,21 @@ func TestPushoverNotifier(t *testing.T) {
|
||||
{
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{"__alert_rule_uid__": "rule uid", "alertname": "alert1", "lbl1": "val1"},
|
||||
Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh"},
|
||||
Annotations: model.LabelSet{"ann1": "annv1", "__dashboardUid__": "abcd", "__panelId__": "efgh", "__alertScreenshotToken__": "test-image-1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expMsg: map[string]string{
|
||||
"user": "<userKey>",
|
||||
"token": "<apiToken>",
|
||||
"priority": "0",
|
||||
"sound": "",
|
||||
"title": "[FIRING:1] (val1)",
|
||||
"url": "http://localhost/alerting/list",
|
||||
"url_title": "Show alert rule",
|
||||
"message": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
"html": "1",
|
||||
"user": "<userKey>",
|
||||
"token": "<apiToken>",
|
||||
"priority": "0",
|
||||
"sound": "",
|
||||
"title": "[FIRING:1] (val1)",
|
||||
"url": "http://localhost/alerting/list",
|
||||
"url_title": "Show alert rule",
|
||||
"message": "**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\nDashboard: http://localhost/d/abcd\nPanel: http://localhost/d/abcd?viewPanel=efgh\n",
|
||||
"attachment": "\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\b\x04\x00\x00\x00\xb5\x1c\f\x02\x00\x00\x00\vIDATx\xdacd`\x00\x00\x00\x06\x00\x020\x81\xd0/\x00\x00\x00\x00IEND\xaeB`\x82",
|
||||
"html": "1",
|
||||
},
|
||||
expMsgError: nil,
|
||||
},
|
||||
@ -82,28 +85,29 @@ func TestPushoverNotifier(t *testing.T) {
|
||||
{
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{"__alert_rule_uid__": "rule uid", "alertname": "alert1", "lbl1": "val1"},
|
||||
Annotations: model.LabelSet{"ann1": "annv1"},
|
||||
Annotations: model.LabelSet{"ann1": "annv1", "__alertScreenshotToken__": "test-image-1"},
|
||||
},
|
||||
}, {
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val2"},
|
||||
Annotations: model.LabelSet{"ann1": "annv2"},
|
||||
Annotations: model.LabelSet{"ann1": "annv2", "__alertScreenshotToken__": "test-image-2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
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",
|
||||
"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",
|
||||
"attachment": "\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\b\x04\x00\x00\x00\xb5\x1c\f\x02\x00\x00\x00\vIDATx\xdacd`\x00\x00\x00\x06\x00\x020\x81\xd0/\x00\x00\x00\x00IEND\xaeB`\x82",
|
||||
"html": "1",
|
||||
"retry": "30",
|
||||
"expire": "86400",
|
||||
"device": "device",
|
||||
},
|
||||
expMsgError: nil,
|
||||
},
|
||||
@ -157,7 +161,7 @@ func TestPushoverNotifier(t *testing.T) {
|
||||
|
||||
ctx := notify.WithGroupKey(context.Background(), "alertname")
|
||||
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
|
||||
pn := NewPushoverNotifier(cfg, webhookSender, tmpl)
|
||||
pn := NewPushoverNotifier(cfg, images, webhookSender, tmpl)
|
||||
ok, err := pn.Notify(ctx, c.alerts...)
|
||||
if c.expMsgError != nil {
|
||||
require.Error(t, err)
|
||||
|
Loading…
Reference in New Issue
Block a user