mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -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"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
|
||||||
"github.com/prometheus/alertmanager/template"
|
"github.com/prometheus/alertmanager/template"
|
||||||
@ -24,6 +25,7 @@ type EmailNotifier struct {
|
|||||||
Message string
|
Message string
|
||||||
log log.Logger
|
log log.Logger
|
||||||
ns notifications.EmailSender
|
ns notifications.EmailSender
|
||||||
|
images ImageStore
|
||||||
tmpl *template.Template
|
tmpl *template.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,7 +44,7 @@ func EmailFactory(fc FactoryConfig) (NotificationChannel, error) {
|
|||||||
Cfg: *fc.Config,
|
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) {
|
func NewEmailConfig(config *NotificationChannelConfig) (*EmailConfig, error) {
|
||||||
@ -62,7 +64,7 @@ func NewEmailConfig(config *NotificationChannelConfig) (*EmailConfig, error) {
|
|||||||
|
|
||||||
// NewEmailNotifier is the constructor function
|
// NewEmailNotifier is the constructor function
|
||||||
// for the EmailNotifier.
|
// 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{
|
return &EmailNotifier{
|
||||||
Base: NewBase(&models.AlertNotification{
|
Base: NewBase(&models.AlertNotification{
|
||||||
Uid: config.UID,
|
Uid: config.UID,
|
||||||
@ -76,6 +78,7 @@ func NewEmailNotifier(config *EmailConfig, ns notifications.EmailSender, t *temp
|
|||||||
Message: config.Message,
|
Message: config.Message,
|
||||||
log: log.New("alerting.notifier.email"),
|
log: log.New("alerting.notifier.email"),
|
||||||
ns: ns,
|
ns: ns,
|
||||||
|
images: images,
|
||||||
tmpl: t,
|
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 {
|
if tmplErr != nil {
|
||||||
en.log.Warn("failed to template email message", "err", tmplErr.Error())
|
en.log.Warn("failed to template email message", "err", tmplErr.Error())
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ func TestEmailNotifier(t *testing.T) {
|
|||||||
Settings: settingsJSON,
|
Settings: settingsJSON,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
emailNotifier := NewEmailNotifier(cfg, emailSender, tmpl)
|
emailNotifier := NewEmailNotifier(cfg, emailSender, &UnavailableImageStore{}, tmpl)
|
||||||
|
|
||||||
alerts := []*types.Alert{
|
alerts := []*types.Alert{
|
||||||
{
|
{
|
||||||
@ -289,7 +289,7 @@ func createSut(t *testing.T, messageTmpl string, emailTmpl *template.Template, n
|
|||||||
Settings: settingsJSON,
|
Settings: settingsJSON,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
emailNotifier := NewEmailNotifier(cfg, ns, emailTmpl)
|
emailNotifier := NewEmailNotifier(cfg, ns, &UnavailableImageStore{}, emailTmpl)
|
||||||
|
|
||||||
return emailNotifier
|
return emailNotifier
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ type FactoryConfig struct {
|
|||||||
// A specialization of store.ImageStore, to avoid an import loop.
|
// A specialization of store.ImageStore, to avoid an import loop.
|
||||||
type ImageStore interface {
|
type ImageStore interface {
|
||||||
GetURL(ctx context.Context, token string) (string, error)
|
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)
|
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
|
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) {
|
func (n *UnavailableImageStore) GetData(ctx context.Context, token string) (io.ReadCloser, error) {
|
||||||
return nil, ErrImagesUnavailable
|
return nil, ErrImagesUnavailable
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,10 @@ func (f *FakeConfigStore) GetURL(ctx context.Context, token string) (string, err
|
|||||||
return "", store.ErrImageNotFound
|
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
|
// Returns an io.ReadCloser that reads out the image data for the provided
|
||||||
// token, if available. May return ErrImageNotFound.
|
// token, if available. May return ErrImageNotFound.
|
||||||
func (f *FakeConfigStore) GetData(ctx context.Context, token string) (io.ReadCloser, error) {
|
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)
|
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
|
// Returns an io.ReadCloser that reads out the image data for the provided
|
||||||
// token, if available. May return ErrImageNotFound.
|
// token, if available. May return ErrImageNotFound.
|
||||||
GetData(ctx context.Context, token string) (io.ReadCloser, error)
|
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
|
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) {
|
func (st *DBstore) GetData(ctx context.Context, token string) (io.ReadCloser, error) {
|
||||||
// TODO: Should we support getting data from image.URL? One could configure
|
// 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
|
// the system to upload to S3 while still reading data for notifiers like
|
||||||
|
Loading…
Reference in New Issue
Block a user