|
@@ -1,7 +1,11 @@
|
|
|
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() {
|
|
|
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() {
|
|
|
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,14 @@ 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()
|
|
|
|
|
|
|
|
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,
|
|
|
log: log.New("alerting.notifier.slack"),
|
|
log: log.New("alerting.notifier.slack"),
|
|
|
}, nil
|
|
}, nil
|
|
|
}
|
|
}
|
|
@@ -73,6 +90,7 @@ type SlackNotifier struct {
|
|
|
Url string
|
|
Url string
|
|
|
Recipient string
|
|
Recipient string
|
|
|
Mention string
|
|
Mention string
|
|
|
|
|
+ Token string
|
|
|
log log.Logger
|
|
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.
|
|
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 +143,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 +156,69 @@ 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 != "" {
|
|
|
|
|
+ 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
|
|
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
|
|
|
|
|
+}
|