From 58de0dabd40fba8ba7563498b82ec9d8f78914de Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 24 Apr 2020 11:35:55 +0200 Subject: [PATCH] Alerting: Upload error image when image renderer unavailable (#23713) When Include image is enabled for an alert notification channel, but there's no image renderer available/installed when sending notification an error image will be uploaded/attached explaining that you need to install the Grafana Image Renderer plugin. Ref #13802 Co-Authored-By: Arve Knudsen --- pkg/services/alerting/notifier.go | 31 ++++++++---------- pkg/services/alerting/notifier_test.go | 6 ++-- pkg/services/rendering/rendering.go | 15 +++++++-- public/img/rendering_plugin_not_installed.png | Bin 3651 -> 9095 bytes 4 files changed, 29 insertions(+), 23 deletions(-) diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index d848ef405bf..951b3d2aa3d 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -53,25 +53,19 @@ func (n *notificationService) SendIfNeeded(evalCtx *EvalContext) error { } if notifierStates.ShouldUploadImage() { - if n.renderService.IsAvailable() { - // Create a copy of EvalContext and give it a new, shorter, timeout context to upload the image - uploadEvalCtx := *evalCtx - timeout := setting.AlertingNotificationTimeout / 2 - var uploadCtxCancel func() - uploadEvalCtx.Ctx, uploadCtxCancel = context.WithTimeout(evalCtx.Ctx, timeout) - - // Try to upload the image without consuming all the time allocated for EvalContext - if err = n.renderAndUploadImage(&uploadEvalCtx, timeout); err != nil { - n.log.Error("Failed to render and upload alert panel image.", "ruleId", uploadEvalCtx.Rule.ID, "error", err) - } - uploadCtxCancel() - evalCtx.ImageOnDiskPath = uploadEvalCtx.ImageOnDiskPath - evalCtx.ImagePublicURL = uploadEvalCtx.ImagePublicURL - } else { - n.log.Warn("Could not render image for alert notification, no image renderer found/installed. " + - "For image rendering support please install the grafana-image-renderer plugin. " + - "Read more at https://grafana.com/docs/grafana/latest/administration/image_rendering/") + // Create a copy of EvalContext and give it a new, shorter, timeout context to upload the image + uploadEvalCtx := *evalCtx + timeout := setting.AlertingNotificationTimeout / 2 + var uploadCtxCancel func() + uploadEvalCtx.Ctx, uploadCtxCancel = context.WithTimeout(evalCtx.Ctx, timeout) + + // Try to upload the image without consuming all the time allocated for EvalContext + if err = n.renderAndUploadImage(&uploadEvalCtx, timeout); err != nil { + n.log.Error("Failed to render and upload alert panel image.", "ruleId", uploadEvalCtx.Rule.ID, "error", err) } + uploadCtxCancel() + evalCtx.ImageOnDiskPath = uploadEvalCtx.ImageOnDiskPath + evalCtx.ImagePublicURL = uploadEvalCtx.ImagePublicURL } return n.sendNotifications(evalCtx, notifierStates) @@ -174,6 +168,7 @@ func (n *notificationService) renderAndUploadImage(evalCtx *EvalContext, timeout n.log.Debug("Rendered alert panel image", "ruleId", evalCtx.Rule.ID, "path", result.FilePath, "took", took) evalCtx.ImageOnDiskPath = result.FilePath + n.log.Debug("Uploading alert panel image to external image store", "ruleId", evalCtx.Rule.ID, "path", evalCtx.ImageOnDiskPath) start = time.Now() diff --git a/pkg/services/alerting/notifier_test.go b/pkg/services/alerting/notifier_test.go index 27712789c4f..e885365864b 100644 --- a/pkg/services/alerting/notifier_test.go +++ b/pkg/services/alerting/notifier_test.go @@ -39,13 +39,13 @@ func TestNotificationService(t *testing.T) { require.Truef(t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't") }) - notificationServiceScenario(t, "Given alert rule with upload image enabled but no renderer available should not render and upload image, but send notification", evalCtx, true, func(scenarioCtx *scenarioContext) { + notificationServiceScenario(t, "Given alert rule with upload image enabled but no renderer available should render and upload unavailable image and send notification", evalCtx, true, func(scenarioCtx *scenarioContext) { scenarioCtx.rendererAvailable = false err := scenarioCtx.notificationService.SendIfNeeded(evalCtx) require.NoError(t, err) - require.Equalf(t, 0, scenarioCtx.renderCount, "expected render to not be called, but it was") - require.Equalf(t, 0, scenarioCtx.imageUploadCount, "expected image to not be uploaded, but it was") + require.Equalf(t, 1, scenarioCtx.renderCount, "expected render to be called, but it wasn't") + require.Equalf(t, 1, scenarioCtx.imageUploadCount, "expected image to be uploaded, but it wasn't") require.Truef(t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't") }) diff --git a/pkg/services/rendering/rendering.go b/pkg/services/rendering/rendering.go index b00d431228e..04f442104f0 100644 --- a/pkg/services/rendering/rendering.go +++ b/pkg/services/rendering/rendering.go @@ -121,6 +121,14 @@ func (rs *RenderingService) RenderErrorImage(err error) (*RenderResult, error) { }, nil } +func (rs *RenderingService) renderUnavailableImage() *RenderResult { + imgPath := "public/img/rendering_plugin_not_installed.png" + + return &RenderResult{ + FilePath: filepath.Join(setting.HomePath, imgPath), + } +} + func (rs *RenderingService) Render(ctx context.Context, opts Opts) (*RenderResult, error) { if rs.inProgressCount > opts.ConcurrentLimit { return &RenderResult{ @@ -128,8 +136,11 @@ func (rs *RenderingService) Render(ctx context.Context, opts Opts) (*RenderResul }, nil } - if rs.renderAction == nil { - return nil, fmt.Errorf("no renderer found") + if !rs.IsAvailable() { + rs.log.Warn("Could not render image, no image renderer found/installed. " + + "For image rendering support please install the grafana-image-renderer plugin. " + + "Read more at https://grafana.com/docs/grafana/latest/administration/image_rendering/") + return rs.renderUnavailableImage(), nil } rs.log.Info("Rendering", "path", opts.Path) diff --git a/public/img/rendering_plugin_not_installed.png b/public/img/rendering_plugin_not_installed.png index f135ff7cc9f9b3898396404cfe1b510fd81bbc42..0445996d40dc86e979092819aa985fe6df35029b 100644 GIT binary patch literal 9095 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 VMf+a&<>Wta6l7Ip>ZQy={{v@Wt)&0} literal 3651 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 UD64y-2ma-(&JfL*r##9320j&!NB{r;