mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -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"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/prometheus/alertmanager/template"
|
"github.com/prometheus/alertmanager/template"
|
||||||
@ -14,9 +16,14 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
"github.com/grafana/grafana/pkg/services/notifications"
|
"github.com/grafana/grafana/pkg/services/notifications"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
pushoverMaxFileSize = 1 << 21 // 2MB
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
PushoverEndpoint = "https://api.pushover.net/1/messages.json"
|
PushoverEndpoint = "https://api.pushover.net/1/messages.json"
|
||||||
)
|
)
|
||||||
@ -38,6 +45,7 @@ type PushoverNotifier struct {
|
|||||||
Message string
|
Message string
|
||||||
tmpl *template.Template
|
tmpl *template.Template
|
||||||
log log.Logger
|
log log.Logger
|
||||||
|
images ImageStore
|
||||||
ns notifications.WebhookSender
|
ns notifications.WebhookSender
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +72,7 @@ func PushoverFactory(fc FactoryConfig) (NotificationChannel, error) {
|
|||||||
Cfg: *fc.Config,
|
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) {
|
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
|
// 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{
|
return &PushoverNotifier{
|
||||||
Base: NewBase(&models.AlertNotification{
|
Base: NewBase(&models.AlertNotification{
|
||||||
Uid: config.UID,
|
Uid: config.UID,
|
||||||
@ -126,6 +135,7 @@ func NewPushoverNotifier(config *PushoverConfig, ns notifications.WebhookSender,
|
|||||||
Message: config.Message,
|
Message: config.Message,
|
||||||
tmpl: t,
|
tmpl: t,
|
||||||
log: log.New("alerting.notifier.pushover"),
|
log: log.New("alerting.notifier.pushover"),
|
||||||
|
images: images,
|
||||||
ns: ns,
|
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) {
|
func (pn *PushoverNotifier) genPushoverBody(ctx context.Context, as ...*types.Alert) (map[string]string, bytes.Buffer, error) {
|
||||||
var b bytes.Buffer
|
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)
|
|
||||||
|
|
||||||
w := multipart.NewWriter(&b)
|
w := multipart.NewWriter(&b)
|
||||||
boundary := GetBoundary()
|
|
||||||
if boundary != "" {
|
// tests use a non-random boundary separator
|
||||||
|
if boundary := GetBoundary(); boundary != "" {
|
||||||
err := w.SetBoundary(boundary)
|
err := w.SetBoundary(boundary)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, b, err
|
return nil, b, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the user token
|
var tmplErr error
|
||||||
err := w.WriteField("user", tmpl(pn.UserKey))
|
tmpl, _ := TmplText(ctx, pn.tmpl, as, pn.log, &tmplErr)
|
||||||
if err != nil {
|
|
||||||
return nil, b, err
|
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
|
if err := w.WriteField("token", pn.APIToken); err != nil {
|
||||||
err = w.WriteField("token", pn.APIToken)
|
return nil, b, fmt.Errorf("failed to write the token: %w", err)
|
||||||
if err != nil {
|
|
||||||
return nil, b, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add priority
|
status := types.Alerts(as...).Status()
|
||||||
priority := pn.AlertingPriority
|
priority := pn.AlertingPriority
|
||||||
if alerts.Status() == model.AlertResolved {
|
if status == model.AlertResolved {
|
||||||
priority = pn.OKPriority
|
priority = pn.OKPriority
|
||||||
}
|
}
|
||||||
err = w.WriteField("priority", strconv.Itoa(priority))
|
if err := w.WriteField("priority", strconv.Itoa(priority)); err != nil {
|
||||||
if err != nil {
|
return nil, b, fmt.Errorf("failed to write the priority: %w", err)
|
||||||
return nil, b, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if priority == 2 {
|
if priority == 2 {
|
||||||
err = w.WriteField("retry", strconv.Itoa(pn.Retry))
|
if err := w.WriteField("retry", strconv.Itoa(pn.Retry)); err != nil {
|
||||||
if err != nil {
|
return nil, b, fmt.Errorf("failed to write retry: %w", err)
|
||||||
return nil, b, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = w.WriteField("expire", strconv.Itoa(pn.Expire))
|
if err := w.WriteField("expire", strconv.Itoa(pn.Expire)); err != nil {
|
||||||
if err != nil {
|
return nil, b, fmt.Errorf("failed to write expire: %w", err)
|
||||||
return nil, b, err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add device
|
|
||||||
if pn.Device != "" {
|
if pn.Device != "" {
|
||||||
err = w.WriteField("device", tmpl(pn.Device))
|
if err := w.WriteField("device", tmpl(pn.Device)); err != nil {
|
||||||
if err != nil {
|
return nil, b, fmt.Errorf("failed to write the device: %w", err)
|
||||||
return nil, b, err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add sound
|
if err := w.WriteField("title", tmpl(DefaultMessageTitleEmbed)); err != nil {
|
||||||
sound := tmpl(pn.AlertingSound)
|
return nil, b, fmt.Errorf("failed to write the title: %w", err)
|
||||||
if alerts.Status() == model.AlertResolved {
|
}
|
||||||
|
|
||||||
|
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)
|
sound = tmpl(pn.OKSound)
|
||||||
|
} else {
|
||||||
|
sound = tmpl(pn.AlertingSound)
|
||||||
}
|
}
|
||||||
if sound != "default" {
|
if sound != "default" {
|
||||||
err = w.WriteField("sound", sound)
|
if err := w.WriteField("sound", sound); err != nil {
|
||||||
if err != nil {
|
return nil, b, fmt.Errorf("failed to write the sound: %w", err)
|
||||||
return nil, b, err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add title
|
// Mark the message as HTML
|
||||||
err = w.WriteField("title", tmpl(DefaultMessageTitleEmbed))
|
if err := w.WriteField("html", "1"); err != nil {
|
||||||
if err != nil {
|
return nil, b, fmt.Errorf("failed to mark the message as HTML: %w", err)
|
||||||
return nil, b, err
|
|
||||||
}
|
}
|
||||||
|
if err := w.Close(); err != nil {
|
||||||
// Add URL
|
return nil, b, fmt.Errorf("failed to close the multipart request: %w", err)
|
||||||
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 {
|
if tmplErr != nil {
|
||||||
pn.log.Warn("failed to template pushover message", "err", tmplErr.Error())
|
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{
|
headers := map[string]string{
|
||||||
"Content-Type": w.FormDataContentType(),
|
"Content-Type": w.FormDataContentType(),
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,8 @@ import (
|
|||||||
func TestPushoverNotifier(t *testing.T) {
|
func TestPushoverNotifier(t *testing.T) {
|
||||||
tmpl := templateForTests(t)
|
tmpl := templateForTests(t)
|
||||||
|
|
||||||
|
images := newFakeImageStoreWithFile(t, 2)
|
||||||
|
|
||||||
externalURL, err := url.Parse("http://localhost")
|
externalURL, err := url.Parse("http://localhost")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
tmpl.ExternalURL = externalURL
|
tmpl.ExternalURL = externalURL
|
||||||
@ -38,7 +40,7 @@ func TestPushoverNotifier(t *testing.T) {
|
|||||||
expMsgError error
|
expMsgError error
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Correct config with one alert",
|
name: "Correct config with single alert",
|
||||||
settings: `{
|
settings: `{
|
||||||
"userKey": "<userKey>",
|
"userKey": "<userKey>",
|
||||||
"apiToken": "<apiToken>"
|
"apiToken": "<apiToken>"
|
||||||
@ -47,20 +49,21 @@ func TestPushoverNotifier(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Alert: model.Alert{
|
Alert: model.Alert{
|
||||||
Labels: model.LabelSet{"__alert_rule_uid__": "rule uid", "alertname": "alert1", "lbl1": "val1"},
|
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{
|
expMsg: map[string]string{
|
||||||
"user": "<userKey>",
|
"user": "<userKey>",
|
||||||
"token": "<apiToken>",
|
"token": "<apiToken>",
|
||||||
"priority": "0",
|
"priority": "0",
|
||||||
"sound": "",
|
"sound": "",
|
||||||
"title": "[FIRING:1] (val1)",
|
"title": "[FIRING:1] (val1)",
|
||||||
"url": "http://localhost/alerting/list",
|
"url": "http://localhost/alerting/list",
|
||||||
"url_title": "Show alert rule",
|
"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",
|
"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",
|
"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,
|
expMsgError: nil,
|
||||||
},
|
},
|
||||||
@ -82,28 +85,29 @@ func TestPushoverNotifier(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Alert: model.Alert{
|
Alert: model.Alert{
|
||||||
Labels: model.LabelSet{"__alert_rule_uid__": "rule uid", "alertname": "alert1", "lbl1": "val1"},
|
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{
|
Alert: model.Alert{
|
||||||
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val2"},
|
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{
|
expMsg: map[string]string{
|
||||||
"user": "<userKey>",
|
"user": "<userKey>",
|
||||||
"token": "<apiToken>",
|
"token": "<apiToken>",
|
||||||
"priority": "2",
|
"priority": "2",
|
||||||
"sound": "echo",
|
"sound": "echo",
|
||||||
"title": "[FIRING:2] ",
|
"title": "[FIRING:2] ",
|
||||||
"url": "http://localhost/alerting/list",
|
"url": "http://localhost/alerting/list",
|
||||||
"url_title": "Show alert rule",
|
"url_title": "Show alert rule",
|
||||||
"message": "2 alerts are firing, 0 are resolved",
|
"message": "2 alerts are firing, 0 are resolved",
|
||||||
"html": "1",
|
"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",
|
||||||
"retry": "30",
|
"html": "1",
|
||||||
"expire": "86400",
|
"retry": "30",
|
||||||
"device": "device",
|
"expire": "86400",
|
||||||
|
"device": "device",
|
||||||
},
|
},
|
||||||
expMsgError: nil,
|
expMsgError: nil,
|
||||||
},
|
},
|
||||||
@ -157,7 +161,7 @@ func TestPushoverNotifier(t *testing.T) {
|
|||||||
|
|
||||||
ctx := notify.WithGroupKey(context.Background(), "alertname")
|
ctx := notify.WithGroupKey(context.Background(), "alertname")
|
||||||
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"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...)
|
ok, err := pn.Notify(ctx, c.alerts...)
|
||||||
if c.expMsgError != nil {
|
if c.expMsgError != nil {
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
Loading…
Reference in New Issue
Block a user