Commit 2b276d5c by Torkel Ödegaard

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

parent f9ddfb43
...@@ -3,7 +3,6 @@ package imguploader ...@@ -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 ( ...@@ -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 { ...@@ -28,13 +27,7 @@ func NewS3Uploader(bucket, accessKey, secretKey string) *S3Uploader {
} }
} }
func (u *S3Uploader) Upload(imgUrl string) (string, error) { func (u *S3Uploader) Upload(path string) (string, error) {
client := http.Client{Timeout: time.Duration(60 * time.Second)}
res, err := client.Get(imgUrl)
if err != nil {
return "", err
}
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) { ...@@ -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
} }
......
...@@ -9,10 +9,11 @@ import ( ...@@ -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 { ...@@ -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) { ...@@ -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
} }
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 Firing bool
IsTestRun bool IsTestRun bool
Events []*Event Events []*Event
Logs []*ResultLogEntry Logs []*ResultLogEntry
Error error Error error
Description string Description string
StartTime time.Time StartTime time.Time
EndTime time.Time EndTime time.Time
Rule *Rule Rule *Rule
DoneChan chan bool DoneChan chan bool
CancelChan chan bool CancelChan chan bool
log log.Logger 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 { ...@@ -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(),
......
...@@ -4,8 +4,11 @@ import ( ...@@ -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) { ...@@ -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}
......
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
}
...@@ -39,7 +39,7 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) { ...@@ -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) { ...@@ -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",
......
...@@ -41,7 +41,7 @@ func (this *SlackNotifier) Notify(context *alerting.EvalContext) { ...@@ -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) { ...@@ -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, "fields": fields,
// "image_url": "http://my-website.com/path/to/image.jpg", "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",
......
...@@ -148,6 +148,11 @@ var ( ...@@ -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 { ...@@ -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
} }
......
...@@ -35,6 +35,17 @@ ...@@ -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; }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment