From 00726e93eb5021361be3dea5b593e53cfa4b062c Mon Sep 17 00:00:00 2001 From: Matthew McGinn Date: Sat, 30 Sep 2017 19:26:19 -0400 Subject: [PATCH 1/4] adding support for token-based slack file.upload API call for posting images to slack --- docs/sources/alerting/notifications.md | 5 +- pkg/services/alerting/notifiers/slack.go | 86 +++++++++++++++++- pkg/services/alerting/notifiers/slack_test.go | 3 + public/img/mixed_styles.png | Bin 0 -> 29916 bytes 4 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 public/img/mixed_styles.png diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index 359315650d9..de9e5abd472 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -48,12 +48,15 @@ external image destination if available or fallback to attaching the image in th To set up slack you need to configure an incoming webhook url at slack. You can follow their guide for how to do that https://api.slack.com/incoming-webhooks If you want to include screenshots of the firing alerts -in the slack messages you have to configure the [external image destination](#external-image-store) in Grafana. +in the slack messages you have to configure either the [external image destination](#external-image-store) in Grafana, +or a bot integration via Slack Apps. Follow Slack's guide to set up a bot integration and use the token provided +https://api.slack.com/bot-users, which starts with "xoxb". Setting | Description ---------- | ----------- Recipient | allows you to override the slack recipient. Mention | make it possible to include a mention in the slack notification sent by Grafana. Ex @here or @channel +Token | If provided, Grafana will upload the generated image via Slack's file.upload API method, not the external image destination. ### PagerDuty diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index d917daa3620..1ee16453a5f 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -1,7 +1,11 @@ package notifiers import ( + "bytes" "encoding/json" + "io" + "mime/multipart" + "os" "time" "github.com/grafana/grafana/pkg/bus" @@ -15,7 +19,7 @@ func init() { alerting.RegisterNotifier(&alerting.NotifierPlugin{ Type: "slack", Name: "Slack", - Description: "Sends notifications using Grafana server configured STMP settings", + Description: "Sends notifications to Slack via Slack Webhooks", Factory: NewSlackNotifier, OptionsTemplate: `

Slack settings

@@ -45,6 +49,17 @@ func init() { Mention a user or a group using @ when notifying in a channel +
+ Token + + + + Provide a bot token to use the Slack file.upload API (starts with "xoxb") + +
`, }) @@ -58,12 +73,14 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) { recipient := model.Settings.Get("recipient").MustString() mention := model.Settings.Get("mention").MustString() + token := model.Settings.Get("token").MustString() return &SlackNotifier{ NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), Url: url, Recipient: recipient, Mention: mention, + Token: token, log: log.New("alerting.notifier.slack"), }, nil } @@ -73,6 +90,7 @@ type SlackNotifier struct { Url string Recipient string Mention string + Token string log log.Logger } @@ -110,6 +128,11 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok. message += " " + evalContext.Rule.Message } + image_url := "" + // default to file.upload API method if a token is provided + if this.Token == "" { + image_url = evalContext.ImagePublicUrl + } body := map[string]interface{}{ "attachments": []map[string]interface{}{ @@ -120,7 +143,7 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { "title_link": ruleUrl, "text": message, "fields": fields, - "image_url": evalContext.ImagePublicUrl, + "image_url": image_url, "footer": "Grafana v" + setting.BuildVersion, "footer_icon": "https://grafana.com/assets/img/fav32.png", "ts": time.Now().Unix(), @@ -133,14 +156,69 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { if this.Recipient != "" { body["channel"] = this.Recipient } - data, _ := json.Marshal(&body) cmd := &m.SendWebhookSync{Url: this.Url, Body: string(data)} - if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { this.log.Error("Failed to send slack notification", "error", err, "webhook", this.Name) return err } + if this.Token != "" { + slackUploadUrl := "https://slack.com/api/files.upload" + if evalContext.ImageOnDiskPath == "" { + evalContext.ImageOnDiskPath = "public/img/mixed_styles.png" + } + this.log.Info("Uploading to slack via file.upload API") + headers, uploadBody, err := GenerateSlackUpload(evalContext.ImageOnDiskPath, this.Token, this.Recipient) + if err != nil { + return err + } + cmd := &m.SendWebhookSync{Url: slackUploadUrl, Body: uploadBody.String(), HttpHeader: headers, HttpMethod: "POST"} + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + this.log.Error("Failed to upload slack image", "error", err, "webhook", "file.upload") + return err + } + if err != nil { + return err + } + } return nil } + +func GenerateSlackUpload(file string, token string, recipient string) (map[string]string, bytes.Buffer, error) { + // Slack requires all POSTs to files.upload to present + // an "application/x-www-form-urlencoded" encoded querystring + // See https://api.slack.com/methods/files.upload + var b bytes.Buffer + w := multipart.NewWriter(&b) + // Add the generated image file + f, err := os.Open(file) + if err != nil { + return nil, b, err + } + defer f.Close() + fw, err := w.CreateFormFile("file", file) + if err != nil { + return nil, b, err + } + _, err = io.Copy(fw, f) + if err != nil { + return nil, b, err + } + // Add the authorization token + err = w.WriteField("token", token) + if err != nil { + return nil, b, err + } + // Add the channel(s) to POST to + err = w.WriteField("channels", recipient) + if err != nil { + return nil, b, err + } + w.Close() + headers := map[string]string{ + "Content-Type": w.FormDataContentType(), + "Authorization": "auth_token=\"" + token + "\"", + } + return headers, b, nil +} diff --git a/pkg/services/alerting/notifiers/slack_test.go b/pkg/services/alerting/notifiers/slack_test.go index 5b1763064aa..6f5b69fcb0a 100644 --- a/pkg/services/alerting/notifiers/slack_test.go +++ b/pkg/services/alerting/notifiers/slack_test.go @@ -48,6 +48,7 @@ func TestSlackNotifier(t *testing.T) { So(slackNotifier.Url, ShouldEqual, "http://google.com") So(slackNotifier.Recipient, ShouldEqual, "") So(slackNotifier.Mention, ShouldEqual, "") + So(slackNotifier.Token, ShouldEqual, "") }) Convey("from settings with Recipient and Mention", func() { @@ -56,6 +57,7 @@ func TestSlackNotifier(t *testing.T) { "url": "http://google.com", "recipient": "#ds-opentsdb", "mention": "@carl" + "token": "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX", }` settingsJSON, _ := simplejson.NewJson([]byte(json)) @@ -74,6 +76,7 @@ func TestSlackNotifier(t *testing.T) { So(slackNotifier.Url, ShouldEqual, "http://google.com") So(slackNotifier.Recipient, ShouldEqual, "#ds-opentsdb") So(slackNotifier.Mention, ShouldEqual, "@carl") + So(slackNotifier.Token, ShouldEqual, "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX") }) }) diff --git a/public/img/mixed_styles.png b/public/img/mixed_styles.png new file mode 100644 index 0000000000000000000000000000000000000000..042d95c09b50fdb5c10a393fa68a7dcfb69f9254 GIT binary patch literal 29916 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 LnmkYQBof0EczP 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 Date: Sun, 1 Oct 2017 10:37:59 -0400 Subject: [PATCH 2/4] fixing json for CI --- pkg/services/alerting/notifiers/slack_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/services/alerting/notifiers/slack_test.go b/pkg/services/alerting/notifiers/slack_test.go index 6f5b69fcb0a..13f8c7b48b7 100644 --- a/pkg/services/alerting/notifiers/slack_test.go +++ b/pkg/services/alerting/notifiers/slack_test.go @@ -51,13 +51,13 @@ func TestSlackNotifier(t *testing.T) { So(slackNotifier.Token, ShouldEqual, "") }) - Convey("from settings with Recipient and Mention", func() { + Convey("from settings with Recipient, Mention, and Token", func() { json := ` { "url": "http://google.com", "recipient": "#ds-opentsdb", - "mention": "@carl" - "token": "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX", + "mention": "@carl", + "token": "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX" }` settingsJSON, _ := simplejson.NewJson([]byte(json)) From 122e2b5c426d39b508f5a04fc05223e680cdb531 Mon Sep 17 00:00:00 2001 From: Matthew McGinn Date: Mon, 2 Oct 2017 20:45:22 -0400 Subject: [PATCH 3/4] break out slack upload into separate function --- pkg/services/alerting/notifiers/slack.go | 38 ++++++++++++++---------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index 1ee16453a5f..908aab612ec 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -163,29 +163,35 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { return err } if this.Token != "" { - slackUploadUrl := "https://slack.com/api/files.upload" - if evalContext.ImageOnDiskPath == "" { - evalContext.ImageOnDiskPath = "public/img/mixed_styles.png" - } - this.log.Info("Uploading to slack via file.upload API") - headers, uploadBody, err := GenerateSlackUpload(evalContext.ImageOnDiskPath, this.Token, this.Recipient) - if err != nil { - return err - } - cmd := &m.SendWebhookSync{Url: slackUploadUrl, Body: uploadBody.String(), HttpHeader: headers, HttpMethod: "POST"} - if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { - this.log.Error("Failed to upload slack image", "error", err, "webhook", "file.upload") - return err - } + err = SlackFileUpload(evalContext, this.log, "https://slack.com/api/files.upload", this.Recipient, this.Token) if err != nil { return err } } - return nil } -func GenerateSlackUpload(file string, token string, recipient string) (map[string]string, bytes.Buffer, error) { +func SlackFileUpload(evalContext *alerting.EvalContext, log log.Logger, url string, recipient string, token string) error { + if evalContext.ImageOnDiskPath == "" { + evalContext.ImageOnDiskPath = "public/img/mixed_styles.png" + } + log.Info("Uploading to slack via file.upload API") + headers, uploadBody, err := GenerateSlackBody(evalContext.ImageOnDiskPath, token, recipient) + if err != nil { + return err + } + cmd := &m.SendWebhookSync{Url: url, Body: uploadBody.String(), HttpHeader: headers, HttpMethod: "POST"} + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + log.Error("Failed to upload slack image", "error", err, "webhook", "file.upload") + return err + } + if err != nil { + return err + } + return nil +} + +func GenerateSlackBody(file string, token string, recipient string) (map[string]string, bytes.Buffer, error) { // Slack requires all POSTs to files.upload to present // an "application/x-www-form-urlencoded" encoded querystring // See https://api.slack.com/methods/files.upload From be0d47146751733e60807240c5082ea777a74109 Mon Sep 17 00:00:00 2001 From: Matthew McGinn Date: Mon, 2 Oct 2017 23:18:48 -0400 Subject: [PATCH 4/4] properly parse & pass upload image bool from config --- pkg/services/alerting/notifiers/slack.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index 908aab612ec..ed1451da419 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -74,6 +74,7 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) { recipient := model.Settings.Get("recipient").MustString() mention := model.Settings.Get("mention").MustString() token := model.Settings.Get("token").MustString() + uploadImage := model.Settings.Get("uploadImage").MustBool(true) return &SlackNotifier{ NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), @@ -81,6 +82,7 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) { Recipient: recipient, Mention: mention, Token: token, + Upload: uploadImage, log: log.New("alerting.notifier.slack"), }, nil } @@ -91,6 +93,7 @@ type SlackNotifier struct { Recipient string Mention string Token string + Upload bool log log.Logger } @@ -162,7 +165,7 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Error("Failed to send slack notification", "error", err, "webhook", this.Name) return err } - if this.Token != "" { + if this.Token != "" && this.UploadImage { err = SlackFileUpload(evalContext, this.log, "https://slack.com/api/files.upload", this.Recipient, this.Token) if err != nil { return err