adding support for token-based slack file.upload API call for posting images to slack

This commit is contained in:
Matthew McGinn
2017-09-30 21:20:20 -04:00
parent f6cc741a6d
commit 00726e93eb
4 changed files with 89 additions and 5 deletions
+82 -4
View File
@@ -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: `
<h3 class="page-heading">Slack settings</h3>
@@ -45,6 +49,17 @@ func init() {
Mention a user or a group using @ when notifying in a channel
</info-popover>
</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,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
}
@@ -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")
})
})