diff --git a/pkg/components/null/float.go b/pkg/components/null/float.go index caf4d8b677c..4f783f2c584 100644 --- a/pkg/components/null/float.go +++ b/pkg/components/null/float.go @@ -106,6 +106,15 @@ func (f Float) String() string { return fmt.Sprintf("%1.3f", f.Float64) } +// FullString returns float as string in full precision +func (f Float) FullString() string { + if !f.Valid { + return "null" + } + + return fmt.Sprintf("%f", f.Float64) +} + // SetValid changes this Float's value and also sets it to be non-null. func (f *Float) SetValid(n float64) { f.Float64 = n diff --git a/pkg/models/notifications.go b/pkg/models/notifications.go index 089d7c4360d..4b25ecb4dc7 100644 --- a/pkg/models/notifications.go +++ b/pkg/models/notifications.go @@ -19,12 +19,13 @@ type SendEmailCommandSync struct { } type SendWebhookSync struct { - Url string - User string - Password string - Body string - HttpMethod string - HttpHeader map[string]string + Url string + User string + Password string + Body string + HttpMethod string + HttpHeader map[string]string + ContentType string } type SendResetPasswordEmailCommand struct { diff --git a/pkg/services/alerting/notifiers/discord.go b/pkg/services/alerting/notifiers/discord.go new file mode 100644 index 00000000000..b6831c90cfe --- /dev/null +++ b/pkg/services/alerting/notifiers/discord.go @@ -0,0 +1,171 @@ +package notifiers + +import ( + "bytes" + "io" + "mime/multipart" + "os" + "strconv" + "strings" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/setting" +) + +func init() { + alerting.RegisterNotifier(&alerting.NotifierPlugin{ + Type: "discord", + Name: "Discord", + Description: "Sends notifications to Discord", + Factory: NewDiscordNotifier, + OptionsTemplate: ` +

Discord settings

+
+ Webhook URL + +
+ `, + }) +} + +func NewDiscordNotifier(model *m.AlertNotification) (alerting.Notifier, error) { + url := model.Settings.Get("url").MustString() + if url == "" { + return nil, alerting.ValidationError{Reason: "Could not find webhook url property in settings"} + } + + return &DiscordNotifier{ + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + WebhookURL: url, + log: log.New("alerting.notifier.discord"), + }, nil +} + +type DiscordNotifier struct { + NotifierBase + WebhookURL string + log log.Logger +} + +func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error { + this.log.Info("Sending alert notification to", "webhook_url", this.WebhookURL) + + ruleUrl, err := evalContext.GetRuleUrl() + if err != nil { + this.log.Error("Failed get rule link", "error", err) + return err + } + + bodyJSON := simplejson.New() + bodyJSON.Set("username", "Grafana") + + fields := make([]map[string]interface{}, 0) + + for _, evt := range evalContext.EvalMatches { + + fields = append(fields, map[string]interface{}{ + "name": evt.Metric, + "value": evt.Value.FullString(), + "inline": true, + }) + } + + footer := map[string]interface{}{ + "text": "Grafana v" + setting.BuildVersion, + "icon_url": "https://grafana.com/assets/img/fav32.png", + } + + color, _ := strconv.ParseInt(strings.TrimLeft(evalContext.GetStateModel().Color, "#"), 16, 0) + + embed := simplejson.New() + embed.Set("title", evalContext.GetNotificationTitle()) + //Discord takes integer for color + embed.Set("color", color) + embed.Set("url", ruleUrl) + embed.Set("description", evalContext.Rule.Message) + embed.Set("type", "rich") + embed.Set("fields", fields) + embed.Set("footer", footer) + + var image = make(map[string]interface{}) + + var embeddedImage = false + + if evalContext.ImagePublicUrl != "" { + image = map[string]interface{}{ + "url": evalContext.ImagePublicUrl, + } + embed.Set("image", image) + } else { + image = map[string]interface{}{ + "url": "attachment://graph.png", + } + embed.Set("image", image) + embeddedImage = true + } + + bodyJSON.Set("embeds", []interface{}{embed}) + + json, _ := bodyJSON.MarshalJSON() + + content_type := "application/json" + + var body []byte + + if embeddedImage { + + var b bytes.Buffer + + w := multipart.NewWriter(&b) + + f, err := os.Open(evalContext.ImageOnDiskPath) + + if err != nil { + this.log.Error("Can't open graph file", err) + return err + } + + defer f.Close() + + fw, err := w.CreateFormField("payload_json") + if err != nil { + return err + } + + if _, err = fw.Write([]byte(string(json))); err != nil { + return err + } + + fw, err = w.CreateFormFile("file", "graph.png") + + if _, err = io.Copy(fw, f); err != nil { + return err + } + + w.Close() + + body = b.Bytes() + content_type = w.FormDataContentType() + + } else { + body = json + } + + cmd := &m.SendWebhookSync{ + Url: this.WebhookURL, + Body: string(body), + HttpMethod: "POST", + ContentType: content_type, + } + + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + this.log.Error("Failed to send notification to Discord", "error", err) + return err + } + + return nil +} diff --git a/pkg/services/alerting/notifiers/discord_test.go b/pkg/services/alerting/notifiers/discord_test.go new file mode 100644 index 00000000000..fe925aab362 --- /dev/null +++ b/pkg/services/alerting/notifiers/discord_test.go @@ -0,0 +1,52 @@ +package notifiers + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" + . "github.com/smartystreets/goconvey/convey" +) + +func TestDiscordNotifier(t *testing.T) { + Convey("Telegram notifier tests", t, func() { + + Convey("Parsing alert notification from settings", func() { + Convey("empty settings should return error", func() { + json := `{ }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "discord_testing", + Type: "discord", + Settings: settingsJSON, + } + + _, err := NewDiscordNotifier(model) + So(err, ShouldNotBeNil) + }) + + Convey("settings should trigger incident", func() { + json := ` + { + "url": "https://web.hook/" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "discord_testing", + Type: "discord", + Settings: settingsJSON, + } + + not, err := NewDiscordNotifier(model) + discordNotifier := not.(*DiscordNotifier) + + So(err, ShouldBeNil) + So(discordNotifier.Name, ShouldEqual, "discord_testing") + So(discordNotifier.Type, ShouldEqual, "discord") + So(discordNotifier.WebhookURL, ShouldEqual, "https://web.hook/") + }) + }) + }) +} diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go index ee54e7269f7..14d362c5e1e 100644 --- a/pkg/services/notifications/notifications.go +++ b/pkg/services/notifications/notifications.go @@ -104,12 +104,13 @@ func (ns *NotificationService) Run(ctx context.Context) error { func (ns *NotificationService) SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error { return ns.sendWebRequestSync(ctx, &Webhook{ - Url: cmd.Url, - User: cmd.User, - Password: cmd.Password, - Body: cmd.Body, - HttpMethod: cmd.HttpMethod, - HttpHeader: cmd.HttpHeader, + Url: cmd.Url, + User: cmd.User, + Password: cmd.Password, + Body: cmd.Body, + HttpMethod: cmd.HttpMethod, + HttpHeader: cmd.HttpHeader, + ContentType: cmd.ContentType, }) } diff --git a/pkg/services/notifications/webhook.go b/pkg/services/notifications/webhook.go index 01db2d56471..a236a1d1c4e 100644 --- a/pkg/services/notifications/webhook.go +++ b/pkg/services/notifications/webhook.go @@ -15,12 +15,13 @@ import ( ) type Webhook struct { - Url string - User string - Password string - Body string - HttpMethod string - HttpHeader map[string]string + Url string + User string + Password string + Body string + HttpMethod string + HttpHeader map[string]string + ContentType string } var netTransport = &http.Transport{ @@ -48,8 +49,13 @@ func (ns *NotificationService) sendWebRequestSync(ctx context.Context, webhook * return err } - request.Header.Add("Content-Type", "application/json") + if webhook.ContentType == "" { + webhook.ContentType = "application/json" + } + + request.Header.Add("Content-Type", webhook.ContentType) request.Header.Add("User-Agent", "Grafana") + if webhook.User != "" && webhook.Password != "" { request.Header.Add("Authorization", util.GetBasicAuthHeader(webhook.User, webhook.Password)) }