Alerting: Add support for images in SensuGo alerts (#50718) (#50853)

This commit is contained in:
Grot (@grafanabot)
2022-06-15 11:30:35 +02:00
committed by GitHub
parent d543bb242f
commit efc29caf67
3 changed files with 89 additions and 15 deletions
@@ -13,14 +13,16 @@ 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"
)
type SensuGoNotifier struct {
*Base
log log.Logger
ns notifications.WebhookSender
tmpl *template.Template
log log.Logger
images ImageStore
ns notifications.WebhookSender
tmpl *template.Template
URL string
Entity string
@@ -50,7 +52,7 @@ func SensuGoFactory(fc FactoryConfig) (NotificationChannel, error) {
Cfg: *fc.Config,
}
}
return NewSensuGoNotifier(cfg, fc.NotificationService, fc.Template), nil
return NewSensuGoNotifier(cfg, fc.ImageStore, fc.NotificationService, fc.Template), nil
}
func NewSensuGoConfig(config *NotificationChannelConfig, decryptFunc GetDecryptedValueFn) (*SensuGoConfig, error) {
@@ -75,7 +77,7 @@ func NewSensuGoConfig(config *NotificationChannelConfig, decryptFunc GetDecrypte
}
// NewSensuGoNotifier is the constructor for the SensuGo notifier
func NewSensuGoNotifier(config *SensuGoConfig, ns notifications.WebhookSender, t *template.Template) *SensuGoNotifier {
func NewSensuGoNotifier(config *SensuGoConfig, images ImageStore, ns notifications.WebhookSender, t *template.Template) *SensuGoNotifier {
return &SensuGoNotifier{
Base: NewBase(&models.AlertNotification{
Uid: config.UID,
@@ -93,6 +95,7 @@ func NewSensuGoNotifier(config *SensuGoConfig, ns notifications.WebhookSender, t
APIKey: config.APIKey,
Message: config.Message,
log: log.New("alerting.notifier.sensugo"),
images: images,
ns: ns,
tmpl: t,
}
@@ -134,7 +137,26 @@ func (sn *SensuGoNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool
handlers = []string{tmpl(sn.Handler)}
}
labels := make(map[string]string)
var imageURL string
_ = withStoredImages(ctx, sn.log, sn.images,
func(index int, image *ngmodels.Image) error {
// If there is an image for this alert and the image has been uploaded
// to a public URL then add it to the request. We cannot add more than
// one image per request.
if image != nil && image.URL != "" && imageURL == "" {
imageURL = image.URL
}
return nil
}, as...)
if imageURL != "" {
labels["imageURL"] = imageURL
}
ruleURL := joinUrlPath(sn.tmpl.ExternalURL.String(), "/alerting/list", sn.log)
labels["ruleURL"] = ruleURL
bodyMsgType := map[string]interface{}{
"entity": map[string]interface{}{
"metadata": map[string]interface{}{
@@ -144,10 +166,8 @@ func (sn *SensuGoNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool
},
"check": map[string]interface{}{
"metadata": map[string]interface{}{
"name": check,
"labels": map[string]string{
"ruleURL": ruleURL,
},
"name": check,
"labels": labels,
},
"output": tmpl(sn.Message),
"issued": timeNow().Unix(),
@@ -27,6 +27,9 @@ func TestSensuGoNotifier(t *testing.T) {
require.NoError(t, err)
tmpl.ExternalURL = externalURL
images, deleteFunc := newFakeImageStore(t)
defer deleteFunc()
cases := []struct {
name string
settings string
@@ -42,7 +45,7 @@ func TestSensuGoNotifier(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"},
},
},
},
@@ -57,7 +60,8 @@ func TestSensuGoNotifier(t *testing.T) {
"metadata": map[string]interface{}{
"name": "default",
"labels": map[string]string{
"ruleURL": "http://localhost/alerting/list",
"imageURL": "https://www.example.com/test-image.jpg",
"ruleURL": "http://localhost/alerting/list",
},
},
"output": "**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",
@@ -84,12 +88,12 @@ func TestSensuGoNotifier(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"},
},
}, {
Alert: model.Alert{
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val2"},
Annotations: model.LabelSet{"ann1": "annv2"},
Annotations: model.LabelSet{"ann1": "annv2", "__alertScreenshotToken__": "test-image-2"},
},
},
},
@@ -104,7 +108,8 @@ func TestSensuGoNotifier(t *testing.T) {
"metadata": map[string]interface{}{
"name": "grafana_rule_0",
"labels": map[string]string{
"ruleURL": "http://localhost/alerting/list",
"imageURL": "https://www.example.com/test-image.jpg",
"ruleURL": "http://localhost/alerting/list",
},
},
"output": "2 alerts are firing, 0 are resolved",
@@ -157,7 +162,7 @@ func TestSensuGoNotifier(t *testing.T) {
ctx := notify.WithGroupKey(context.Background(), "alertname")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
sn := NewSensuGoNotifier(cfg, webhookSender, tmpl)
sn := NewSensuGoNotifier(cfg, images, webhookSender, tmpl)
ok, err := sn.Notify(ctx, c.alerts...)
if c.expMsgError != nil {
require.False(t, ok)
@@ -2,12 +2,61 @@ package channels
import (
"context"
"encoding/base64"
"os"
"testing"
"time"
"github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
)
// deleteFunc deletes the fake image.
type deleteFunc func()
// newFakeImageStore creates a fake image on disk and returns an image
// with a token, path and URL. The test should call deleteFunc to delete
// the image from disk at the end of the test.
func newFakeImageStore(t *testing.T) (ImageStore, deleteFunc) {
f, err := os.CreateTemp("", "ngalert-test-image-*.png")
if err != nil {
t.Fatalf("failed to create temp image: %s", err)
}
// 1x1 transparent PNG
b, err := base64.StdEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=")
if err != nil {
t.Fatalf("failed to decode PNG data: %s", err)
}
if _, err := f.Write(b); err != nil {
t.Fatalf("failed to write to file: %s", err)
}
if err := f.Close(); err != nil {
t.Fatalf("failed to close file: %s", err)
}
fn := func() {
if err := os.Remove(f.Name()); err != nil {
t.Fatalf("failed to delete file: %s", err)
}
}
store := &fakeImageStore{
Images: []*ngmodels.Image{
{
Token: "test-image",
Path: f.Name(),
URL: "https://www.example.com/test-image.jpg",
CreatedAt: time.Now().UTC(),
},
},
}
return store, fn
}
// mockTimeNow replaces function timeNow to return constant time.
// It returns a function that resets the variable back to its original value.
// This allows usage of this function with defer: