Просмотр исходного кода

feat(alerting): working on alert notification and image rendering

Torkel Ödegaard 9 лет назад
Родитель
Сommit
2b276d5cd1

+ 3 - 10
pkg/components/imguploader/imguploader.go

@@ -3,7 +3,6 @@ package imguploader
 import (
 import (
 	"io/ioutil"
 	"io/ioutil"
 	"net/http"
 	"net/http"
-	"time"
 
 
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/util"
 	"github.com/grafana/grafana/pkg/util"
@@ -11,7 +10,7 @@ import (
 )
 )
 
 
 type Uploader interface {
 type Uploader interface {
-	Upload(imgUrl string) (string, error)
+	Upload(path string) (string, error)
 }
 }
 
 
 type S3Uploader struct {
 type S3Uploader struct {
@@ -28,13 +27,7 @@ func NewS3Uploader(bucket, accessKey, secretKey string) *S3Uploader {
 	}
 	}
 }
 }
 
 
-func (u *S3Uploader) Upload(imgUrl string) (string, error) {
-	client := http.Client{Timeout: time.Duration(60 * time.Second)}
-
-	res, err := client.Get(imgUrl)
-	if err != nil {
-		return "", err
-	}
+func (u *S3Uploader) Upload(path string) (string, error) {
 
 
 	s3util.DefaultConfig.AccessKey = u.accessKey
 	s3util.DefaultConfig.AccessKey = u.accessKey
 	s3util.DefaultConfig.SecretKey = u.secretKey
 	s3util.DefaultConfig.SecretKey = u.secretKey
@@ -53,7 +46,7 @@ func (u *S3Uploader) Upload(imgUrl string) (string, error) {
 
 
 	defer writer.Close()
 	defer writer.Close()
 
 
-	imgData, err := ioutil.ReadAll(res.Body)
+	imgData, err := ioutil.ReadFile(path)
 	if err != nil {
 	if err != nil {
 		return "", err
 		return "", err
 	}
 	}

+ 7 - 3
pkg/components/renderer/renderer.go

@@ -9,10 +9,11 @@ import (
 	"runtime"
 	"runtime"
 	"time"
 	"time"
 
 
+	"strconv"
+
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/util"
 	"github.com/grafana/grafana/pkg/util"
-	"strconv"
 )
 )
 
 
 type RenderOpts struct {
 type RenderOpts struct {
@@ -23,8 +24,10 @@ type RenderOpts struct {
 	Timeout   string
 	Timeout   string
 }
 }
 
 
+var rendererLog log.Logger = log.New("png-renderer")
+
 func RenderToPng(params *RenderOpts) (string, error) {
 func RenderToPng(params *RenderOpts) (string, error) {
-	log.Info("PhantomRenderer::renderToPng url %v", params.Url)
+	rendererLog.Info("Rendering", "url", params.Url)
 
 
 	var executable = "phantomjs"
 	var executable = "phantomjs"
 	if runtime.GOOS == "windows" {
 	if runtime.GOOS == "windows" {
@@ -71,11 +74,12 @@ func RenderToPng(params *RenderOpts) (string, error) {
 	select {
 	select {
 	case <-time.After(time.Duration(timeout) * time.Second):
 	case <-time.After(time.Duration(timeout) * time.Second):
 		if err := cmd.Process.Kill(); err != nil {
 		if err := cmd.Process.Kill(); err != nil {
-			log.Error(4, "failed to kill: %v", err)
+			rendererLog.Error("failed to kill", "error", err)
 		}
 		}
 		return "", fmt.Errorf("PhantomRenderer::renderToPng timeout (>%vs)", timeout)
 		return "", fmt.Errorf("PhantomRenderer::renderToPng timeout (>%vs)", timeout)
 	case <-done:
 	case <-done:
 	}
 	}
 
 
+	rendererLog.Debug("Image rendered", "path", pngPath)
 	return pngPath, nil
 	return pngPath, nil
 }
 }

+ 50 - 12
pkg/services/alerting/eval_context.go

@@ -1,25 +1,31 @@
 package alerting
 package alerting
 
 
 import (
 import (
+	"fmt"
 	"time"
 	"time"
 
 
+	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/setting"
 )
 )
 
 
 type EvalContext struct {
 type EvalContext struct {
-	Firing      bool
-	IsTestRun   bool
-	Events      []*Event
-	Logs        []*ResultLogEntry
-	Error       error
-	Description string
-	StartTime   time.Time
-	EndTime     time.Time
-	Rule        *Rule
-	DoneChan    chan bool
-	CancelChan  chan bool
-	log         log.Logger
+	Firing          bool
+	IsTestRun       bool
+	Events          []*Event
+	Logs            []*ResultLogEntry
+	Error           error
+	Description     string
+	StartTime       time.Time
+	EndTime         time.Time
+	Rule            *Rule
+	DoneChan        chan bool
+	CancelChan      chan bool
+	log             log.Logger
+	dashboardSlug   string
+	ImagePublicUrl  string
+	ImageOnDiskPath string
 }
 }
 
 
 func (a *EvalContext) GetDurationMs() float64 {
 func (a *EvalContext) GetDurationMs() float64 {
@@ -50,6 +56,38 @@ func (c *EvalContext) GetStateText() string {
 	}
 	}
 }
 }
 
 
+func (c *EvalContext) getDashboardSlug() (string, error) {
+	if c.dashboardSlug != "" {
+		return c.dashboardSlug, nil
+	}
+
+	slugQuery := &m.GetDashboardSlugByIdQuery{Id: c.Rule.DashboardId}
+	if err := bus.Dispatch(slugQuery); err != nil {
+		return "", err
+	}
+
+	c.dashboardSlug = slugQuery.Result
+	return c.dashboardSlug, nil
+}
+
+func (c *EvalContext) GetRuleUrl() (string, error) {
+	if slug, err := c.getDashboardSlug(); err != nil {
+		return "", err
+	} else {
+		ruleUrl := fmt.Sprintf("%sdashboard/db/%s?fullscreen&edit&tab=alert&panelId=%d", setting.AppUrl, slug, c.Rule.PanelId)
+		return ruleUrl, nil
+	}
+}
+
+func (c *EvalContext) GetImageUrl() (string, error) {
+	if slug, err := c.getDashboardSlug(); err != nil {
+		return "", err
+	} else {
+		ruleUrl := fmt.Sprintf("%sdashboard-solo/db/%s?&panelId=%d", setting.AppUrl, slug, c.Rule.PanelId)
+		return ruleUrl, nil
+	}
+}
+
 func NewEvalContext(rule *Rule) *EvalContext {
 func NewEvalContext(rule *Rule) *EvalContext {
 	return &EvalContext{
 	return &EvalContext{
 		StartTime:  time.Now(),
 		StartTime:  time.Now(),

+ 42 - 0
pkg/services/alerting/notifier.go

@@ -4,8 +4,11 @@ import (
 	"errors"
 	"errors"
 
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/components/imguploader"
+	"github.com/grafana/grafana/pkg/components/renderer"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/setting"
 )
 )
 
 
 type RootNotifier struct {
 type RootNotifier struct {
@@ -35,12 +38,51 @@ func (n *RootNotifier) Notify(context *EvalContext) {
 		return
 		return
 	}
 	}
 
 
+	err = n.uploadImage(context)
+	if err != nil {
+		n.log.Error("Failed to upload alert panel image", "error", err)
+	}
+
 	for _, notifier := range notifiers {
 	for _, notifier := range notifiers {
 		n.log.Info("Sending notification", "firing", context.Firing, "type", notifier.GetType())
 		n.log.Info("Sending notification", "firing", context.Firing, "type", notifier.GetType())
 		go notifier.Notify(context)
 		go notifier.Notify(context)
 	}
 	}
 }
 }
 
 
+func (n *RootNotifier) uploadImage(context *EvalContext) error {
+	uploader := imguploader.NewS3Uploader(
+		setting.S3TempImageStoreBucketUrl,
+		setting.S3TempImageStoreAccessKey,
+		setting.S3TempImageStoreSecretKey)
+
+	imageUrl, err := context.GetImageUrl()
+	if err != nil {
+		return err
+	}
+
+	renderOpts := &renderer.RenderOpts{
+		Url:       imageUrl,
+		Width:     "800",
+		Height:    "400",
+		SessionId: "123",
+		Timeout:   "10",
+	}
+
+	if imagePath, err := renderer.RenderToPng(renderOpts); err != nil {
+		return err
+	} else {
+		context.ImageOnDiskPath = imagePath
+	}
+
+	context.ImagePublicUrl, err = uploader.Upload(context.ImageOnDiskPath)
+	if err != nil {
+		return err
+	}
+
+	n.log.Info("uploaded", "url", context.ImagePublicUrl)
+	return nil
+}
+
 func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64) ([]Notifier, error) {
 func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64) ([]Notifier, error) {
 	query := &m.GetAlertNotificationsQuery{OrgId: orgId, Ids: notificationIds}
 	query := &m.GetAlertNotificationsQuery{OrgId: orgId, Ids: notificationIds}
 
 

+ 0 - 19
pkg/services/alerting/notifiers/common.go

@@ -1,20 +1 @@
 package notifiers
 package notifiers
-
-import (
-	"fmt"
-
-	"github.com/grafana/grafana/pkg/bus"
-	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/services/alerting"
-	"github.com/grafana/grafana/pkg/setting"
-)
-
-func getRuleLink(rule *alerting.Rule) (string, error) {
-	slugQuery := &m.GetDashboardSlugByIdQuery{Id: rule.DashboardId}
-	if err := bus.Dispatch(slugQuery); err != nil {
-		return "", err
-	}
-
-	ruleLink := fmt.Sprintf("%sdashboard/db/%s?fullscreen&edit&tab=alert&panelId=%d", setting.AppUrl, slugQuery.Result, rule.PanelId)
-	return ruleLink, nil
-}

+ 2 - 2
pkg/services/alerting/notifiers/email.go

@@ -39,7 +39,7 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
 func (this *EmailNotifier) Notify(context *alerting.EvalContext) {
 func (this *EmailNotifier) Notify(context *alerting.EvalContext) {
 	this.log.Info("Sending alert notification to", "addresses", this.Addresses)
 	this.log.Info("Sending alert notification to", "addresses", this.Addresses)
 
 
-	ruleLink, err := getRuleLink(context.Rule)
+	ruleUrl, err := context.GetRuleUrl()
 	if err != nil {
 	if err != nil {
 		this.log.Error("Failed get rule link", "error", err)
 		this.log.Error("Failed get rule link", "error", err)
 		return
 		return
@@ -50,7 +50,7 @@ func (this *EmailNotifier) Notify(context *alerting.EvalContext) {
 			"RuleState": context.Rule.State,
 			"RuleState": context.Rule.State,
 			"RuleName":  context.Rule.Name,
 			"RuleName":  context.Rule.Name,
 			"Severity":  context.Rule.Severity,
 			"Severity":  context.Rule.Severity,
-			"RuleLink":  ruleLink,
+			"RuleUrl":   ruleUrl,
 		},
 		},
 		To:       this.Addresses,
 		To:       this.Addresses,
 		Template: "alert_notification.html",
 		Template: "alert_notification.html",

+ 4 - 4
pkg/services/alerting/notifiers/slack.go

@@ -41,7 +41,7 @@ func (this *SlackNotifier) Notify(context *alerting.EvalContext) {
 
 
 	rule := context.Rule
 	rule := context.Rule
 
 
-	ruleLink, err := getRuleLink(rule)
+	ruleUrl, err := context.GetRuleUrl()
 	if err != nil {
 	if err != nil {
 		this.log.Error("Failed get rule link", "error", err)
 		this.log.Error("Failed get rule link", "error", err)
 		return
 		return
@@ -69,10 +69,10 @@ func (this *SlackNotifier) Notify(context *alerting.EvalContext) {
 				// "author_link": "http://flickr.com/bobby/",
 				// "author_link": "http://flickr.com/bobby/",
 				// "author_icon": "http://flickr.com/icons/bobby.jpg",
 				// "author_icon": "http://flickr.com/icons/bobby.jpg",
 				"title":      "[" + context.GetStateText() + "] " + rule.Name,
 				"title":      "[" + context.GetStateText() + "] " + rule.Name,
-				"title_link": ruleLink,
+				"title_link": ruleUrl,
 				// "text":       "Optional text that appears within the attachment",
 				// "text":       "Optional text that appears within the attachment",
-				"fields": fields,
-				// "image_url":   "http://my-website.com/path/to/image.jpg",
+				"fields":    fields,
+				"image_url": context.ImagePublicUrl,
 				// "thumb_url":   "http://example.com/path/to/thumb.png",
 				// "thumb_url":   "http://example.com/path/to/thumb.png",
 				"footer":      "Grafana v4.0.0",
 				"footer":      "Grafana v4.0.0",
 				"footer_icon": "http://grafana.org/assets/img/fav32.png",
 				"footer_icon": "http://grafana.org/assets/img/fav32.png",

+ 9 - 0
pkg/setting/setting.go

@@ -148,6 +148,11 @@ var (
 
 
 	// Grafana.NET URL
 	// Grafana.NET URL
 	GrafanaNetUrl string
 	GrafanaNetUrl string
+
+	// S3 temp image store
+	S3TempImageStoreBucketUrl string
+	S3TempImageStoreAccessKey string
+	S3TempImageStoreSecretKey string
 )
 )
 
 
 type CommandLineArgs struct {
 type CommandLineArgs struct {
@@ -534,6 +539,10 @@ func NewConfigContext(args *CommandLineArgs) error {
 
 
 	GrafanaNetUrl = Cfg.Section("grafana.net").Key("url").MustString("https://grafana.net")
 	GrafanaNetUrl = Cfg.Section("grafana.net").Key("url").MustString("https://grafana.net")
 
 
+	s3temp := Cfg.Section("s3-temp-image-store")
+	S3TempImageStoreBucketUrl = s3temp.Key("bucket_url").String()
+	S3TempImageStoreAccessKey = s3temp.Key("access_key").String()
+	S3TempImageStoreSecretKey = s3temp.Key("secret_key").String()
 	return nil
 	return nil
 }
 }
 
 

+ 11 - 0
vendor/phantomjs/render.js

@@ -35,6 +35,17 @@
   page.open(params.url, function (status) {
   page.open(params.url, function (status) {
     // console.log('Loading a web page: ' + params.url + ' status: ' + status);
     // console.log('Loading a web page: ' + params.url + ' status: ' + status);
 
 
+    page.onError = function(msg, trace) {
+      var msgStack = ['ERROR: ' + msg];
+      if (trace && trace.length) {
+        msgStack.push('TRACE:');
+        trace.forEach(function(t) {
+          msgStack.push(' -> ' + t.file + ': ' + t.line + (t.function ? ' (in function "' + t.function +'")' : ''));
+        });
+      }
+      console.error(msgStack.join('\n'));
+    };
+
     function checkIsReady() {
     function checkIsReady() {
       var panelsRendered = page.evaluate(function() {
       var panelsRendered = page.evaluate(function() {
         if (!window.angular) { return false; }
         if (!window.angular) { return false; }