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:
Joe Blubaugh 2022-05-23 23:08:28 +08:00 committed by GitHub
parent 5a3cd45f79
commit ccd160a75e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 61 additions and 4 deletions

View File

@ -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())
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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) {

View File

@ -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