From c15c44d3818fa542c0b5efd0cb764504f7472235 Mon Sep 17 00:00:00 2001 From: Aleksandr Matsko <90016901+AleksandrMatsko@users.noreply.github.com> Date: Mon, 29 Jul 2024 17:59:52 +0700 Subject: [PATCH] feat: markdown description to telegram senders (#1055) --- datatypes.go | 8 +- senders/mattermost/sender.go | 174 ++++++-------------- senders/mattermost/sender_internal_test.go | 126 -------------- senders/msgformat/highlighter.go | 167 +++++++++++++++++++ senders/msgformat/highlighter_test.go | 182 +++++++++++++++++++++ senders/msgformat/msgformat.go | 23 +++ senders/slack/slack.go | 139 +++++----------- senders/slack/slack_test.go | 71 ++++---- senders/telegram/emoji_provider.go | 18 ++ senders/telegram/init.go | 91 +++++++++-- senders/telegram/init_test.go | 4 +- senders/telegram/send.go | 73 +++------ senders/telegram/send_test.go | 105 +----------- 13 files changed, 618 insertions(+), 563 deletions(-) create mode 100644 senders/msgformat/highlighter.go create mode 100644 senders/msgformat/highlighter_test.go create mode 100644 senders/msgformat/msgformat.go create mode 100644 senders/telegram/emoji_provider.go diff --git a/datatypes.go b/datatypes.go index c9a1a234f..9796479ef 100644 --- a/datatypes.go +++ b/datatypes.go @@ -28,7 +28,9 @@ const ( ) const ( - format = "15:04 02.01.2006" + // DefaultDateTimeFormat used for formatting timestamps. + DefaultDateTimeFormat = "15:04 02.01.2006" + // DefaultTimeFormat used for formatting time. DefaultTimeFormat = "15:04" remindMessage = "This metric has been in bad state for more than %v hours - please, fix." limit = 1000 @@ -108,7 +110,7 @@ func (event *NotificationEvent) CreateMessage(location *time.Location) string { } if event.MessageEventInfo.Maintenance.StartTime != nil { messageBuffer.WriteString(" at ") - messageBuffer.WriteString(time.Unix(*event.MessageEventInfo.Maintenance.StartTime, 0).In(location).Format(format)) + messageBuffer.WriteString(time.Unix(*event.MessageEventInfo.Maintenance.StartTime, 0).In(location).Format(DefaultDateTimeFormat)) } if event.MessageEventInfo.Maintenance.StopUser != nil || event.MessageEventInfo.Maintenance.StopTime != nil { messageBuffer.WriteString(" and removed") @@ -118,7 +120,7 @@ func (event *NotificationEvent) CreateMessage(location *time.Location) string { } if event.MessageEventInfo.Maintenance.StopTime != nil { messageBuffer.WriteString(" at ") - messageBuffer.WriteString(time.Unix(*event.MessageEventInfo.Maintenance.StopTime, 0).In(location).Format(format)) + messageBuffer.WriteString(time.Unix(*event.MessageEventInfo.Maintenance.StopTime, 0).In(location).Format(DefaultDateTimeFormat)) } } messageBuffer.WriteString(".") diff --git a/senders/mattermost/sender.go b/senders/mattermost/sender.go index 2a1687214..d83dbf6c7 100644 --- a/senders/mattermost/sender.go +++ b/senders/mattermost/sender.go @@ -5,11 +5,11 @@ import ( "crypto/tls" "fmt" "net/http" - "strings" "time" + "github.com/moira-alert/moira/senders/msgformat" + "github.com/moira-alert/moira" - "github.com/moira-alert/moira/senders" "github.com/moira-alert/moira/senders/emoji_provider" "github.com/mattermost/mattermost/server/public/model" @@ -31,17 +31,18 @@ type config struct { // It implements moira.Sender. // You must call Init method before SendEvents method. type Sender struct { - frontURI string - useEmoji bool - emojiProvider emoji_provider.StateEmojiGetter - logger moira.Logger - location *time.Location - client Client + logger moira.Logger + client Client + formatter msgformat.MessageFormatter } const ( messageMaxCharacters = 4_000 - quotas = "```" +) + +var ( + codeBlockStart = "```" + codeBlockEnd = "```" ) // Init configures Sender. @@ -81,15 +82,48 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca if err != nil { return fmt.Errorf("cannot initialize mattermost sender, err: %w", err) } - sender.emojiProvider = emojiProvider - sender.frontURI = cfg.FrontURI - sender.useEmoji = cfg.UseEmoji - sender.location = location sender.logger = logger + sender.formatter = msgformat.NewHighlightSyntaxFormatter( + emojiProvider, + cfg.UseEmoji, + cfg.FrontURI, + location, + uriFormatter, + descriptionFormatter, + boldFormatter, + eventStringFormatter, + codeBlockStart, + codeBlockEnd) return nil } +func uriFormatter(triggerURI, triggerName string) string { + return fmt.Sprintf("[%s](%s)", triggerName, triggerURI) +} + +func descriptionFormatter(trigger moira.TriggerData) string { + desc := trigger.Desc + if trigger.Desc != "" { + desc += "\n" + } + return desc +} + +func boldFormatter(str string) string { + return fmt.Sprintf("**%s**", str) +} + +func eventStringFormatter(event moira.NotificationEvent, loc *time.Location) string { + return fmt.Sprintf( + "%s: %s = %s (%s to %s)", + event.FormatTimestamp(loc, moira.DefaultTimeFormat), + event.Metric, + event.GetMetricsValues(moira.DefaultNotificationSettings), + event.OldState, + event.State) +} + // SendEvents implements moira.Sender interface. func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) error { message := sender.buildMessage(events, trigger, throttled) @@ -113,114 +147,12 @@ func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira. } func (sender *Sender) buildMessage(events moira.NotificationEvents, trigger moira.TriggerData, throttled bool) string { - var message strings.Builder - title := sender.buildTitle(events, trigger, throttled) - titleLen := len([]rune(title)) - - desc := sender.buildDescription(trigger) - descLen := len([]rune(desc)) - - eventsString := sender.buildEventsString(events, -1, throttled) - eventsStringLen := len([]rune(eventsString)) - - charsLeftAfterTitle := messageMaxCharacters - titleLen - - descNewLen, eventsNewLen := senders.CalculateMessagePartsLength(charsLeftAfterTitle, descLen, eventsStringLen) - if descLen != descNewLen { - desc = desc[:descNewLen] + "...\n" - } - if eventsNewLen != eventsStringLen { - eventsString = sender.buildEventsString(events, eventsNewLen, throttled) - } - - message.WriteString(title) - message.WriteString(desc) - message.WriteString(eventsString) - return message.String() -} - -func (sender *Sender) buildDescription(trigger moira.TriggerData) string { - desc := trigger.Desc - if trigger.Desc != "" { - desc += "\n" - } - return desc -} - -func (sender *Sender) buildTitle(events moira.NotificationEvents, trigger moira.TriggerData, throttled bool) string { - state := events.GetCurrentState(throttled) - title := "" - if sender.useEmoji { - title += sender.emojiProvider.GetStateEmoji(state) + " " - } - - title += fmt.Sprintf("**%s**", state) - triggerURI := trigger.GetTriggerURI(sender.frontURI) - if triggerURI != "" { - title += fmt.Sprintf(" [%s](%s)", trigger.Name, triggerURI) - } else if trigger.Name != "" { - title += " " + trigger.Name - } - - tags := trigger.GetTags() - if tags != "" { - title += " " + tags - } - - title += "\n" - return title -} - -// buildEventsString builds the string from moira events and limits it to charsForEvents. -// If n is negative buildEventsString does not limit the events string. -func (sender *Sender) buildEventsString(events moira.NotificationEvents, charsForEvents int, throttled bool) string { - charsForThrottleMsg := 0 - throttleMsg := "\nPlease, *fix your system or tune this trigger* to generate less events." - if throttled { - charsForThrottleMsg = len([]rune(throttleMsg)) - } - charsLeftForEvents := charsForEvents - charsForThrottleMsg - - eventsString := quotas - var tailString string - - eventsLenLimitReached := false - eventsPrinted := 0 - for _, event := range events { - line := fmt.Sprintf( - "\n%s: %s = %s (%s to %s)", - event.FormatTimestamp(sender.location, moira.DefaultTimeFormat), - event.Metric, - event.GetMetricsValues(moira.DefaultNotificationSettings), - event.OldState, - event.State, - ) - if msg := event.CreateMessage(sender.location); len(msg) > 0 { - line += fmt.Sprintf(". %s", msg) - } - - tailString = fmt.Sprintf("\n...and %d more events.", len(events)-eventsPrinted) - tailStringLen := len([]rune(quotas)) + len([]rune(tailString)) - if !(charsForEvents < 0) && (len([]rune(eventsString))+len([]rune(line)) > charsLeftForEvents-tailStringLen) { - eventsLenLimitReached = true - break - } - - eventsString += line - eventsPrinted++ - } - eventsString += "\n" - eventsString += quotas - - if eventsLenLimitReached { - eventsString += tailString - } - - if throttled { - eventsString += throttleMsg - } - - return eventsString + return sender.formatter.Format(msgformat.MessageFormatterParams{ + Events: events, + Trigger: trigger, + MessageMaxChars: messageMaxCharacters, + Throttled: throttled, + }) } func (sender *Sender) sendMessage(ctx context.Context, message string, contact string, triggerID string) (*model.Post, error) { diff --git a/senders/mattermost/sender_internal_test.go b/senders/mattermost/sender_internal_test.go index 538035c9f..1ae3da0a2 100644 --- a/senders/mattermost/sender_internal_test.go +++ b/senders/mattermost/sender_internal_test.go @@ -3,9 +3,7 @@ package mattermost import ( "context" "errors" - "strings" "testing" - "time" "github.com/mattermost/mattermost/server/public/model" @@ -72,127 +70,3 @@ func TestSendEvents(t *testing.T) { }) }) } - -func TestBuildMessage(t *testing.T) { - logger, _ := logging.ConfigureLog("stdout", "debug", "test", true) - sender := &Sender{} - - Convey("Given configured sender", t, func() { - senderSettings := map[string]interface{}{ - "url": "qwerty", "api_token": "qwerty", // redundant, but necessary config - "front_uri": "http://moira.url", - "insecure_tls": true, - } - location, _ := time.LoadLocation("UTC") - err := sender.Init(senderSettings, logger, location, "") - So(err, ShouldBeNil) - - event := moira.NotificationEvent{ - TriggerID: "TriggerID", - Values: map[string]float64{"t1": 123}, - Timestamp: 150000000, - Metric: "Metric", - OldState: moira.StateOK, - State: moira.StateNODATA, - } - - const shortDesc = `My description` - trigger := moira.TriggerData{ - Tags: []string{"tag1", "tag2"}, - Name: "Name", - ID: "TriggerID", - Desc: shortDesc, - } - - Convey("Message with one event", func() { - events, throttled := moira.NotificationEvents{event}, false - msg := sender.buildMessage(events, trigger, throttled) - - expected := "**NODATA** [Name](http://moira.url/trigger/TriggerID) [tag1][tag2]\n" + - shortDesc + "\n" + - "```\n" + - "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```" - So(msg, ShouldEqual, expected) - }) - - Convey("Message with one event and throttled", func() { - events, throttled := moira.NotificationEvents{event}, true - msg := sender.buildMessage(events, trigger, throttled) - - expected := "**NODATA** [Name](http://moira.url/trigger/TriggerID) [tag1][tag2]\n" + - shortDesc + "\n" + - "```\n" + - "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```" + "\n" + - "Please, *fix your system or tune this trigger* to generate less events." - So(msg, ShouldEqual, expected) - }) - - Convey("Moira message with 3 events", func() { - actual := sender.buildMessage([]moira.NotificationEvent{event, event, event}, trigger, false) - expected := "**NODATA** [Name](http://moira.url/trigger/TriggerID) [tag1][tag2]\n" + - shortDesc + "\n" + - "```\n" + - "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n" + - "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n" + - "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```" - So(actual, ShouldResemble, expected) - }) - - Convey("Long message parts", func() { - const ( - msgLimit = 4_000 - halfLimit = msgLimit / 2 - greaterThanHalf = halfLimit + 100 - lessThanHalf = halfLimit - 100 - ) - - const eventLine = "\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)" - oneEventLineLen := len([]rune(eventLine)) - - longDesc := strings.Repeat("a", greaterThanHalf) - - // Events list with chars greater than half of the message limit - var longEvents moira.NotificationEvents - for i := 0; i < greaterThanHalf/oneEventLineLen; i++ { - longEvents = append(longEvents, event) - } - - Convey("Long description. desc > msgLimit/2", func() { - var events moira.NotificationEvents - for i := 0; i < lessThanHalf/oneEventLineLen; i++ { - events = append(events, event) - } - - actual := sender.buildMessage(events, moira.TriggerData{Desc: longDesc}, false) - expected := "**NODATA**\n" + - strings.Repeat("a", 2100) + "\n" + - "```\n" + - strings.Repeat("02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n", 39) + - "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```" - So(actual, ShouldResemble, expected) - }) - - Convey("Many events. eventString > msgLimit/2", func() { - desc := strings.Repeat("a", lessThanHalf) - actual := sender.buildMessage(longEvents, moira.TriggerData{Desc: desc}, false) - expected := "**NODATA**\n" + - desc + "\n" + - "```\n" + - strings.Repeat("02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n", 43) + - "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```" - So(actual, ShouldResemble, expected) - }) - - Convey("Long description and many events. both desc and events > msgLimit/2", func() { - actual := sender.buildMessage(longEvents, moira.TriggerData{Desc: longDesc}, false) - expected := "**NODATA**\n" + - strings.Repeat("a", 1984) + "...\n" + - "```\n" + - strings.Repeat("02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n", 40) + - "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```\n" + - "...and 3 more events." - So(actual, ShouldResemble, expected) - }) - }) - }) -} diff --git a/senders/msgformat/highlighter.go b/senders/msgformat/highlighter.go new file mode 100644 index 000000000..156609957 --- /dev/null +++ b/senders/msgformat/highlighter.go @@ -0,0 +1,167 @@ +package msgformat + +import ( + "fmt" + "strings" + "time" + + "github.com/moira-alert/moira" + "github.com/moira-alert/moira/senders" + "github.com/moira-alert/moira/senders/emoji_provider" +) + +// UriFormatter is used for formatting uris, for example for Markdown use something like +// fmt.Sprintf("[%s](%s)", triggerName, triggerURI). +type UriFormatter func(triggerURI, triggerName string) string + +// DescriptionFormatter is used to format trigger description to supported description. +type DescriptionFormatter func(trigger moira.TriggerData) string + +// BoldFormatter makes str bold. For example in Markdown it should return **str**. +type BoldFormatter func(str string) string + +// EventStringFormatter formats single event string. +type EventStringFormatter func(event moira.NotificationEvent, location *time.Location) string + +// HighlightSyntaxFormatter formats message by using functions, emojis and some other highlight patterns. +type HighlightSyntaxFormatter struct { + // emojiGetter used in titles for better description. + emojiGetter emoji_provider.StateEmojiGetter + frontURI string + location *time.Location + useEmoji bool + uriFormatter UriFormatter + descriptionFormatter DescriptionFormatter + boldFormatter BoldFormatter + eventsStringFormatter EventStringFormatter + codeBlockStart string + codeBlockEnd string +} + +// NewHighlightSyntaxFormatter creates new HighlightSyntaxFormatter with given arguments. +func NewHighlightSyntaxFormatter( + emojiGetter emoji_provider.StateEmojiGetter, + useEmoji bool, + frontURI string, + location *time.Location, + uriFormatter UriFormatter, + descriptionFormatter DescriptionFormatter, + boldFormatter BoldFormatter, + eventsStringFormatter EventStringFormatter, + codeBlockStart string, + codeBlockEnd string, +) MessageFormatter { + return &HighlightSyntaxFormatter{ + emojiGetter: emojiGetter, + frontURI: frontURI, + location: location, + useEmoji: useEmoji, + uriFormatter: uriFormatter, + descriptionFormatter: descriptionFormatter, + boldFormatter: boldFormatter, + eventsStringFormatter: eventsStringFormatter, + codeBlockStart: codeBlockStart, + codeBlockEnd: codeBlockEnd, + } +} + +// Format formats message using given params and formatter functions. +func (formatter *HighlightSyntaxFormatter) Format(params MessageFormatterParams) string { + var message strings.Builder + state := params.Events.GetCurrentState(params.Throttled) + emoji := formatter.emojiGetter.GetStateEmoji(state) + + title := formatter.buildTitle(params.Events, params.Trigger, emoji, params.Throttled) + titleLen := len([]rune(title)) + + desc := formatter.descriptionFormatter(params.Trigger) + descLen := len([]rune(desc)) + + eventsString := formatter.buildEventsString(params.Events, -1, params.Throttled) + eventsStringLen := len([]rune(eventsString)) + + charsLeftAfterTitle := params.MessageMaxChars - titleLen + + descNewLen, eventsNewLen := senders.CalculateMessagePartsLength(charsLeftAfterTitle, descLen, eventsStringLen) + if descLen != descNewLen { + desc = desc[:descNewLen] + "...\n" + } + if eventsNewLen != eventsStringLen { + eventsString = formatter.buildEventsString(params.Events, eventsNewLen, params.Throttled) + } + + message.WriteString(title) + message.WriteString(desc) + message.WriteString(eventsString) + return message.String() +} + +func (formatter *HighlightSyntaxFormatter) buildTitle(events moira.NotificationEvents, trigger moira.TriggerData, emoji string, throttled bool) string { + state := events.GetCurrentState(throttled) + title := "" + if formatter.useEmoji { + title += emoji + " " + } + + title += formatter.boldFormatter(string(state)) + triggerURI := trigger.GetTriggerURI(formatter.frontURI) + if triggerURI != "" { + title += fmt.Sprintf(" %s", formatter.uriFormatter(triggerURI, trigger.Name)) + } else if trigger.Name != "" { + title += " " + trigger.Name + } + + tags := trigger.GetTags() + if tags != "" { + title += " " + tags + } + + title += "\n" + return title +} + +// buildEventsString builds the string from moira events and limits it to charsForEvents. +// if charsForEvents is negative buildEventsString does not limit the events string. +func (formatter *HighlightSyntaxFormatter) buildEventsString(events moira.NotificationEvents, charsForEvents int, throttled bool) string { + charsForThrottleMsg := 0 + throttleMsg := fmt.Sprintf("\nPlease, %s to generate less events.", formatter.boldFormatter(changeRecommendation)) + if throttled { + charsForThrottleMsg = len([]rune(throttleMsg)) + } + charsLeftForEvents := charsForEvents - charsForThrottleMsg + + var eventsString string + eventsString += formatter.codeBlockStart + var tailString string + + eventsLenLimitReached := false + eventsPrinted := 0 + for _, event := range events { + line := fmt.Sprintf("\n%s", formatter.eventsStringFormatter(event, formatter.location)) + if msg := event.CreateMessage(formatter.location); len(msg) > 0 { + line += fmt.Sprintf(". %s", msg) + } + + tailString = fmt.Sprintf("\n...and %d more events.", len(events)-eventsPrinted) + tailStringLen := len([]rune(formatter.codeBlockEnd)) + len("\n") + len([]rune(tailString)) + if !(charsForEvents < 0) && (len([]rune(eventsString))+len([]rune(line)) > charsLeftForEvents-tailStringLen) { + eventsLenLimitReached = true + break + } + + eventsString += line + eventsPrinted++ + } + eventsString += "\n" + eventsString += formatter.codeBlockEnd + + if eventsLenLimitReached { + eventsString += tailString + } + + if throttled { + eventsString += throttleMsg + } + + return eventsString +} diff --git a/senders/msgformat/highlighter_test.go b/senders/msgformat/highlighter_test.go new file mode 100644 index 000000000..79bfaf85b --- /dev/null +++ b/senders/msgformat/highlighter_test.go @@ -0,0 +1,182 @@ +package msgformat + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/moira-alert/moira" + "github.com/moira-alert/moira/senders/emoji_provider" + + . "github.com/smartystreets/goconvey/convey" +) + +const ( + testMaxChars = 4_000 +) + +func TestFormat(t *testing.T) { + Convey("Given configured formatter", t, func() { + location, locationErr := time.LoadLocation("UTC") + So(locationErr, ShouldBeNil) + + provider, err := emoji_provider.NewEmojiProvider("", nil) + So(err, ShouldBeNil) + formatter := NewHighlightSyntaxFormatter( + provider, + false, + "http://moira.url", + location, + testUriFormatter, + testDescriptionFormatter, + testBoldFormatter, + testEventStringFormatter, + "```", + "```", + ) + + event := moira.NotificationEvent{ + TriggerID: "TriggerID", + Values: map[string]float64{"t1": 123}, + Timestamp: 150000000, + Metric: "Metric", + OldState: moira.StateOK, + State: moira.StateNODATA, + } + + const shortDesc = `My description` + trigger := moira.TriggerData{ + Tags: []string{"tag1", "tag2"}, + Name: "Name", + ID: "TriggerID", + Desc: shortDesc, + } + + Convey("Message with one event", func() { + events, throttled := moira.NotificationEvents{event}, false + msg := formatter.Format(getParams(events, trigger, throttled)) + + expected := "**NODATA** [Name](http://moira.url/trigger/TriggerID) [tag1][tag2]\n" + + shortDesc + "\n" + + "```\n" + + "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```" + So(msg, ShouldEqual, expected) + }) + + Convey("Message with one event and throttled", func() { + events, throttled := moira.NotificationEvents{event}, true + msg := formatter.Format(getParams(events, trigger, throttled)) + + expected := "**NODATA** [Name](http://moira.url/trigger/TriggerID) [tag1][tag2]\n" + + shortDesc + "\n" + + "```\n" + + "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```" + "\n" + + "Please, **fix your system or tune this trigger** to generate less events." + So(msg, ShouldEqual, expected) + }) + + Convey("Moira message with 3 events", func() { + actual := formatter.Format(getParams([]moira.NotificationEvent{event, event, event}, trigger, false)) + expected := "**NODATA** [Name](http://moira.url/trigger/TriggerID) [tag1][tag2]\n" + + shortDesc + "\n" + + "```\n" + + "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n" + + "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n" + + "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```" + So(actual, ShouldResemble, expected) + }) + + Convey("Long message parts", func() { + const ( + msgLimit = 4_000 + halfLimit = msgLimit / 2 + greaterThanHalf = halfLimit + 100 + lessThanHalf = halfLimit - 100 + ) + + const eventLine = "\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)" + oneEventLineLen := len([]rune(eventLine)) + + longDesc := strings.Repeat("a", greaterThanHalf) + + // Events list with chars greater than half of the message limit + var longEvents moira.NotificationEvents + for i := 0; i < greaterThanHalf/oneEventLineLen; i++ { + longEvents = append(longEvents, event) + } + + Convey("Long description. desc > msgLimit/2", func() { + var events moira.NotificationEvents + for i := 0; i < lessThanHalf/oneEventLineLen; i++ { + events = append(events, event) + } + + actual := formatter.Format(getParams(events, moira.TriggerData{Desc: longDesc}, false)) + expected := "**NODATA**\n" + + strings.Repeat("a", 2100) + "\n" + + "```\n" + + strings.Repeat("02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n", 39) + + "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```" + So(actual, ShouldResemble, expected) + }) + + Convey("Many events. eventString > msgLimit/2", func() { + desc := strings.Repeat("a", lessThanHalf) + actual := formatter.Format(getParams(longEvents, moira.TriggerData{Desc: desc}, false)) + expected := "**NODATA**\n" + + desc + "\n" + + "```\n" + + strings.Repeat("02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n", 43) + + "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```" + So(actual, ShouldResemble, expected) + }) + + Convey("Long description and many events. both desc and events > msgLimit/2", func() { + actual := formatter.Format(getParams(longEvents, moira.TriggerData{Desc: longDesc}, false)) + expected := "**NODATA**\n" + + strings.Repeat("a", 1984) + "...\n" + + "```\n" + + strings.Repeat("02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n", 40) + + "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```\n" + + "...and 3 more events." + So(actual, ShouldResemble, expected) + }) + }) + }) +} + +func testBoldFormatter(str string) string { + return fmt.Sprintf("**%s**", str) +} + +func testDescriptionFormatter(trigger moira.TriggerData) string { + desc := trigger.Desc + if trigger.Desc != "" { + desc += "\n" + } + return desc +} + +func testUriFormatter(triggerURI, triggerName string) string { + return fmt.Sprintf("[%s](%s)", triggerName, triggerURI) +} + +func testEventStringFormatter(event moira.NotificationEvent, location *time.Location) string { + return fmt.Sprintf( + "%s: %s = %s (%s to %s)", + event.FormatTimestamp(location, moira.DefaultTimeFormat), + event.Metric, + event.GetMetricsValues(moira.DefaultNotificationSettings), + event.OldState, + event.State) +} + +func getParams(events moira.NotificationEvents, trigger moira.TriggerData, throttled bool) MessageFormatterParams { + return MessageFormatterParams{ + Events: events, + Trigger: trigger, + MessageMaxChars: testMaxChars, + Throttled: throttled, + } +} diff --git a/senders/msgformat/msgformat.go b/senders/msgformat/msgformat.go new file mode 100644 index 000000000..5d00724b2 --- /dev/null +++ b/senders/msgformat/msgformat.go @@ -0,0 +1,23 @@ +// Package msgformat provides MessageFormatter interface which may be used for formatting messages. +// Also, it contains some realizations such as HighlightSyntaxFormatter. +package msgformat + +import ( + "github.com/moira-alert/moira" +) + +const changeRecommendation = "fix your system or tune this trigger" + +// MessageFormatter is used for formatting messages to send via telegram, mattermost, etc. +type MessageFormatter interface { + Format(params MessageFormatterParams) string +} + +// MessageFormatterParams is the parameters for MessageFormatter. +type MessageFormatterParams struct { + Events moira.NotificationEvents + Trigger moira.TriggerData + // MessageMaxChars is a limit for future message. If -1 then no limit is set. + MessageMaxChars int + Throttled bool +} diff --git a/senders/slack/slack.go b/senders/slack/slack.go index b5c4f7aac..54f960c90 100644 --- a/senders/slack/slack.go +++ b/senders/slack/slack.go @@ -3,13 +3,13 @@ package slack import ( "bytes" "fmt" - "strings" "time" + "github.com/moira-alert/moira/senders/msgformat" + "github.com/mitchellh/mapstructure" slackdown "github.com/moira-alert/blackfriday-slack" "github.com/moira-alert/moira" - "github.com/moira-alert/moira/senders" "github.com/moira-alert/moira/senders/emoji_provider" slack_client "github.com/slack-go/slack" @@ -22,7 +22,11 @@ const ( ErrorTextChannelArchived = "is_archived" ErrorTextChannelNotFound = "channel_not_found" ErrorTextNotInChannel = "not_in_channel" - quotes = "```" +) + +var ( + codeBlockStart = "```" + codeBlockEnd = "```" ) // Structure that represents the Slack configuration in the YAML file. @@ -36,12 +40,10 @@ type config struct { // Sender implements moira sender interface via slack. type Sender struct { - frontURI string - useEmoji bool emojiProvider emoji_provider.StateEmojiGetter logger moira.Logger - location *time.Location client *slack_client.Client + formatter msgformat.MessageFormatter } // Init read yaml config. @@ -57,13 +59,21 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca } emojiProvider, err := emoji_provider.NewEmojiProvider(cfg.DefaultEmoji, cfg.EmojiMap) if err != nil { - return fmt.Errorf("cannot initialize mattermost sender, err: %w", err) + return fmt.Errorf("cannot initialize slack sender, err: %w", err) } - sender.emojiProvider = emojiProvider - sender.useEmoji = cfg.UseEmoji sender.logger = logger - sender.frontURI = cfg.FrontURI - sender.location = location + sender.emojiProvider = emojiProvider + sender.formatter = msgformat.NewHighlightSyntaxFormatter( + emojiProvider, + cfg.UseEmoji, + cfg.FrontURI, + location, + uriFormatter, + descriptionFormatter, + boldFormatter, + eventStringFormatter, + codeBlockStart, + codeBlockEnd) sender.client = slack_client.New(cfg.APIToken) return nil } @@ -96,35 +106,19 @@ func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira. } func (sender *Sender) buildMessage(events moira.NotificationEvents, trigger moira.TriggerData, throttled bool) string { - var message strings.Builder - - title := sender.buildTitle(events, trigger, throttled) - titleLen := len([]rune(title)) - - desc := sender.buildDescription(trigger) - descLen := len([]rune(desc)) - - eventsString := sender.buildEventsString(events, -1, throttled) - eventsStringLen := len([]rune(eventsString)) - - charsLeftAfterTitle := messageMaxCharacters - titleLen - - descNewLen, eventsNewLen := senders.CalculateMessagePartsLength(charsLeftAfterTitle, descLen, eventsStringLen) - - if descLen != descNewLen { - desc = desc[:descNewLen] + "...\n" - } - if eventsNewLen != eventsStringLen { - eventsString = sender.buildEventsString(events, eventsNewLen, throttled) - } + return sender.formatter.Format(msgformat.MessageFormatterParams{ + Events: events, + Trigger: trigger, + MessageMaxChars: messageMaxCharacters, + Throttled: throttled, + }) +} - message.WriteString(title) - message.WriteString(desc) - message.WriteString(eventsString) - return message.String() +func uriFormatter(triggerURI, triggerName string) string { + return fmt.Sprintf("<%s|%s>", triggerURI, triggerName) } -func (sender *Sender) buildDescription(trigger moira.TriggerData) string { +func descriptionFormatter(trigger moira.TriggerData) string { desc := trigger.Desc if trigger.Desc != "" { desc = string(slackdown.Run([]byte(desc))) @@ -133,69 +127,18 @@ func (sender *Sender) buildDescription(trigger moira.TriggerData) string { return desc } -func (sender *Sender) buildTitle(events moira.NotificationEvents, trigger moira.TriggerData, throttled bool) string { - state := events.GetCurrentState(throttled) - title := fmt.Sprintf("*%s*", state) - triggerURI := trigger.GetTriggerURI(sender.frontURI) - - if triggerURI != "" { - title += fmt.Sprintf(" <%s|%s>", triggerURI, trigger.Name) - } else if trigger.Name != "" { - title += " " + trigger.Name - } - - tags := trigger.GetTags() - if tags != "" { - title += " " + tags - } - - title += "\n" - return title +func boldFormatter(str string) string { + return fmt.Sprintf("*%s*", str) } -// buildEventsString builds the string from moira events and limits it to charsForEvents -// if n is negative buildEventsString does not limit the events string. -func (sender *Sender) buildEventsString(events moira.NotificationEvents, charsForEvents int, throttled bool) string { - charsForThrottleMsg := 0 - throttleMsg := "\nPlease, *fix your system or tune this trigger* to generate less events." - if throttled { - charsForThrottleMsg = len([]rune(throttleMsg)) - } - charsLeftForEvents := charsForEvents - charsForThrottleMsg - - var eventsString string - eventsString += quotes - var tailString string - - eventsLenLimitReached := false - eventsPrinted := 0 - for _, event := range events { - line := fmt.Sprintf("\n%s: %s = %s (%s to %s)", event.FormatTimestamp(sender.location, moira.DefaultTimeFormat), event.Metric, event.GetMetricsValues(moira.DefaultNotificationSettings), event.OldState, event.State) - if msg := event.CreateMessage(sender.location); len(msg) > 0 { - line += fmt.Sprintf(". %s", msg) - } - - tailString = fmt.Sprintf("\n...and %d more events.", len(events)-eventsPrinted) - tailStringLen := len([]rune(quotes)) + len([]rune(tailString)) - if !(charsForEvents < 0) && (len([]rune(eventsString))+len([]rune(line)) > charsLeftForEvents-tailStringLen) { - eventsLenLimitReached = true - break - } - - eventsString += line - eventsPrinted++ - } - eventsString += quotes - - if eventsLenLimitReached { - eventsString += tailString - } - - if throttled { - eventsString += throttleMsg - } - - return eventsString +func eventStringFormatter(event moira.NotificationEvent, loc *time.Location) string { + return fmt.Sprintf( + "%s: %s = %s (%s to %s)", + event.FormatTimestamp(loc, moira.DefaultTimeFormat), + event.Metric, + event.GetMetricsValues(moira.DefaultNotificationSettings), + event.OldState, + event.State) } func (sender *Sender) sendMessage(message string, contact string, triggerID string, useDirectMessaging bool, emoji string) (string, string, error) { diff --git a/senders/slack/slack_test.go b/senders/slack/slack_test.go index 35c9e2a97..ac58d2f68 100644 --- a/senders/slack/slack_test.go +++ b/senders/slack/slack_test.go @@ -1,7 +1,6 @@ package slack import ( - "fmt" "strings" "testing" "time" @@ -18,33 +17,18 @@ func TestInit(t *testing.T) { senderSettings := map[string]interface{}{} Convey("Empty map", func() { err := sender.Init(senderSettings, logger, nil, "") - So(err, ShouldResemble, fmt.Errorf("can not read slack api_token from config")) - So(sender, ShouldResemble, Sender{}) + So(err, ShouldNotBeNil) + }) + + Convey("has empty api_token", func() { + senderSettings["api_token"] = "" + err := sender.Init(senderSettings, logger, nil, "") + So(err, ShouldNotBeNil) }) Convey("has api_token", func() { senderSettings["api_token"] = "123" - Convey("use_emoji not set", func() { - err := sender.Init(senderSettings, logger, nil, "") - So(err, ShouldBeNil) - So(sender.useEmoji, ShouldBeFalse) - }) - - Convey("use_emoji set to false", func() { - senderSettings["use_emoji"] = false - err := sender.Init(senderSettings, logger, nil, "") - So(err, ShouldBeNil) - So(sender.useEmoji, ShouldBeFalse) - }) - - Convey("use_emoji set to true", func() { - senderSettings["use_emoji"] = true - err := sender.Init(senderSettings, logger, nil, "") - So(err, ShouldBeNil) - So(sender.useEmoji, ShouldBeTrue) - }) - Convey("use_emoji set to something wrong", func() { senderSettings["use_emoji"] = 123 err := sender.Init(senderSettings, logger, nil, "") @@ -64,10 +48,19 @@ func TestUseDirectMessaging(t *testing.T) { } func TestBuildMessage(t *testing.T) { + logger, _ := logging.ConfigureLog("stdout", "debug", "test", true) location, _ := time.LoadLocation("UTC") - sender := Sender{location: location, frontURI: "http://moira.url"} Convey("Build Moira Message tests", t, func() { + sender := &Sender{} + senderSettings := map[string]interface{}{ + "use_emoji": false, + "front_uri": "http://moira.url", + "api_token": "qwerty", + } + initErr := sender.Init(senderSettings, logger, location, moira.DefaultDateTimeFormat) + So(initErr, ShouldBeNil) + event := moira.NotificationEvent{ TriggerID: "TriggerID", Values: map[string]float64{"t1": 123}, @@ -97,13 +90,13 @@ some other text italic text Convey("Print moira message with one event", func() { actual := sender.buildMessage([]moira.NotificationEvent{event}, trigger, false) expected := "*NODATA* [tag1][tag2]\n" + slackCompatibleMD + - "\n\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)```" + "\n\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```" So(actual, ShouldResemble, expected) }) Convey("Print moira message with empty trigger", func() { actual := sender.buildMessage([]moira.NotificationEvent{event}, moira.TriggerData{}, false) - expected := "*NODATA*\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)```" + expected := "*NODATA*\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```" So(actual, ShouldResemble, expected) }) @@ -112,27 +105,27 @@ some other text italic text event.MessageEventInfo = &moira.EventInfo{Interval: &interval} actual := sender.buildMessage([]moira.NotificationEvent{event}, trigger, false) expected := "*NODATA* [tag1][tag2]\n" + slackCompatibleMD + - "\n\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA). This metric has been in bad state for more than 24 hours - please, fix.```" + "\n\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA). This metric has been in bad state for more than 24 hours - please, fix.\n```" So(actual, ShouldResemble, expected) }) Convey("Print moira message with one event and throttled", func() { actual := sender.buildMessage([]moira.NotificationEvent{event}, trigger, true) expected := "*NODATA* [tag1][tag2]\n" + slackCompatibleMD + - "\n\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)```\nPlease, *fix your system or tune this trigger* to generate less events." + "\n\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```\nPlease, *fix your system or tune this trigger* to generate less events." So(actual, ShouldResemble, expected) }) Convey("Print moira message with 6 events", func() { actual := sender.buildMessage([]moira.NotificationEvent{event, event, event, event, event, event}, trigger, false) expected := "*NODATA* [tag1][tag2]\n" + slackCompatibleMD + - "\n\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)```" + "\n\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```" So(actual, ShouldResemble, expected) }) Convey("Print moira message with empty triggerID, but with trigger name", func() { actual := sender.buildMessage([]moira.NotificationEvent{event}, moira.TriggerData{Name: "Name"}, false) - expected := "*NODATA* Name\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)```" + expected := "*NODATA* Name\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```" So(actual, ShouldResemble, expected) }) @@ -156,7 +149,7 @@ some other text italic text Convey("Print moira message with desc + events < msgLimit", func() { actual := sender.buildMessage(shortEvents, moira.TriggerData{Desc: longDesc}, false) - expected := "*NODATA*\n" + longDesc + "\n```" + shortEventsString + "```" + expected := "*NODATA*\n" + longDesc + "\n```" + shortEventsString + "\n```" So(actual, ShouldResemble, expected) }) @@ -168,28 +161,26 @@ some other text italic text eventsString += eventLine } actual := sender.buildMessage(events, moira.TriggerData{Desc: longDesc}, false) - expected := "*NODATA*\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)```" + expected := "*NODATA*\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```" So(actual, ShouldResemble, expected) }) Convey("Print moira message events string > msgLimit/2", func() { desc := strings.Repeat("a", messageMaxCharacters/2-100) actual := sender.buildMessage(longEvents, moira.TriggerData{Desc: desc}, false) - expected := "*NODATA*\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)```\n...and 3 more events." + expected := "*NODATA*\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```\n...and 3 more events." So(actual, ShouldResemble, expected) }) Convey("Print moira message with both desc and events > msgLimit/2", func() { actual := sender.buildMessage(longEvents, moira.TriggerData{Desc: longDesc}, false) - expected := "*NODATA*\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)```\n...and 5 more events." + expected := "*NODATA*\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```\n...and 5 more events." So(actual, ShouldResemble, expected) }) }) } func TestBuildDescription(t *testing.T) { - location, _ := time.LoadLocation("UTC") - sender := Sender{location: location, frontURI: "http://moira.url"} Convey("Build desc tests", t, func() { trigger := moira.TriggerData{ Desc: `# header1 @@ -206,13 +197,13 @@ some other text italic text ` Convey("Build empty desc", func() { - actual := sender.buildDescription(moira.TriggerData{Desc: ""}) + actual := descriptionFormatter(moira.TriggerData{Desc: ""}) expected := "" So(actual, ShouldResemble, expected) }) Convey("Build desc with headers and bold", func() { - actual := sender.buildDescription(trigger) + actual := descriptionFormatter(trigger) expected := slackCompatibleMD + "\n\n" So(actual, ShouldResemble, expected) }) @@ -230,8 +221,8 @@ some other text italic text ` - Convey("Expect buildDescription not to panic", func() { - actual := sender.buildDescription(trigger) + Convey("Expect descriptionFormatter not to panic", func() { + actual := descriptionFormatter(trigger) So(actual, ShouldEqual, expected) }) diff --git a/senders/telegram/emoji_provider.go b/senders/telegram/emoji_provider.go new file mode 100644 index 000000000..05bd63431 --- /dev/null +++ b/senders/telegram/emoji_provider.go @@ -0,0 +1,18 @@ +package telegram + +import "github.com/moira-alert/moira" + +var emojiStates = map[moira.State]string{ + moira.StateOK: "\xe2\x9c\x85", + moira.StateWARN: "\xe2\x9a\xa0", + moira.StateERROR: "\xe2\xad\x95", + moira.StateNODATA: "\xf0\x9f\x92\xa3", + moira.StateTEST: "\xf0\x9f\x98\x8a", +} + +type telegramEmojiProvider struct{} + +// GetStateEmoji returns emoji suitable for moira.State. +func (_ telegramEmojiProvider) GetStateEmoji(subjectState moira.State) string { + return emojiStates[subjectState] +} diff --git a/senders/telegram/init.go b/senders/telegram/init.go index 5982a0016..d423fce4d 100644 --- a/senders/telegram/init.go +++ b/senders/telegram/init.go @@ -3,9 +3,14 @@ package telegram import ( "errors" "fmt" + "regexp" "strings" "time" + "github.com/russross/blackfriday/v2" + + "github.com/moira-alert/moira/senders/msgformat" + "github.com/mitchellh/mapstructure" "github.com/moira-alert/moira" "github.com/moira-alert/moira/worker" @@ -21,16 +26,20 @@ const ( ) var ( - pollerTimeout = 10 * time.Second - emojiStates = map[moira.State]string{ - moira.StateOK: "\xe2\x9c\x85", - moira.StateWARN: "\xe2\x9a\xa0", - moira.StateERROR: "\xe2\xad\x95", - moira.StateNODATA: "\xf0\x9f\x92\xa3", - moira.StateTEST: "\xf0\x9f\x98\x8a", - } + // startHeaderRegexp is used for removing start html header tag from description (which is converted from markdown to html). + // Because of not supporting it in telegram. + startHeaderRegexp = regexp.MustCompile("") + + // endHeaderRegexp is used for removing end html header tag from description (which is converted from markdown to html). + // Because of not supporting it in telegram. + endHeaderRegexp = regexp.MustCompile("") + + codeBlockStart = "
" + codeBlockEnd = "
" ) +var pollerTimeout = 10 * time.Second + // Structure that represents the Telegram configuration in the YAML file. type config struct { ContactType string `mapstructure:"contact_type"` @@ -51,12 +60,11 @@ type Bot interface { // Sender implements moira sender interface via telegram. type Sender struct { - DataBase moira.Database - logger moira.Logger - apiToken string - frontURI string - bot Bot - location *time.Location + DataBase moira.Database + logger moira.Logger + bot Bot + formatter msgformat.MessageFormatter + apiToken string } func (sender *Sender) removeTokenFromError(err error) error { @@ -77,13 +85,24 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca if cfg.APIToken == "" { return fmt.Errorf("can not read telegram api_token from config") } - sender.apiToken = cfg.APIToken - sender.frontURI = cfg.FrontURI + + emojiProvider := telegramEmojiProvider{} + sender.formatter = msgformat.NewHighlightSyntaxFormatter( + emojiProvider, + true, + cfg.FrontURI, + location, + urlFormatter, + descriptionFormatter, + boldFormatter, + eventStringFormatter, + codeBlockStart, + codeBlockEnd) + sender.logger = logger - sender.location = location sender.bot, err = telebot.NewBot(telebot.Settings{ - Token: sender.apiToken, + Token: cfg.APIToken, Poller: &telebot.LongPoller{Timeout: pollerTimeout}, }) if err != nil { @@ -126,3 +145,39 @@ func (sender *Sender) runTelebot(contactType string) { func telegramLockKey(contactType string) string { return telegramLockPrefix + contactType } + +func urlFormatter(triggerURI, triggerName string) string { + return fmt.Sprintf("%s", triggerURI, triggerName) +} + +func descriptionFormatter(trigger moira.TriggerData) string { + desc := trigger.Desc + if trigger.Desc != "" { + desc = trigger.Desc + desc += "\n" + } + + htmlDescStr := string(blackfriday.Run([]byte(desc))) + + // html headers are not supported by telegram html, so make them bold instead. + htmlDescStr = startHeaderRegexp.ReplaceAllString(htmlDescStr, "") + withReplacedHeaders := endHeaderRegexp.ReplaceAllString(htmlDescStr, "") + + // html paragraphs are not supported by telegram html, so delete them. + replacer := strings.NewReplacer("

", "", "

", "") + return replacer.Replace(withReplacedHeaders) +} + +func boldFormatter(str string) string { + return fmt.Sprintf("%s", str) +} + +func eventStringFormatter(event moira.NotificationEvent, loc *time.Location) string { + return fmt.Sprintf( + "%s: %s = %s (%s to %s)", + event.FormatTimestamp(loc, moira.DefaultTimeFormat), + event.Metric, + event.GetMetricsValues(moira.DefaultNotificationSettings), + event.OldState, + event.State) +} diff --git a/senders/telegram/init_test.go b/senders/telegram/init_test.go index 3542fc13f..f55cdfcd0 100644 --- a/senders/telegram/init_test.go +++ b/senders/telegram/init_test.go @@ -26,10 +26,8 @@ func TestInit(t *testing.T) { "front_uri": "http://moira.uri", } sender.Init(senderSettings, logger, location, "15:04") //nolint - So(sender.apiToken, ShouldResemble, "123") - So(sender.frontURI, ShouldResemble, "http://moira.uri") So(sender.logger, ShouldResemble, logger) - So(sender.location, ShouldResemble, location) + So(sender.apiToken, ShouldResemble, "123") }) }) } diff --git a/senders/telegram/send.go b/senders/telegram/send.go index 87f29d616..71af4df1e 100644 --- a/senders/telegram/send.go +++ b/senders/telegram/send.go @@ -8,6 +8,7 @@ import ( "strconv" "strings" + "github.com/moira-alert/moira/senders/msgformat" "gopkg.in/telebot.v3" "github.com/moira-alert/moira" @@ -18,14 +19,13 @@ type messageType string const ( // Album type used if notification has plots. Album messageType = "album" - // Message type used if notification has not plot. + // Message type used if notification has no plot. Message messageType = "message" ) const ( - albumCaptionMaxCharacters = 1024 - messageMaxCharacters = 4096 - additionalInfoCharactersCount = 400 + albumCaptionMaxCharacters = 1024 + messageMaxCharacters = 4096 ) var characterLimits = map[messageType]int{ @@ -35,7 +35,7 @@ var characterLimits = map[messageType]int{ var unmarshalTypeError *json.UnmarshalTypeError -// Structure that represents chat metadata required to send message to recipient. +// Chat is a structure that represents chat metadata required to send message to recipient. // It implements gopkg.in/telebot.v3#Recipient interface and thus might be passed to telebot methods directly. type Chat struct { ID int64 `json:"chat_id" example:"-1001234567890"` @@ -55,7 +55,7 @@ var brokenContactAPIErrors = map[*telebot.Error]struct{}{ telebot.ErrNotStartedByUser: {}, } -// Chat implements gopkg.in/telebot.v3#Recipient interface. +// Recipient allow Chat implements gopkg.in/telebot.v3#Recipient interface. func (c *Chat) Recipient() string { return strconv.FormatInt(c.ID, 10) } @@ -82,49 +82,12 @@ func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira. } func (sender *Sender) buildMessage(events moira.NotificationEvents, trigger moira.TriggerData, throttled bool, maxChars int) string { - var buffer bytes.Buffer - state := events.GetCurrentState(throttled) - tags := trigger.GetTags() - emoji := emojiStates[state] - - title := fmt.Sprintf("%s%s %s %s (%d)\n", emoji, state, trigger.Name, tags, len(events)) - buffer.WriteString(title) - - var messageCharsCount, printEventsCount int - messageCharsCount += len([]rune(title)) - messageLimitReached := false - - for _, event := range events { - line := fmt.Sprintf("\n%s: %s = %s (%s to %s)", event.FormatTimestamp(sender.location, moira.DefaultTimeFormat), event.Metric, event.GetMetricsValues(moira.DefaultNotificationSettings), event.OldState, event.State) - if msg := event.CreateMessage(sender.location); len(msg) > 0 { - line += fmt.Sprintf(". %s", msg) - } - - lineCharsCount := len([]rune(line)) - if messageCharsCount+lineCharsCount > maxChars-additionalInfoCharactersCount { - messageLimitReached = true - break - } - - buffer.WriteString(line) - messageCharsCount += lineCharsCount - printEventsCount++ - } - - if messageLimitReached { - buffer.WriteString(fmt.Sprintf("\n\n...and %d more events.", len(events)-printEventsCount)) - } - - url := trigger.GetTriggerURI(sender.frontURI) - if url != "" { - buffer.WriteString(fmt.Sprintf("\n\n%s\n", url)) - } - - if throttled { - buffer.WriteString("\nPlease, fix your system or tune this trigger to generate less events.") - } - - return buffer.String() + return sender.formatter.Format(msgformat.MessageFormatterParams{ + Events: events, + Trigger: trigger, + MessageMaxChars: maxChars, + Throttled: throttled, + }) } func (sender *Sender) getChat(contactValue string) (*Chat, error) { @@ -247,7 +210,11 @@ func (sender *Sender) talk(chat *Chat, message string, plots [][]byte, messageTy } func (sender *Sender) sendAsMessage(chat *Chat, message string) error { - _, err := sender.bot.Send(chat, message, &telebot.SendOptions{ThreadID: chat.ThreadID}) + _, err := sender.bot.Send(chat, message, &telebot.SendOptions{ + ThreadID: chat.ThreadID, + ParseMode: telebot.ModeHTML, + DisableWebPagePreview: true, + }) if err != nil { err = sender.removeTokenFromError(err) sender.logger.Debug(). @@ -308,7 +275,11 @@ func prepareAlbum(plots [][]byte, caption string) telebot.Album { func (sender *Sender) sendAsAlbum(chat *Chat, plots [][]byte, caption string) error { album := prepareAlbum(plots, caption) - _, err := sender.bot.SendAlbum(chat, album, &telebot.SendOptions{ThreadID: chat.ThreadID}) + _, err := sender.bot.SendAlbum(chat, album, &telebot.SendOptions{ + ThreadID: chat.ThreadID, + ParseMode: telebot.ModeHTML, + DisableWebPagePreview: true, + }) if err != nil { err = sender.removeTokenFromError(err) sender.logger.Debug(). diff --git a/senders/telegram/send_test.go b/senders/telegram/send_test.go index 69e4393b5..e7413021b 100644 --- a/senders/telegram/send_test.go +++ b/senders/telegram/send_test.go @@ -4,7 +4,6 @@ import ( "fmt" "strconv" "testing" - "time" "github.com/pkg/errors" "go.uber.org/mock/gomock" @@ -19,113 +18,13 @@ import ( "gopkg.in/telebot.v3" ) -func TestBuildMessage(t *testing.T) { - location, _ := time.LoadLocation("UTC") - sender := Sender{location: location, frontURI: "http://moira.url"} - - Convey("Build Moira Message tests", t, func() { - event := moira.NotificationEvent{ - TriggerID: "TriggerID", - Values: map[string]float64{"t1": 97.4458331200185}, - Timestamp: 150000000, - Metric: "Metric name", - OldState: moira.StateOK, - State: moira.StateNODATA, - } - - trigger := moira.TriggerData{ - Tags: []string{"tag1", "tag2"}, - Name: "Trigger Name", - ID: "TriggerID", - } - - Convey("Print moira message with one event", func() { - actual := sender.buildMessage([]moira.NotificationEvent{event}, trigger, false, messageMaxCharacters) - expected := `đź’ŁNODATA Trigger Name [tag1][tag2] (1) - -02:40 (GMT+00:00): Metric name = 97.4458331200185 (OK to NODATA) - -http://moira.url/trigger/TriggerID -` - So(actual, ShouldResemble, expected) - }) - - Convey("Print moira message with empty triggerID, but with trigger Name", func() { - actual := sender.buildMessage([]moira.NotificationEvent{event}, moira.TriggerData{Name: "Name"}, false, messageMaxCharacters) - expected := `đź’ŁNODATA Name (1) - -02:40 (GMT+00:00): Metric name = 97.4458331200185 (OK to NODATA)` - So(actual, ShouldResemble, expected) - }) - - Convey("Print moira message with empty trigger", func() { - actual := sender.buildMessage([]moira.NotificationEvent{event}, moira.TriggerData{}, false, messageMaxCharacters) - expected := `đź’ŁNODATA (1) - -02:40 (GMT+00:00): Metric name = 97.4458331200185 (OK to NODATA)` - So(actual, ShouldResemble, expected) - }) - - Convey("Print moira message with one event and message", func() { - event.TriggerID = "" - trigger.ID = "" - var interval int64 = 24 - event.MessageEventInfo = &moira.EventInfo{Interval: &interval} - actual := sender.buildMessage([]moira.NotificationEvent{event}, trigger, false, messageMaxCharacters) - expected := `đź’ŁNODATA Trigger Name [tag1][tag2] (1) - -02:40 (GMT+00:00): Metric name = 97.4458331200185 (OK to NODATA). This metric has been in bad state for more than 24 hours - please, fix.` - So(actual, ShouldResemble, expected) - }) - - Convey("Print moira message with one event and throttled", func() { - actual := sender.buildMessage([]moira.NotificationEvent{event}, trigger, true, messageMaxCharacters) - expected := `đź’ŁNODATA Trigger Name [tag1][tag2] (1) - -02:40 (GMT+00:00): Metric name = 97.4458331200185 (OK to NODATA) - -http://moira.url/trigger/TriggerID - -Please, fix your system or tune this trigger to generate less events.` - So(actual, ShouldResemble, expected) - }) - - events := make([]moira.NotificationEvent, 0) - Convey("Print moira message with 6 events and photo message length", func() { - for i := 0; i < 18; i++ { - events = append(events, event) - } - actual := sender.buildMessage(events, trigger, false, albumCaptionMaxCharacters) - expected := `đź’ŁNODATA Trigger Name [tag1][tag2] (18) - -02:40 (GMT+00:00): Metric name = 97.4458331200185 (OK to NODATA) -02:40 (GMT+00:00): Metric name = 97.4458331200185 (OK to NODATA) -02:40 (GMT+00:00): Metric name = 97.4458331200185 (OK to NODATA) -02:40 (GMT+00:00): Metric name = 97.4458331200185 (OK to NODATA) -02:40 (GMT+00:00): Metric name = 97.4458331200185 (OK to NODATA) -02:40 (GMT+00:00): Metric name = 97.4458331200185 (OK to NODATA) -02:40 (GMT+00:00): Metric name = 97.4458331200185 (OK to NODATA) -02:40 (GMT+00:00): Metric name = 97.4458331200185 (OK to NODATA) -02:40 (GMT+00:00): Metric name = 97.4458331200185 (OK to NODATA) - -...and 9 more events. - -http://moira.url/trigger/TriggerID -` - fmt.Printf("Bytes: %v\n", len(expected)) - fmt.Printf("Symbols: %v\n", len([]rune(expected))) - So(actual, ShouldResemble, expected) - }) - }) -} - func TestGetChat(t *testing.T) { - location, _ := time.LoadLocation("UTC") mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() dataBase := mock_moira_alert.NewMockDatabase(mockCtrl) + bot := mock_telegram.NewMockBot(mockCtrl) - sender := Sender{location: location, frontURI: "http://moira.url", DataBase: dataBase, bot: bot} + sender := Sender{DataBase: dataBase, bot: bot} Convey("Get Telegram Chat From DB", t, func() { Convey("Compatibility with Moira < 2.12.0", func() {