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