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))
}