mirror of
https://github.com/grafana/grafana.git
synced 2024-11-29 20:24:18 -06:00
Alerting: Add image url or file attachment to email notifications. (#49381)
If an image token is present in an alert instance, the email notifier will attempt to find a public URL for the image token. If found, it will add that to the email as the `ImageLink` field. If only local file data is available, the notifier will attach the file to the outgoing email using the `EmbeddedImage` field.
This commit is contained in:
parent
5a3cd45f79
commit
ccd160a75e
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/prometheus/alertmanager/template"
|
||||
@ -24,6 +25,7 @@ type EmailNotifier struct {
|
||||
Message string
|
||||
log log.Logger
|
||||
ns notifications.EmailSender
|
||||
images ImageStore
|
||||
tmpl *template.Template
|
||||
}
|
||||
|
||||
@ -42,7 +44,7 @@ func EmailFactory(fc FactoryConfig) (NotificationChannel, error) {
|
||||
Cfg: *fc.Config,
|
||||
}
|
||||
}
|
||||
return NewEmailNotifier(cfg, fc.NotificationService, fc.Template), nil
|
||||
return NewEmailNotifier(cfg, fc.NotificationService, fc.ImageStore, fc.Template), nil
|
||||
}
|
||||
|
||||
func NewEmailConfig(config *NotificationChannelConfig) (*EmailConfig, error) {
|
||||
@ -62,7 +64,7 @@ func NewEmailConfig(config *NotificationChannelConfig) (*EmailConfig, error) {
|
||||
|
||||
// NewEmailNotifier is the constructor function
|
||||
// for the EmailNotifier.
|
||||
func NewEmailNotifier(config *EmailConfig, ns notifications.EmailSender, t *template.Template) *EmailNotifier {
|
||||
func NewEmailNotifier(config *EmailConfig, ns notifications.EmailSender, images ImageStore, t *template.Template) *EmailNotifier {
|
||||
return &EmailNotifier{
|
||||
Base: NewBase(&models.AlertNotification{
|
||||
Uid: config.UID,
|
||||
@ -76,6 +78,7 @@ func NewEmailNotifier(config *EmailConfig, ns notifications.EmailSender, t *temp
|
||||
Message: config.Message,
|
||||
log: log.New("alerting.notifier.email"),
|
||||
ns: ns,
|
||||
images: images,
|
||||
tmpl: t,
|
||||
}
|
||||
}
|
||||
@ -121,6 +124,41 @@ func (en *EmailNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: modify the email sender code to support multiple file or image URL
|
||||
// fields. We cannot use images from every alert yet.
|
||||
imgToken := getTokenFromAnnotations(as[0].Annotations)
|
||||
if len(imgToken) != 0 {
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, ImageStoreTimeout)
|
||||
imgURL, err := en.images.GetURL(timeoutCtx, imgToken)
|
||||
cancel()
|
||||
if err != nil {
|
||||
if !errors.Is(err, ErrImagesUnavailable) {
|
||||
// Ignore errors. Don't log "ImageUnavailable", which means the storage doesn't exist.
|
||||
en.log.Warn("failed to retrieve image url from store", "error", err)
|
||||
}
|
||||
} else if len(imgURL) > 0 {
|
||||
cmd.Data["ImageLink"] = imgURL
|
||||
} else { // Try to upload
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, ImageStoreTimeout)
|
||||
imgPath, err := en.images.GetFilepath(timeoutCtx, imgToken)
|
||||
cancel()
|
||||
if err != nil {
|
||||
if !errors.Is(err, ErrImagesUnavailable) {
|
||||
// Ignore errors. Don't log "ImageUnavailable", which means the storage doesn't exist.
|
||||
en.log.Warn("failed to retrieve image url from store", "error", err)
|
||||
}
|
||||
} else if len(imgPath) != 0 {
|
||||
file, err := os.Stat(imgPath)
|
||||
if err == nil {
|
||||
cmd.EmbeddedFiles = []string{imgPath}
|
||||
cmd.Data["EmbeddedImage"] = file.Name()
|
||||
} else {
|
||||
en.log.Warn("failed to access email notification image attachment data", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tmplErr != nil {
|
||||
en.log.Warn("failed to template email message", "err", tmplErr.Error())
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ func TestEmailNotifier(t *testing.T) {
|
||||
Settings: settingsJSON,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
emailNotifier := NewEmailNotifier(cfg, emailSender, tmpl)
|
||||
emailNotifier := NewEmailNotifier(cfg, emailSender, &UnavailableImageStore{}, tmpl)
|
||||
|
||||
alerts := []*types.Alert{
|
||||
{
|
||||
@ -289,7 +289,7 @@ func createSut(t *testing.T, messageTmpl string, emailTmpl *template.Template, n
|
||||
Settings: settingsJSON,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
emailNotifier := NewEmailNotifier(cfg, ns, emailTmpl)
|
||||
emailNotifier := NewEmailNotifier(cfg, ns, &UnavailableImageStore{}, emailTmpl)
|
||||
|
||||
return emailNotifier
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ type FactoryConfig struct {
|
||||
// A specialization of store.ImageStore, to avoid an import loop.
|
||||
type ImageStore interface {
|
||||
GetURL(ctx context.Context, token string) (string, error)
|
||||
GetFilepath(ctx context.Context, token string) (string, error)
|
||||
GetData(ctx context.Context, token string) (io.ReadCloser, error)
|
||||
}
|
||||
|
||||
|
@ -51,6 +51,10 @@ func (n *UnavailableImageStore) GetURL(ctx context.Context, token string) (strin
|
||||
return "", ErrImagesUnavailable
|
||||
}
|
||||
|
||||
func (n *UnavailableImageStore) GetFilepath(ctx context.Context, token string) (string, error) {
|
||||
return "", ErrImagesUnavailable
|
||||
}
|
||||
|
||||
func (n *UnavailableImageStore) GetData(ctx context.Context, token string) (io.ReadCloser, error) {
|
||||
return nil, ErrImagesUnavailable
|
||||
}
|
||||
|
@ -23,6 +23,10 @@ func (f *FakeConfigStore) GetURL(ctx context.Context, token string) (string, err
|
||||
return "", store.ErrImageNotFound
|
||||
}
|
||||
|
||||
func (f *FakeConfigStore) GetFilepath(ctx context.Context, token string) (string, error) {
|
||||
return "", store.ErrImageNotFound
|
||||
}
|
||||
|
||||
// Returns an io.ReadCloser that reads out the image data for the provided
|
||||
// token, if available. May return ErrImageNotFound.
|
||||
func (f *FakeConfigStore) GetData(ctx context.Context, token string) (io.ReadCloser, error) {
|
||||
|
@ -41,6 +41,8 @@ type ImageStore interface {
|
||||
|
||||
GetURL(ctx context.Context, token string) (string, error)
|
||||
|
||||
GetFilepath(ctx context.Context, token string) (string, error)
|
||||
|
||||
// Returns an io.ReadCloser that reads out the image data for the provided
|
||||
// token, if available. May return ErrImageNotFound.
|
||||
GetData(ctx context.Context, token string) (io.ReadCloser, error)
|
||||
@ -99,6 +101,14 @@ func (st *DBstore) GetURL(ctx context.Context, token string) (string, error) {
|
||||
return img.URL, nil
|
||||
}
|
||||
|
||||
func (st *DBstore) GetFilepath(ctx context.Context, token string) (string, error) {
|
||||
img, err := st.GetImage(ctx, token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return img.Path, nil
|
||||
}
|
||||
|
||||
func (st *DBstore) GetData(ctx context.Context, token string) (io.ReadCloser, error) {
|
||||
// TODO: Should we support getting data from image.URL? One could configure
|
||||
// the system to upload to S3 while still reading data for notifiers like
|
||||
|
Loading…
Reference in New Issue
Block a user