Alerting: isolate ImageStore in notify package (#60353)

pull/60203/head^2
Yuri Tseretyan 2 years ago committed by GitHub
parent 286af5a53b
commit de008005ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      pkg/services/ngalert/notifier/alertmanager.go
  2. 3
      pkg/services/ngalert/notifier/channels/alertmanager.go
  3. 5
      pkg/services/ngalert/notifier/channels/discord.go
  4. 3
      pkg/services/ngalert/notifier/channels/email.go
  5. 7
      pkg/services/ngalert/notifier/channels/factory.go
  6. 3
      pkg/services/ngalert/notifier/channels/googlechat.go
  7. 26
      pkg/services/ngalert/notifier/channels/images.go
  8. 3
      pkg/services/ngalert/notifier/channels/kafka.go
  9. 3
      pkg/services/ngalert/notifier/channels/opsgenie.go
  10. 3
      pkg/services/ngalert/notifier/channels/pagerduty.go
  11. 3
      pkg/services/ngalert/notifier/channels/pushover.go
  12. 3
      pkg/services/ngalert/notifier/channels/sensugo.go
  13. 9
      pkg/services/ngalert/notifier/channels/slack.go
  14. 3
      pkg/services/ngalert/notifier/channels/slack_test.go
  15. 3
      pkg/services/ngalert/notifier/channels/teams.go
  16. 3
      pkg/services/ngalert/notifier/channels/telegram.go
  17. 11
      pkg/services/ngalert/notifier/channels/testing.go
  18. 3
      pkg/services/ngalert/notifier/channels/threema.go
  19. 10
      pkg/services/ngalert/notifier/channels/util.go
  20. 6
      pkg/services/ngalert/notifier/channels/util_test.go
  21. 3
      pkg/services/ngalert/notifier/channels/victorops.go
  22. 3
      pkg/services/ngalert/notifier/channels/webex.go
  23. 3
      pkg/services/ngalert/notifier/channels/webhook.go
  24. 39
      pkg/services/ngalert/notifier/images.go

@ -514,7 +514,7 @@ func (am *Alertmanager) buildReceiverIntegration(r *apimodels.PostableGrafanaRec
SecureSettings: secureSettings,
}
)
factoryConfig, err := channels.NewFactoryConfig(cfg, NewNotificationSender(am.NotificationService), am.decryptFn, tmpl, am.Store)
factoryConfig, err := channels.NewFactoryConfig(cfg, NewNotificationSender(am.NotificationService), am.decryptFn, tmpl, newImageStore(am.Store))
if err != nil {
return nil, InvalidReceiverError{
Receiver: r,

@ -14,7 +14,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
)
// GetDecryptedValueFn is a function that returns the decrypted value of
@ -104,7 +103,7 @@ func (n *AlertmanagerNotifier) Notify(ctx context.Context, as ...*types.Alert) (
}
_ = withStoredImages(ctx, n.logger, n.images,
func(index int, image ngmodels.Image) error {
func(index int, image Image) error {
// If there is an image for this alert and the image has been uploaded
// to a public URL then include it as an annotation
if image.URL != "" {

@ -19,7 +19,6 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"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/setting"
)
@ -193,7 +192,7 @@ func (d DiscordNotifier) constructAttachments(ctx context.Context, as []*types.A
attachments := make([]discordAttachment, 0)
_ = withStoredImages(ctx, d.log, d.images,
func(index int, image ngmodels.Image) error {
func(index int, image Image) error {
if embedQuota < 1 {
return ErrImagesDone
}
@ -213,7 +212,7 @@ func (d DiscordNotifier) constructAttachments(ctx context.Context, as []*types.A
base := filepath.Base(image.Path)
url := fmt.Sprintf("attachment://%s", base)
reader, err := openImage(image.Path)
if err != nil && !errors.Is(err, ngmodels.ErrImageNotFound) {
if err != nil && !errors.Is(err, ErrImageNotFound) {
d.log.Warn("failed to retrieve image data from store", "error", err)
return nil
}

@ -13,7 +13,6 @@ 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/util"
)
@ -110,7 +109,7 @@ func (en *EmailNotifier) Notify(ctx context.Context, alerts ...*types.Alert) (bo
// Extend alerts data with images, if available.
var embeddedFiles []string
_ = withStoredImages(ctx, en.log, en.images,
func(index int, image ngmodels.Image) error {
func(index int, image Image) error {
if len(image.URL) != 0 {
data.Alerts[index].ImageURL = image.URL
} else if len(image.Path) != 0 {

@ -1,13 +1,10 @@
package channels
import (
"context"
"errors"
"strings"
"github.com/prometheus/alertmanager/template"
"github.com/grafana/grafana/pkg/services/ngalert/models"
)
type FactoryConfig struct {
@ -19,10 +16,6 @@ type FactoryConfig struct {
Template *template.Template
}
type ImageStore interface {
GetImage(ctx context.Context, token string) (*models.Image, error)
}
func NewFactoryConfig(config *NotificationChannelConfig, notificationService NotificationSender,
decryptFunc GetDecryptedValueFn, template *template.Template, imageStore ImageStore) (FactoryConfig, error) {
if config.Settings == nil {

@ -13,7 +13,6 @@ 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/setting"
)
@ -197,7 +196,7 @@ func (gcn *GoogleChatNotifier) buildScreenshotCard(ctx context.Context, alerts [
}
_ = withStoredImages(ctx, gcn.log, gcn.images,
func(index int, image ngmodels.Image) error {
func(index int, image Image) error {
if len(image.URL) == 0 {
return nil
}

@ -0,0 +1,26 @@
package channels
import (
"context"
"errors"
"time"
)
var (
ErrImageNotFound = errors.New("image not found")
)
type Image struct {
Token string
Path string
URL string
CreatedAt time.Time
}
func (i Image) HasURL() bool {
return i.URL != ""
}
type ImageStore interface {
GetImage(ctx context.Context, token string) (*Image, error)
}

@ -13,7 +13,6 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
)
// KafkaNotifier is responsible for sending
@ -161,7 +160,7 @@ func buildState(as ...*types.Alert) models.AlertStateType {
func buildContextImages(ctx context.Context, l log.Logger, imageStore ImageStore, as ...*types.Alert) []interface{} {
var contexts []interface{}
_ = withStoredImages(ctx, l, imageStore,
func(_ int, image ngmodels.Image) error {
func(_ int, image Image) error {
if image.URL != "" {
imageJSON := simplejson.New()
imageJSON.Set("type", "image")

@ -17,7 +17,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
)
const (
@ -246,7 +245,7 @@ func (on *OpsgenieNotifier) buildOpsgenieMessage(ctx context.Context, alerts mod
}
var images []string
_ = withStoredImages(ctx, on.log, on.images,
func(_ int, image ngmodels.Image) error {
func(_ int, image Image) error {
if len(image.URL) == 0 {
return nil
}

@ -15,7 +15,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
)
const (
@ -231,7 +230,7 @@ func (pn *PagerdutyNotifier) buildPagerdutyMessage(ctx context.Context, alerts m
}
_ = withStoredImages(ctx, pn.log, pn.images,
func(_ int, image ngmodels.Image) error {
func(_ int, image Image) error {
if len(image.URL) != 0 {
msg.Images = append(msg.Images, pagerDutyImage{Src: image.URL})
}

@ -19,7 +19,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
)
const (
@ -316,7 +315,7 @@ func (pn *PushoverNotifier) genPushoverBody(ctx context.Context, as ...*types.Al
func (pn *PushoverNotifier) writeImageParts(ctx context.Context, w *multipart.Writer, as ...*types.Alert) {
// 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 {
_ = withStoredImages(ctx, pn.log, pn.images, func(index int, image Image) error {
f, err := os.Open(image.Path)
if err != nil {
return fmt.Errorf("failed to open the image: %w", err)

@ -13,7 +13,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
)
type SensuGoNotifier struct {
@ -127,7 +126,7 @@ func (sn *SensuGoNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool
labels := make(map[string]string)
_ = withStoredImages(ctx, sn.log, sn.images,
func(_ int, image ngmodels.Image) error {
func(_ int, image 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.

@ -24,7 +24,6 @@ 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/setting"
)
@ -220,7 +219,7 @@ func (sn *SlackNotifier) Notify(ctx context.Context, alerts ...*types.Alert) (bo
// Do not upload images if using an incoming webhook as incoming webhooks cannot upload files
if !isIncomingWebhook(sn.settings) {
if err := withStoredImages(ctx, sn.log, sn.images, func(index int, image ngmodels.Image) error {
if err := withStoredImages(ctx, sn.log, sn.images, func(index int, image Image) error {
// If we have exceeded the maximum number of images for this thread_ts
// then tell the recipient and stop iterating subsequent images
if index >= maxImagesPerThreadTs {
@ -393,7 +392,7 @@ func (sn *SlackNotifier) createSlackMessage(ctx context.Context, alerts []*types
if isIncomingWebhook(sn.settings) {
// Incoming webhooks cannot upload files, instead share images via their URL
_ = withStoredImages(ctx, sn.log, sn.images, func(index int, image ngmodels.Image) error {
_ = withStoredImages(ctx, sn.log, sn.images, func(index int, image Image) error {
if image.URL != "" {
req.Attachments[0].ImageURL = image.URL
return ErrImagesDone
@ -476,7 +475,7 @@ func (sn *SlackNotifier) sendSlackMessage(ctx context.Context, m *slackMessage)
// createImageMultipart returns the mutlipart/form-data request and headers for files.upload.
// It returns an error if the image does not exist or there was an error preparing the
// multipart form.
func (sn *SlackNotifier) createImageMultipart(image ngmodels.Image, channel, comment, thread_ts string) (http.Header, []byte, error) {
func (sn *SlackNotifier) createImageMultipart(image Image, channel, comment, thread_ts string) (http.Header, []byte, error) {
buf := bytes.Buffer{}
w := multipart.NewWriter(&buf)
defer func() {
@ -553,7 +552,7 @@ func (sn *SlackNotifier) sendMultipart(ctx context.Context, headers http.Header,
// uploadImage shares the image to the channel names or IDs. It returns an error if the file
// does not exist, or if there was an error either preparing or sending the multipart/form-data
// request.
func (sn *SlackNotifier) uploadImage(ctx context.Context, image ngmodels.Image, channel, comment, thread_ts string) error {
func (sn *SlackNotifier) uploadImage(ctx context.Context, image Image, channel, comment, thread_ts string) error {
sn.log.Debug("Uploadimg image", "image", image.Token)
headers, data, err := sn.createImageMultipart(image, channel, comment, thread_ts)
if err != nil {

@ -21,7 +21,6 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/setting"
@ -416,7 +415,7 @@ func setupSlackForTests(t *testing.T, settings string) (*SlackNotifier, *slackRe
})
images := &fakeImageStore{
Images: []*models.Image{{
Images: []*Image{{
Token: "image-on-disk",
Path: f.Name(),
}, {

@ -13,7 +13,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
)
const (
@ -309,7 +308,7 @@ func (tn *TeamsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
var s AdaptiveCardImageSetItem
_ = withStoredImages(ctx, tn.log, tn.images,
func(_ int, image ngmodels.Image) error {
func(_ int, image Image) error {
if image.URL != "" {
s.AppendImage(AdaptiveCardImageItem{URL: image.URL})
}

@ -16,7 +16,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
)
var (
@ -144,7 +143,7 @@ func (tn *TelegramNotifier) Notify(ctx context.Context, as ...*types.Alert) (boo
}
// Create the cmd to upload each image
_ = withStoredImages(ctx, tn.log, tn.images, func(index int, image ngmodels.Image) error {
_ = withStoredImages(ctx, tn.log, tn.images, func(index int, image Image) error {
cmd, err = tn.newWebhookSyncCmd("sendPhoto", func(w *multipart.Writer) error {
f, err := os.Open(image.Path)
if err != nil {

@ -13,23 +13,22 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/tracing"
"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/setting"
)
type fakeImageStore struct {
Images []*ngmodels.Image
Images []*Image
}
// getImage returns an image with the same token.
func (f *fakeImageStore) GetImage(_ context.Context, token string) (*ngmodels.Image, error) {
func (f *fakeImageStore) GetImage(_ context.Context, token string) (*Image, error) {
for _, img := range f.Images {
if img.Token == token {
return img, nil
}
}
return nil, ngmodels.ErrImageNotFound
return nil, ErrImageNotFound
}
// newFakeImageStore returns an image store with N test images.
@ -37,7 +36,7 @@ func (f *fakeImageStore) GetImage(_ context.Context, token string) (*ngmodels.Im
func newFakeImageStore(n int) ImageStore {
s := fakeImageStore{}
for i := 1; i <= n; i++ {
s.Images = append(s.Images, &ngmodels.Image{
s.Images = append(s.Images, &Image{
Token: fmt.Sprintf("test-image-%d", i),
URL: fmt.Sprintf("https://www.example.com/test-image-%d.jpg", i),
CreatedAt: time.Now().UTC(),
@ -72,7 +71,7 @@ func newFakeImageStoreWithFile(t *testing.T, n int) ImageStore {
t.Fatalf("failed to create test image: %s", err)
}
files = append(files, file)
s.Images = append(s.Images, &ngmodels.Image{
s.Images = append(s.Images, &Image{
Token: fmt.Sprintf("test-image-%d", i),
Path: file,
URL: fmt.Sprintf("https://www.example.com/test-image-%d", i),

@ -14,7 +14,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
)
var (
@ -158,7 +157,7 @@ func (tn *ThreemaNotifier) buildMessage(ctx context.Context, as ...*types.Alert)
}
_ = withStoredImages(ctx, tn.log, tn.images,
func(_ int, image ngmodels.Image) error {
func(_ int, image Image) error {
if image.URL != "" {
message += fmt.Sprintf("*Image:* %s\n", image.URL)
}

@ -49,11 +49,11 @@ var (
ErrImagesUnavailable = errors.New("alert screenshots are unavailable")
)
type forEachImageFunc func(index int, image models.Image) error
type forEachImageFunc func(index int, image Image) error
// getImage returns the image for the alert or an error. It returns a nil
// image if the alert does not have an image token or the image does not exist.
func getImage(ctx context.Context, l log.Logger, imageStore ImageStore, alert types.Alert) (*models.Image, error) {
func getImage(ctx context.Context, l log.Logger, imageStore ImageStore, alert types.Alert) (*Image, error) {
token := getTokenFromAnnotations(alert.Annotations)
if token == "" {
return nil, nil
@ -63,7 +63,7 @@ func getImage(ctx context.Context, l log.Logger, imageStore ImageStore, alert ty
defer cancelFunc()
img, err := imageStore.GetImage(ctx, token)
if errors.Is(err, models.ErrImageNotFound) || errors.Is(err, ErrImagesUnavailable) {
if errors.Is(err, ErrImageNotFound) || errors.Is(err, ErrImagesUnavailable) {
return nil, nil
} else if err != nil {
l.Warn("failed to get image with token", "token", token, "error", err)
@ -107,7 +107,7 @@ func openImage(path string) (io.ReadCloser, error) {
fp := filepath.Clean(path)
_, err := os.Stat(fp)
if os.IsNotExist(err) || os.IsPermission(err) {
return nil, models.ErrImageNotFound
return nil, ErrImageNotFound
}
f, err := os.Open(fp)
@ -128,7 +128,7 @@ func getTokenFromAnnotations(annotations model.LabelSet) string {
type UnavailableImageStore struct{}
// Get returns the image with the corresponding token, or ErrImageNotFound.
func (u *UnavailableImageStore) GetImage(ctx context.Context, token string) (*models.Image, error) {
func (u *UnavailableImageStore) GetImage(ctx context.Context, token string) (*Image, error) {
return nil, ErrImagesUnavailable
}

@ -29,7 +29,7 @@ func TestWithStoredImages(t *testing.T) {
},
},
}}
imageStore := &fakeImageStore{Images: []*models.Image{{
imageStore := &fakeImageStore{Images: []*Image{{
Token: "test-image-1",
URL: "https://www.example.com/test-image-1.jpg",
CreatedAt: time.Now().UTC(),
@ -45,7 +45,7 @@ func TestWithStoredImages(t *testing.T) {
)
// should iterate all images
err = withStoredImages(ctx, log.New(ctx), imageStore, func(index int, image models.Image) error {
err = withStoredImages(ctx, log.New(ctx), imageStore, func(index int, image Image) error {
i += 1
return nil
}, alerts...)
@ -54,7 +54,7 @@ func TestWithStoredImages(t *testing.T) {
// should iterate just the first image
i = 0
err = withStoredImages(ctx, log.New(ctx), imageStore, func(index int, image models.Image) error {
err = withStoredImages(ctx, log.New(ctx), imageStore, func(index int, image Image) error {
i += 1
return ErrImagesDone
}, alerts...)

@ -15,7 +15,6 @@ 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/setting"
)
@ -139,7 +138,7 @@ func (vn *VictoropsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bo
}
_ = withStoredImages(ctx, vn.log, vn.images,
func(index int, image ngmodels.Image) error {
func(index int, image Image) error {
if image.URL != "" {
bodyJSON["image_url"] = image.URL
return ErrImagesDone

@ -12,7 +12,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
)
const webexAPIURL = "https://webexapis.com/v1/messages"
@ -130,7 +129,7 @@ func (wn *WebexNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
}
// Augment our Alert data with ImageURLs if available.
_ = withStoredImages(ctx, wn.log, wn.images, func(index int, image ngmodels.Image) error {
_ = withStoredImages(ctx, wn.log, wn.images, func(index int, image Image) error {
// Cisco Webex only supports a single image per request: https://developer.webex.com/docs/basics#message-attachments
if image.HasURL() {
data.Alerts[index].ImageURL = image.URL

@ -15,7 +15,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
)
// WebhookNotifier is responsible for sending
@ -160,7 +159,7 @@ func (wn *WebhookNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool
// Augment our Alert data with ImageURLs if available.
_ = withStoredImages(ctx, wn.log, wn.images,
func(index int, image ngmodels.Image) error {
func(index int, image Image) error {
if len(image.URL) != 0 {
data.Alerts[index].ImageURL = image.URL
}

@ -0,0 +1,39 @@
package notifier
import (
"context"
"errors"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/notifier/channels"
"github.com/grafana/grafana/pkg/services/ngalert/store"
)
type imageStore struct {
store store.ImageStore
}
func newImageStore(store store.ImageStore) channels.ImageStore {
return &imageStore{
store: store,
}
}
func (i imageStore) GetImage(ctx context.Context, token string) (*channels.Image, error) {
image, err := i.store.GetImage(ctx, token)
if err != nil {
if errors.Is(err, models.ErrImageNotFound) {
err = channels.ErrImageNotFound
}
}
var result *channels.Image
if image != nil {
result = &channels.Image{
Token: image.Token,
Path: image.Path,
URL: image.URL,
CreatedAt: image.CreatedAt,
}
}
return result, err
}
Loading…
Cancel
Save