diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go
index 0ca1ad3dfe0..88100afe7a1 100644
--- a/pkg/services/alerting/notifiers/telegram.go
+++ b/pkg/services/alerting/notifiers/telegram.go
@@ -12,6 +12,10 @@ import (
"os"
)
+const (
+ captionLengthLimit = 200
+)
+
var (
telegramApiUrl string = "https://api.telegram.org/bot%s/%s"
)
@@ -82,88 +86,81 @@ func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error)
}
func (this *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, sendImageInline bool) *m.SendWebhookSync {
- var imageFile *os.File
- var err error
-
if sendImageInline {
- imageFile, err = os.Open(evalContext.ImageOnDiskPath)
- defer imageFile.Close()
- if err != nil {
- sendImageInline = false // fall back to text message
+ cmd, err := this.buildMessageInlineImage(evalContext)
+ if err == nil {
+ return cmd
+ } else {
+ this.log.Error("Could not generate Telegram message with inline image.", "err", err)
}
}
- message := ""
+ return this.buildMessageLinkedImage(evalContext)
+}
- if sendImageInline {
- // Telegram's API does not allow HTML formatting for image captions.
- message = fmt.Sprintf("%s\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message)
- } else {
- message = fmt.Sprintf("%s\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message)
- }
+func (this *TelegramNotifier) buildMessageLinkedImage(evalContext *alerting.EvalContext) *m.SendWebhookSync {
+ message := fmt.Sprintf("%s\nState: %s\nMessage: %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message)
ruleUrl, err := evalContext.GetRuleUrl()
if err == nil {
message = message + fmt.Sprintf("URL: %s\n", ruleUrl)
}
- if !sendImageInline {
- // only attach this if we are not sending it inline.
- if evalContext.ImagePublicUrl != "" {
- message = message + fmt.Sprintf("Image: %s\n", evalContext.ImagePublicUrl)
- }
- }
-
- metrics := ""
- fieldLimitCount := 4
- for index, evt := range evalContext.EvalMatches {
- metrics += fmt.Sprintf("\n%s: %s", evt.Metric, evt.Value)
- if index > fieldLimitCount {
- break
- }
+ if evalContext.ImagePublicUrl != "" {
+ message = message + fmt.Sprintf("Image: %s\n", evalContext.ImagePublicUrl)
}
+ metrics := generateMetricsMessage(evalContext)
if metrics != "" {
- if sendImageInline {
- // Telegram's API does not allow HTML formatting for image captions.
- message = message + fmt.Sprintf("\nMetrics:%s", metrics)
- } else {
- message = message + fmt.Sprintf("\nMetrics:%s", metrics)
- }
+ message = message + fmt.Sprintf("\nMetrics:%s", metrics)
}
- var body bytes.Buffer
+ cmd := this.generateTelegramCmd(message, "text", "sendMessage", func(w *multipart.Writer) {
+ fw, _ := w.CreateFormField("parse_mode")
+ fw.Write([]byte("html"))
+ })
+ return cmd
+}
+func (this *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.EvalContext) (*m.SendWebhookSync, error) {
+ var imageFile *os.File
+ var err error
+
+ imageFile, err = os.Open(evalContext.ImageOnDiskPath)
+ defer imageFile.Close()
+ if err != nil {
+ return nil, err
+ }
+
+ ruleUrl, err := evalContext.GetRuleUrl()
+
+ metrics := generateMetricsMessage(evalContext)
+ message := generateImageCaption(evalContext, ruleUrl, metrics)
+
+ cmd := this.generateTelegramCmd(message, "caption", "sendPhoto", func(w *multipart.Writer) {
+ fw, _ := w.CreateFormFile("photo", evalContext.ImageOnDiskPath)
+ io.Copy(fw, imageFile)
+ })
+ return cmd, nil
+}
+
+func (this *TelegramNotifier) generateTelegramCmd(message string, messageField string, apiAction string, extraConf func(writer *multipart.Writer)) *m.SendWebhookSync {
+ var body bytes.Buffer
w := multipart.NewWriter(&body)
+
fw, _ := w.CreateFormField("chat_id")
fw.Write([]byte(this.ChatID))
- if sendImageInline {
- fw, _ = w.CreateFormField("caption")
- fw.Write([]byte(message))
+ fw, _ = w.CreateFormField(messageField)
+ fw.Write([]byte(message))
- fw, _ = w.CreateFormFile("photo", evalContext.ImageOnDiskPath)
- io.Copy(fw, imageFile)
- } else {
- fw, _ = w.CreateFormField("text")
- fw.Write([]byte(message))
-
- fw, _ = w.CreateFormField("parse_mode")
- fw.Write([]byte("html"))
- }
+ extraConf(w)
w.Close()
- apiMethod := ""
- if sendImageInline {
- this.log.Info("Sending telegram image notification", "photo", evalContext.ImageOnDiskPath, "chat_id", this.ChatID, "bot_token", this.BotToken)
- apiMethod = "sendPhoto"
- } else {
- this.log.Info("Sending telegram text notification", "chat_id", this.ChatID, "bot_token", this.BotToken)
- apiMethod = "sendMessage"
- }
+ this.log.Info("Sending telegram notification", "chat_id", this.ChatID, "bot_token", this.BotToken, "apiAction", apiAction)
+ url := fmt.Sprintf(telegramApiUrl, this.BotToken, apiAction)
- url := fmt.Sprintf(telegramApiUrl, this.BotToken, apiMethod)
cmd := &m.SendWebhookSync{
Url: url,
Body: body.String(),
@@ -175,6 +172,50 @@ func (this *TelegramNotifier) buildMessage(evalContext *alerting.EvalContext, se
return cmd
}
+func generateMetricsMessage(evalContext *alerting.EvalContext) string {
+ metrics := ""
+ fieldLimitCount := 4
+ for index, evt := range evalContext.EvalMatches {
+ metrics += fmt.Sprintf("\n%s: %s", evt.Metric, evt.Value)
+ if index > fieldLimitCount {
+ break
+ }
+ }
+ return metrics
+}
+
+func generateImageCaption(evalContext *alerting.EvalContext, ruleUrl string, metrics string) string {
+ message := evalContext.GetNotificationTitle()
+
+ if len(evalContext.Rule.Message) > 0 {
+ message = fmt.Sprintf("%s\nMessage: %s", message, evalContext.Rule.Message)
+ }
+
+ if len(message) > captionLengthLimit {
+ message = message[0:captionLengthLimit]
+
+ }
+
+ if len(ruleUrl) > 0 {
+ urlLine := fmt.Sprintf("\nURL: %s", ruleUrl)
+ message = appendIfPossible(message, urlLine, captionLengthLimit)
+ }
+
+ if metrics != "" {
+ metricsLines := fmt.Sprintf("\n\nMetrics:%s", metrics)
+ message = appendIfPossible(message, metricsLines, captionLengthLimit)
+ }
+
+ return message
+}
+func appendIfPossible(message string, extra string, sizeLimit int) string {
+ if len(extra)+len(message) <= sizeLimit {
+ return message + extra
+ }
+ log.Debug("Line too long for image caption.", "value", extra)
+ return message
+}
+
func (this *TelegramNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
diff --git a/pkg/services/alerting/notifiers/telegram_test.go b/pkg/services/alerting/notifiers/telegram_test.go
index 3e8066e273b..05be787dced 100644
--- a/pkg/services/alerting/notifiers/telegram_test.go
+++ b/pkg/services/alerting/notifiers/telegram_test.go
@@ -5,6 +5,7 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/services/alerting"
. "github.com/smartystreets/goconvey/convey"
)
@@ -50,6 +51,71 @@ func TestTelegramNotifier(t *testing.T) {
So(telegramNotifier.ChatID, ShouldEqual, "-1234567890")
})
+ Convey("generateCaption should generate a message with all pertinent details", func() {
+ evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
+ Name: "This is an alarm",
+ Message: "Some kind of message.",
+ State: m.AlertStateOK,
+ })
+
+ caption := generateImageCaption(evalContext, "http://grafa.url/abcdef", "")
+ So(len(caption), ShouldBeLessThanOrEqualTo, 200)
+ So(caption, ShouldContainSubstring, "Some kind of message.")
+ So(caption, ShouldContainSubstring, "[OK] This is an alarm")
+ So(caption, ShouldContainSubstring, "http://grafa.url/abcdef")
+ })
+
+ Convey("When generating a message", func() {
+
+ Convey("URL should be skipped if it's too long", func() {
+ evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
+ Name: "This is an alarm",
+ Message: "Some kind of message.",
+ State: m.AlertStateOK,
+ })
+
+ caption := generateImageCaption(evalContext,
+ "http://grafa.url/abcdefaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "foo bar")
+ So(len(caption), ShouldBeLessThanOrEqualTo, 200)
+ So(caption, ShouldContainSubstring, "Some kind of message.")
+ So(caption, ShouldContainSubstring, "[OK] This is an alarm")
+ So(caption, ShouldContainSubstring, "foo bar")
+ So(caption, ShouldNotContainSubstring, "http")
+ })
+
+ Convey("Message should be trimmed if it's too long", func() {
+ evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
+ Name: "This is an alarm",
+ Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise I will. Yes siree that's it.",
+ State: m.AlertStateOK,
+ })
+
+ caption := generateImageCaption(evalContext,
+ "http://grafa.url/foo",
+ "")
+ So(len(caption), ShouldBeLessThanOrEqualTo, 200)
+ So(caption, ShouldContainSubstring, "[OK] This is an alarm")
+ So(caption, ShouldNotContainSubstring, "http")
+ So(caption, ShouldContainSubstring, "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I promise ")
+ })
+
+ Convey("Metrics should be skipped if they dont fit", func() {
+ evalContext := alerting.NewEvalContext(nil, &alerting.Rule{
+ Name: "This is an alarm",
+ Message: "Some kind of message that is too long for appending to our pretty little message, this line is actually exactly 197 chars long and I will get there in the end I ",
+ State: m.AlertStateOK,
+ })
+
+ caption := generateImageCaption(evalContext,
+ "http://grafa.url/foo",
+ "foo bar long song")
+ So(len(caption), ShouldBeLessThanOrEqualTo, 200)
+ So(caption, ShouldContainSubstring, "[OK] This is an alarm")
+ So(caption, ShouldNotContainSubstring, "http")
+ So(caption, ShouldNotContainSubstring, "foo bar")
+ })
+ })
})
})
}