Commit ba074062 by Carl Bergquist Committed by GitHub

Merge pull request #9392 from xginn8/slack_image_upload

adding support for token-based slack file.upload API call for posting images to slack
parents 3c1beb1b be0d4714
...@@ -48,12 +48,15 @@ external image destination if available or fallback to attaching the image in th ...@@ -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 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 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 Setting | Description
---------- | ----------- ---------- | -----------
Recipient | allows you to override the slack recipient. 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 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 ### PagerDuty
......
package notifiers package notifiers
import ( import (
"bytes"
"encoding/json" "encoding/json"
"io"
"mime/multipart"
"os"
"time" "time"
"github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/bus"
...@@ -15,7 +19,7 @@ func init() { ...@@ -15,7 +19,7 @@ func init() {
alerting.RegisterNotifier(&alerting.NotifierPlugin{ alerting.RegisterNotifier(&alerting.NotifierPlugin{
Type: "slack", Type: "slack",
Name: "Slack", Name: "Slack",
Description: "Sends notifications using Grafana server configured STMP settings", Description: "Sends notifications to Slack via Slack Webhooks",
Factory: NewSlackNotifier, Factory: NewSlackNotifier,
OptionsTemplate: ` OptionsTemplate: `
<h3 class="page-heading">Slack settings</h3> <h3 class="page-heading">Slack settings</h3>
...@@ -45,6 +49,17 @@ func init() { ...@@ -45,6 +49,17 @@ func init() {
Mention a user or a group using @ when notifying in a channel Mention a user or a group using @ when notifying in a channel
</info-popover> </info-popover>
</div> </div>
<div class="gf-form max-width-30">
<span class="gf-form-label width-6">Token</span>
<input type="text"
class="gf-form-input max-width-30"
ng-model="ctrl.model.settings.token"
data-placement="right">
</input>
<info-popover mode="right-absolute">
Provide a bot token to use the Slack file.upload API (starts with "xoxb")
</info-popover>
</div>
`, `,
}) })
...@@ -58,12 +73,16 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) { ...@@ -58,12 +73,16 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
recipient := model.Settings.Get("recipient").MustString() recipient := model.Settings.Get("recipient").MustString()
mention := model.Settings.Get("mention").MustString() mention := model.Settings.Get("mention").MustString()
token := model.Settings.Get("token").MustString()
uploadImage := model.Settings.Get("uploadImage").MustBool(true)
return &SlackNotifier{ return &SlackNotifier{
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
Url: url, Url: url,
Recipient: recipient, Recipient: recipient,
Mention: mention, Mention: mention,
Token: token,
Upload: uploadImage,
log: log.New("alerting.notifier.slack"), log: log.New("alerting.notifier.slack"),
}, nil }, nil
} }
...@@ -73,6 +92,8 @@ type SlackNotifier struct { ...@@ -73,6 +92,8 @@ type SlackNotifier struct {
Url string Url string
Recipient string Recipient string
Mention string Mention string
Token string
Upload bool
log log.Logger log log.Logger
} }
...@@ -110,6 +131,11 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { ...@@ -110,6 +131,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. if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
message += " " + evalContext.Rule.Message 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{}{ body := map[string]interface{}{
"attachments": []map[string]interface{}{ "attachments": []map[string]interface{}{
...@@ -120,7 +146,7 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { ...@@ -120,7 +146,7 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
"title_link": ruleUrl, "title_link": ruleUrl,
"text": message, "text": message,
"fields": fields, "fields": fields,
"image_url": evalContext.ImagePublicUrl, "image_url": image_url,
"footer": "Grafana v" + setting.BuildVersion, "footer": "Grafana v" + setting.BuildVersion,
"footer_icon": "https://grafana.com/assets/img/fav32.png", "footer_icon": "https://grafana.com/assets/img/fav32.png",
"ts": time.Now().Unix(), "ts": time.Now().Unix(),
...@@ -133,14 +159,75 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { ...@@ -133,14 +159,75 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
if this.Recipient != "" { if this.Recipient != "" {
body["channel"] = this.Recipient body["channel"] = this.Recipient
} }
data, _ := json.Marshal(&body) data, _ := json.Marshal(&body)
cmd := &m.SendWebhookSync{Url: this.Url, Body: string(data)} cmd := &m.SendWebhookSync{Url: this.Url, Body: string(data)}
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
this.log.Error("Failed to send slack notification", "error", err, "webhook", this.Name) this.log.Error("Failed to send slack notification", "error", err, "webhook", this.Name)
return err return err
} }
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
}
}
return nil
}
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 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
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
}
...@@ -48,14 +48,16 @@ func TestSlackNotifier(t *testing.T) { ...@@ -48,14 +48,16 @@ func TestSlackNotifier(t *testing.T) {
So(slackNotifier.Url, ShouldEqual, "http://google.com") So(slackNotifier.Url, ShouldEqual, "http://google.com")
So(slackNotifier.Recipient, ShouldEqual, "") So(slackNotifier.Recipient, ShouldEqual, "")
So(slackNotifier.Mention, ShouldEqual, "") So(slackNotifier.Mention, ShouldEqual, "")
So(slackNotifier.Token, ShouldEqual, "")
}) })
Convey("from settings with Recipient and Mention", func() { Convey("from settings with Recipient, Mention, and Token", func() {
json := ` json := `
{ {
"url": "http://google.com", "url": "http://google.com",
"recipient": "#ds-opentsdb", "recipient": "#ds-opentsdb",
"mention": "@carl" "mention": "@carl",
"token": "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX"
}` }`
settingsJSON, _ := simplejson.NewJson([]byte(json)) settingsJSON, _ := simplejson.NewJson([]byte(json))
...@@ -74,6 +76,7 @@ func TestSlackNotifier(t *testing.T) { ...@@ -74,6 +76,7 @@ func TestSlackNotifier(t *testing.T) {
So(slackNotifier.Url, ShouldEqual, "http://google.com") So(slackNotifier.Url, ShouldEqual, "http://google.com")
So(slackNotifier.Recipient, ShouldEqual, "#ds-opentsdb") So(slackNotifier.Recipient, ShouldEqual, "#ds-opentsdb")
So(slackNotifier.Mention, ShouldEqual, "@carl") So(slackNotifier.Mention, ShouldEqual, "@carl")
So(slackNotifier.Token, ShouldEqual, "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX")
}) })
}) })
......
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