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 00000000000..042d95c09b5 Binary files /dev/null and b/public/img/mixed_styles.png differ