diff --git a/api/controller/events.go b/api/controller/events.go index c38fd9152..f982273c5 100644 --- a/api/controller/events.go +++ b/api/controller/events.go @@ -8,7 +8,12 @@ import ( "github.com/moira-alert/moira/api/dto" ) -// GetTriggerEvents gets trigger event from current page and all trigger event count. Events list is filtered by time range +const ( + zeroPage int64 = 0 + allEventsSize int64 = -1 +) + +// GetTriggerEvents gets trigger events from current page and total count of filtered trigger events. Events list is filtered by time range // with `from` and `to` params (`from` and `to` should be "+inf", "-inf" or int64 converted to string), // by metric (regular expression) and by states. If `states` map is empty or nil then all states are accepted. func GetTriggerEvents( @@ -19,11 +24,36 @@ func GetTriggerEvents( metricRegexp *regexp.Regexp, states map[string]struct{}, ) (*dto.EventsList, *api.ErrorResponse) { - events, err := getFilteredNotificationEvents(database, triggerID, page, size, from, to, metricRegexp, states) + events, err := getFilteredNotificationEvents(database, triggerID, from, to, metricRegexp, states) if err != nil { return nil, api.ErrorInternalServer(err) } - eventCount := database.GetNotificationEventCount(triggerID, -1) + + eventCount := int64(len(events)) + + if page < 0 || (page > 0 && size < 0) { + return &dto.EventsList{ + Size: size, + Page: page, + Total: eventCount, + List: []moira.NotificationEvent{}, + }, nil + } + + if page >= 0 && size >= 0 { + start := page * size + end := start + size + + if start >= eventCount { + events = []*moira.NotificationEvent{} + } else { + if end > eventCount { + end = eventCount + } + + events = events[start:end] + } + } eventsList := &dto.EventsList{ Size: size, @@ -42,44 +72,16 @@ func GetTriggerEvents( func getFilteredNotificationEvents( database moira.Database, triggerID string, - page, size int64, from, to string, metricRegexp *regexp.Regexp, states map[string]struct{}, ) ([]*moira.NotificationEvent, error) { - // fetch all events - if size < 0 { - events, err := database.GetNotificationEvents(triggerID, page, size, from, to) - if err != nil { - return nil, err - } - - return filterNotificationEvents(events, metricRegexp, states), nil - } - - // fetch at most `size` events - filtered := make([]*moira.NotificationEvent, 0, size) - var count int64 - - for int64(len(filtered)) < size { - eventsData, err := database.GetNotificationEvents(triggerID, page+count, size, from, to) - if err != nil { - return nil, err - } - - if len(eventsData) == 0 { - break - } - - filtered = append(filtered, filterNotificationEvents(eventsData, metricRegexp, states)...) - count += 1 - - if int64(len(eventsData)) < size { - break - } + events, err := database.GetNotificationEvents(triggerID, zeroPage, allEventsSize, from, to) + if err != nil { + return nil, err } - return filtered, nil + return filterNotificationEvents(events, metricRegexp, states), nil } func filterNotificationEvents( diff --git a/api/controller/events_test.go b/api/controller/events_test.go index 4c84f7fc2..bd1a0c18a 100644 --- a/api/controller/events_test.go +++ b/api/controller/events_test.go @@ -25,31 +25,41 @@ func TestGetEvents(t *testing.T) { defer mockCtrl.Finish() triggerID := uuid.Must(uuid.NewV4()).String() - var page int64 = 10 - var size int64 = 100 + var page int64 = 1 + var size int64 = 2 from := "-inf" to := "+inf" Convey("Test has events", t, func() { - var total int64 = 6000000 - dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to). - Return([]*moira.NotificationEvent{ - { - State: moira.StateNODATA, - OldState: moira.StateOK, - }, - { - State: moira.StateOK, - OldState: moira.StateNODATA, - }, - }, nil) - dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) + events := []*moira.NotificationEvent{ + { + State: moira.StateNODATA, + OldState: moira.StateOK, + }, + { + State: moira.StateOK, + OldState: moira.StateNODATA, + }, + { + State: moira.StateWARN, + OldState: moira.StateOK, + }, + { + State: moira.StateERROR, + OldState: moira.StateWARN, + }, + } + dataBase.EXPECT().GetNotificationEvents(triggerID, zeroPage, allEventsSize, from, to). + Return(events, nil) list, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) So(err, ShouldBeNil) So(list, ShouldResemble, &dto.EventsList{ - List: []moira.NotificationEvent{{State: moira.StateNODATA, OldState: moira.StateOK}, {State: moira.StateOK, OldState: moira.StateNODATA}}, - Total: total, + List: []moira.NotificationEvent{ + *events[2], + *events[3], + }, + Total: int64(len(events)), Size: size, Page: page, }) @@ -57,8 +67,7 @@ func TestGetEvents(t *testing.T) { Convey("Test no events", t, func() { var total int64 - dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(make([]*moira.NotificationEvent, 0), nil) - dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) + dataBase.EXPECT().GetNotificationEvents(triggerID, zeroPage, allEventsSize, from, to).Return(make([]*moira.NotificationEvent, 0), nil) list, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) So(err, ShouldBeNil) So(list, ShouldResemble, &dto.EventsList{ @@ -71,7 +80,7 @@ func TestGetEvents(t *testing.T) { Convey("Test error", t, func() { expected := fmt.Errorf("oooops! Can not get all contacts") - dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(nil, expected) + dataBase.EXPECT().GetNotificationEvents(triggerID, zeroPage, allEventsSize, from, to).Return(nil, expected) list, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) So(err, ShouldResemble, api.ErrorInternalServer(expected)) So(list, ShouldBeNil) @@ -85,19 +94,23 @@ func TestGetEvents(t *testing.T) { filtered := []*moira.NotificationEvent{ {Metric: "metric.test.event1"}, {Metric: "a.metric.test.event2"}, + {Metric: "metric.test.event.other"}, } notFiltered := []*moira.NotificationEvent{ {Metric: "another.mEtric.test.event"}, {Metric: "metric.test"}, } - firstPortion := append(make([]*moira.NotificationEvent, 0), notFiltered[0], filtered[0]) - dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(firstPortion, nil) - secondPortion := append(make([]*moira.NotificationEvent, 0), filtered[1], notFiltered[1]) - dataBase.EXPECT().GetNotificationEvents(triggerID, page+1, size, from, to).Return(secondPortion, nil) + events := []*moira.NotificationEvent{ + notFiltered[0], + filtered[0], + notFiltered[1], + filtered[1], + filtered[2], + } + dataBase.EXPECT().GetNotificationEvents(triggerID, zeroPage, allEventsSize, from, to).Return(events, nil) - total := int64(len(firstPortion) + len(secondPortion)) - dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) + total := int64(len(filtered)) actual, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, regexp.MustCompile(`metric\.test\.event`), allStates) So(err, ShouldBeNil) @@ -105,7 +118,7 @@ func TestGetEvents(t *testing.T) { Page: page, Size: size, Total: total, - List: toDTOList(filtered), + List: toDTOList(filtered[:size]), }) }) }) @@ -125,8 +138,7 @@ func TestGetEvents(t *testing.T) { } Convey("with empty map all allowed", func() { total := int64(len(filtered) + len(notFiltered)) - dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(append(filtered, notFiltered...), nil) - dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) + dataBase.EXPECT().GetNotificationEvents(triggerID, zeroPage, allEventsSize, from, to).Return(append(filtered, notFiltered...), nil) actual, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) So(err, ShouldBeNil) @@ -139,9 +151,8 @@ func TestGetEvents(t *testing.T) { }) Convey("with given states", func() { - total := int64(len(filtered) + len(notFiltered)) - dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(append(filtered, notFiltered...), nil) - dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) + total := int64(len(filtered)) + dataBase.EXPECT().GetNotificationEvents(triggerID, zeroPage, allEventsSize, from, to).Return(append(filtered, notFiltered...), nil) actual, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, map[string]struct{}{ string(moira.StateOK): {}, @@ -158,6 +169,101 @@ func TestGetEvents(t *testing.T) { }) }) }) + + Convey("test paginating", t, func() { + events := []*moira.NotificationEvent{ + { + State: moira.StateNODATA, + OldState: moira.StateOK, + }, + { + State: moira.StateOK, + OldState: moira.StateNODATA, + }, + { + State: moira.StateWARN, + OldState: moira.StateOK, + }, + { + State: moira.StateERROR, + OldState: moira.StateWARN, + }, + } + total := int64(len(events)) + + type testcase struct { + description string + expectedEvents []moira.NotificationEvent + givenPage int64 + givenSize int64 + } + + testcases := []testcase{ + { + description: "with page > 0 and size > 0", + givenPage: 1, + givenSize: 1, + expectedEvents: []moira.NotificationEvent{ + *events[1], + }, + }, + { + description: "with page == 0 and size > 0", + givenPage: 0, + givenSize: 1, + expectedEvents: []moira.NotificationEvent{ + *events[0], + }, + }, + { + description: "with page > 0, size > 0, page * size + size > events count", + givenPage: 1, + givenSize: 3, + expectedEvents: []moira.NotificationEvent{ + *events[3], + }, + }, + { + description: "with page = 0, size < 0 fetch all events", + givenPage: 0, + givenSize: -10, + expectedEvents: toDTOList(events), + }, + { + description: "with page > 0, size < 0 return no events", + givenPage: 1, + givenSize: -1, + expectedEvents: []moira.NotificationEvent{}, + }, + { + description: "with page < 0 return no events", + givenPage: -1, + givenSize: 1, + expectedEvents: []moira.NotificationEvent{}, + }, + { + description: "with page * size >= len(events)", + givenPage: 1, + givenSize: int64(len(events)), + expectedEvents: []moira.NotificationEvent{}, + }, + } + + for i := range testcases { + Convey(fmt.Sprintf("test case %d: %s", i+1, testcases[i].description), func() { + dataBase.EXPECT().GetNotificationEvents(triggerID, zeroPage, allEventsSize, from, to).Return(events, nil) + + actual, err := GetTriggerEvents(dataBase, triggerID, testcases[i].givenPage, testcases[i].givenSize, from, to, allMetrics, allStates) + So(err, ShouldBeNil) + So(actual, ShouldResemble, &dto.EventsList{ + Page: testcases[i].givenPage, + Size: testcases[i].givenSize, + Total: total, + List: testcases[i].expectedEvents, + }) + }) + } + }) } func toDTOList(eventPtrs []*moira.NotificationEvent) []moira.NotificationEvent { diff --git a/api/handler/trigger_test.go b/api/handler/trigger_test.go index ad51a1840..5f099273f 100644 --- a/api/handler/trigger_test.go +++ b/api/handler/trigger_test.go @@ -428,7 +428,6 @@ func TestGetTriggerWithTriggerSource(t *testing.T) { db.EXPECT().GetTrigger(triggerId).Return(trigger, nil) db.EXPECT().GetTriggerThrottling(triggerId) db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) - db.EXPECT().GetNotificationEventCount(triggerId, gomock.Any()).Return(int64(0)) responseWriter := httptest.NewRecorder() getTrigger(responseWriter, request) @@ -472,7 +471,6 @@ func TestGetTriggerWithTriggerSource(t *testing.T) { db.EXPECT().GetTrigger(triggerId).Return(trigger, nil) db.EXPECT().GetTriggerThrottling(triggerId) db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) - db.EXPECT().GetNotificationEventCount(triggerId, gomock.Any()).Return(int64(0)) responseWriter := httptest.NewRecorder() getTrigger(responseWriter, request) @@ -516,7 +514,6 @@ func TestGetTriggerWithTriggerSource(t *testing.T) { db.EXPECT().GetTrigger(triggerId).Return(trigger, nil) db.EXPECT().GetTriggerThrottling(triggerId) db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) - db.EXPECT().GetNotificationEventCount(triggerId, gomock.Any()).Return(int64(0)) responseWriter := httptest.NewRecorder() getTrigger(responseWriter, request) diff --git a/interfaces.go b/interfaces.go index b6e4bcd18..94fc776bd 100644 --- a/interfaces.go +++ b/interfaces.go @@ -60,7 +60,7 @@ type Database interface { DeleteTriggerThrottling(triggerID string) error // NotificationEvent storing - GetNotificationEvents(triggerID string, start, size int64, from, to string) ([]*NotificationEvent, error) + GetNotificationEvents(triggerID string, page, size int64, from, to string) ([]*NotificationEvent, error) PushNotificationEvent(event *NotificationEvent, ui bool) error GetNotificationEventCount(triggerID string, from int64) int64 FetchNotificationEvent() (NotificationEvent, error) diff --git a/senders/mattermost/sender.go b/senders/mattermost/sender.go index d83dbf6c7..36cbabb37 100644 --- a/senders/mattermost/sender.go +++ b/senders/mattermost/sender.go @@ -90,6 +90,7 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca location, uriFormatter, descriptionFormatter, + msgformat.DefaultDescriptionCutter, boldFormatter, eventStringFormatter, codeBlockStart, diff --git a/senders/msgformat/highlighter.go b/senders/msgformat/highlighter.go index 156609957..9a78403e4 100644 --- a/senders/msgformat/highlighter.go +++ b/senders/msgformat/highlighter.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" "time" + "unicode/utf8" "github.com/moira-alert/moira" "github.com/moira-alert/moira/senders" @@ -23,8 +24,11 @@ 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 { +// DescriptionCutter cuts the given description to fit max size. +type DescriptionCutter func(desc string, maxSize int) 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 @@ -32,13 +36,14 @@ type HighlightSyntaxFormatter struct { useEmoji bool uriFormatter UriFormatter descriptionFormatter DescriptionFormatter + descriptionCutter DescriptionCutter boldFormatter BoldFormatter eventsStringFormatter EventStringFormatter codeBlockStart string codeBlockEnd string } -// NewHighlightSyntaxFormatter creates new HighlightSyntaxFormatter with given arguments. +// NewHighlightSyntaxFormatter creates new highlightSyntaxFormatter with given arguments. func NewHighlightSyntaxFormatter( emojiGetter emoji_provider.StateEmojiGetter, useEmoji bool, @@ -46,18 +51,20 @@ func NewHighlightSyntaxFormatter( location *time.Location, uriFormatter UriFormatter, descriptionFormatter DescriptionFormatter, + descriptionCutter DescriptionCutter, boldFormatter BoldFormatter, eventsStringFormatter EventStringFormatter, codeBlockStart string, codeBlockEnd string, ) MessageFormatter { - return &HighlightSyntaxFormatter{ + return &highlightSyntaxFormatter{ emojiGetter: emojiGetter, frontURI: frontURI, location: location, useEmoji: useEmoji, uriFormatter: uriFormatter, descriptionFormatter: descriptionFormatter, + descriptionCutter: descriptionCutter, boldFormatter: boldFormatter, eventsStringFormatter: eventsStringFormatter, codeBlockStart: codeBlockStart, @@ -66,25 +73,25 @@ func NewHighlightSyntaxFormatter( } // Format formats message using given params and formatter functions. -func (formatter *HighlightSyntaxFormatter) Format(params MessageFormatterParams) string { +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)) + titleLen := utf8.RuneCountInString(title) desc := formatter.descriptionFormatter(params.Trigger) - descLen := len([]rune(desc)) + descLen := utf8.RuneCountInString(desc) eventsString := formatter.buildEventsString(params.Events, -1, params.Throttled) - eventsStringLen := len([]rune(eventsString)) + eventsStringLen := utf8.RuneCountInString(eventsString) charsLeftAfterTitle := params.MessageMaxChars - titleLen descNewLen, eventsNewLen := senders.CalculateMessagePartsLength(charsLeftAfterTitle, descLen, eventsStringLen) if descLen != descNewLen { - desc = desc[:descNewLen] + "...\n" + desc = formatter.descriptionCutter(desc, descNewLen) } if eventsNewLen != eventsStringLen { eventsString = formatter.buildEventsString(params.Events, eventsNewLen, params.Throttled) @@ -96,7 +103,7 @@ func (formatter *HighlightSyntaxFormatter) Format(params MessageFormatterParams) return message.String() } -func (formatter *HighlightSyntaxFormatter) buildTitle(events moira.NotificationEvents, trigger moira.TriggerData, emoji string, throttled bool) string { +func (formatter *highlightSyntaxFormatter) buildTitle(events moira.NotificationEvents, trigger moira.TriggerData, emoji string, throttled bool) string { state := events.GetCurrentState(throttled) title := "" if formatter.useEmoji { @@ -122,11 +129,11 @@ func (formatter *HighlightSyntaxFormatter) buildTitle(events moira.NotificationE // 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 { +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)) + throttleMsg := fmt.Sprintf("\nPlease, %s to generate less events.", formatter.boldFormatter(ChangeTriggerRecommendation)) if throttled { - charsForThrottleMsg = len([]rune(throttleMsg)) + charsForThrottleMsg = utf8.RuneCountInString(throttleMsg) } charsLeftForEvents := charsForEvents - charsForThrottleMsg @@ -143,8 +150,8 @@ func (formatter *HighlightSyntaxFormatter) buildEventsString(events moira.Notifi } 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) { + tailStringLen := utf8.RuneCountInString(formatter.codeBlockEnd) + len("\n") + utf8.RuneCountInString(tailString) + if !(charsForEvents < 0) && (utf8.RuneCountInString(eventsString)+utf8.RuneCountInString(line) > charsLeftForEvents-tailStringLen) { eventsLenLimitReached = true break } diff --git a/senders/msgformat/highlighter_test.go b/senders/msgformat/highlighter_test.go index 79bfaf85b..470db230e 100644 --- a/senders/msgformat/highlighter_test.go +++ b/senders/msgformat/highlighter_test.go @@ -30,6 +30,7 @@ func TestFormat(t *testing.T) { location, testUriFormatter, testDescriptionFormatter, + DefaultDescriptionCutter, testBoldFormatter, testEventStringFormatter, "```", @@ -135,7 +136,7 @@ func TestFormat(t *testing.T) { 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" + + strings.Repeat("a", 1980) + "...\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" + diff --git a/senders/msgformat/msgformat.go b/senders/msgformat/msgformat.go index 5d00724b2..72a6cdb37 100644 --- a/senders/msgformat/msgformat.go +++ b/senders/msgformat/msgformat.go @@ -1,12 +1,12 @@ // Package msgformat provides MessageFormatter interface which may be used for formatting messages. -// Also, it contains some realizations such as HighlightSyntaxFormatter. +// 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" +const ChangeTriggerRecommendation = "fix your system or tune this trigger" // MessageFormatter is used for formatting messages to send via telegram, mattermost, etc. type MessageFormatter interface { @@ -21,3 +21,10 @@ type MessageFormatterParams struct { MessageMaxChars int Throttled bool } + +// DefaultDescriptionCutter cuts description, so len(newDesc) <= maxSize. Ensure that len(desc) >= maxSize and +// maxSize >= len("...\n"). +func DefaultDescriptionCutter(desc string, maxSize int) string { + suffix := "...\n" + return desc[:maxSize-len(suffix)] + suffix +} diff --git a/senders/slack/slack.go b/senders/slack/slack.go index 54f960c90..28976c6c6 100644 --- a/senders/slack/slack.go +++ b/senders/slack/slack.go @@ -70,6 +70,7 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca location, uriFormatter, descriptionFormatter, + msgformat.DefaultDescriptionCutter, boldFormatter, eventStringFormatter, codeBlockStart, diff --git a/senders/slack/slack_test.go b/senders/slack/slack_test.go index ac58d2f68..43471896f 100644 --- a/senders/slack/slack_test.go +++ b/senders/slack/slack_test.go @@ -161,7 +161,7 @@ some other text italic text eventsString += eventLine } actual := sender.buildMessage(events, moira.TriggerData{Desc: longDesc}, false) - 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```" + expected := "*NODATA*\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...\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) }) @@ -174,7 +174,7 @@ some other text italic text 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```\n...and 5 more events." + expected := "*NODATA*\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...\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) }) }) diff --git a/senders/telegram/init.go b/senders/telegram/init.go index 7815437e8..4659c1f69 100644 --- a/senders/telegram/init.go +++ b/senders/telegram/init.go @@ -3,7 +3,6 @@ package telegram import ( "errors" "fmt" - "html" "strings" "time" @@ -23,11 +22,6 @@ const ( hidden = "[DATA DELETED]" ) -var ( - codeBlockStart = "
" - codeBlockEnd = "" -) - var pollerTimeout = 10 * time.Second // Structure that represents the Telegram configuration in the YAML file. @@ -78,17 +72,11 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca sender.apiToken = cfg.APIToken emojiProvider := telegramEmojiProvider{} - sender.formatter = msgformat.NewHighlightSyntaxFormatter( + sender.formatter = NewTelegramMessageFormatter( emojiProvider, true, cfg.FrontURI, - location, - urlFormatter, - emptyDescriptionFormatter, - boldFormatter, - eventStringFormatter, - codeBlockStart, - codeBlockEnd) + location) sender.logger = logger sender.bot, err = telebot.NewBot(telebot.Settings{ @@ -135,25 +123,3 @@ 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, html.EscapeString(triggerName)) -} - -func emptyDescriptionFormatter(trigger moira.TriggerData) string { - return "" -} - -func boldFormatter(str string) string { - return fmt.Sprintf("%s", html.EscapeString(str)) -} - -func eventStringFormatter(event moira.NotificationEvent, loc *time.Location) string { - return fmt.Sprintf( - "%s:
%s
= %s (%s to %s)",
- event.FormatTimestamp(loc, moira.DefaultTimeFormat),
- html.EscapeString(event.Metric),
- html.EscapeString(event.GetMetricsValues(moira.DefaultNotificationSettings)),
- event.OldState,
- event.State)
-}
diff --git a/senders/telegram/message_formatter.go b/senders/telegram/message_formatter.go
new file mode 100644
index 000000000..8059a2231
--- /dev/null
+++ b/senders/telegram/message_formatter.go
@@ -0,0 +1,256 @@
+package telegram
+
+import (
+ "fmt"
+ "html"
+ "regexp"
+ "strings"
+ "time"
+ "unicode/utf8"
+
+ "github.com/moira-alert/moira"
+ "github.com/moira-alert/moira/senders"
+ "github.com/moira-alert/moira/senders/emoji_provider"
+ "github.com/moira-alert/moira/senders/msgformat"
+ "github.com/russross/blackfriday/v2"
+)
+
+const (
+ eventsBlockStart = "" + eventsBlockEnd = "" +) + +type messageFormatter struct { + // emojiGetter used in titles for better description. + emojiGetter emoji_provider.StateEmojiGetter + frontURI string + location *time.Location + useEmoji bool +} + +// NewTelegramMessageFormatter returns message formatter which is used in telegram sender. +// The message will be formatted with html tags supported by telegram. +func NewTelegramMessageFormatter( + emojiGetter emoji_provider.StateEmojiGetter, + useEmoji bool, + frontURI string, + location *time.Location, +) msgformat.MessageFormatter { + return &messageFormatter{ + emojiGetter: emojiGetter, + frontURI: frontURI, + location: location, + useEmoji: useEmoji, + } +} + +// Format formats message using given params and formatter functions. +func (formatter *messageFormatter) Format(params msgformat.MessageFormatterParams) string { + state := params.Events.GetCurrentState(params.Throttled) + emoji := formatter.emojiGetter.GetStateEmoji(state) + + title := formatter.buildTitle(params.Events, params.Trigger, emoji, params.Throttled) + titleLen := calcRunesCountWithoutHTML([]rune(title)) + + desc := descriptionFormatter(params.Trigger) + descLen := calcRunesCountWithoutHTML([]rune(desc)) + + eventsString := formatter.buildEventsString(params.Events, -1, params.Throttled) + eventsStringLen := calcRunesCountWithoutHTML([]rune(eventsString)) + + descNewLen, eventsNewLen := senders.CalculateMessagePartsLength(params.MessageMaxChars-titleLen, descLen, eventsStringLen) + if descLen != descNewLen { + desc = descriptionCutter(desc, descNewLen) + } + if eventsStringLen != eventsNewLen { + eventsString = formatter.buildEventsString(params.Events, eventsNewLen, params.Throttled) + } + + return title + desc + eventsString +} + +// calcRunesCountWithoutHTML is used for calculating symbols in text without html tags. Special symbols +// like `>`, `<` etc. are counted not as one symbol, for example, len([]rune(">")). +// This precision is enough for us to evaluate size of message. +func calcRunesCountWithoutHTML(htmlText []rune) int { + textLen := 0 + isTag := false + + for _, r := range htmlText { + if r == '<' { + isTag = true + continue + } + + if !isTag { + textLen += 1 + } + + if r == '>' { + isTag = false + } + } + + return textLen +} + +func (formatter *messageFormatter) buildTitle(events moira.NotificationEvents, trigger moira.TriggerData, emoji string, throttled bool) string { + state := events.GetCurrentState(throttled) + title := "" + if formatter.useEmoji { + title += emoji + " " + } + + title += boldFormatter(string(state)) + triggerURI := trigger.GetTriggerURI(formatter.frontURI) + if triggerURI != "" { + title += fmt.Sprintf(" %s", uriFormatter(triggerURI, trigger.Name)) + } else if trigger.Name != "" { + title += " " + trigger.Name + } + + tags := trigger.GetTags() + if tags != "" { + title += " " + tags + } + + title += "\n" + return title +} + +var throttleMsg = fmt.Sprintf("\nPlease, %s to generate less events.", boldFormatter(msgformat.ChangeTriggerRecommendation)) + +// 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 *messageFormatter) buildEventsString(events moira.NotificationEvents, charsForEvents int, throttled bool) string { + charsForThrottleMsg := 0 + if throttled { + charsForThrottleMsg = calcRunesCountWithoutHTML([]rune(throttleMsg)) + } + charsLeftForEvents := charsForEvents - charsForThrottleMsg + + var eventsString string + eventsString += eventsBlockStart + var tailString string + + eventsLenLimitReached := false + eventsPrinted := 0 + eventsStringLen := 0 + for _, event := range events { + line := fmt.Sprintf("\n%s", eventStringFormatter(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("\n") + utf8.RuneCountInString(tailString) + lineLen := calcRunesCountWithoutHTML([]rune(line)) + + if charsForEvents >= 0 && eventsStringLen+lineLen > charsLeftForEvents-tailStringLen { + eventsLenLimitReached = true + break + } + + eventsString += line + eventsStringLen += lineLen + eventsPrinted++ + } + eventsString += "\n" + eventsString += eventsBlockEnd + + if eventsLenLimitReached { + eventsString += tailString + } + + if throttled { + eventsString += throttleMsg + } + + return eventsString +} + +func uriFormatter(triggerURI, triggerName string) string { + return fmt.Sprintf("%s", triggerURI, html.EscapeString(triggerName)) +} + +var ( + startHeaderRegexp = regexp.MustCompile("
", "", + "
", "", + "%s
= %s (%s to %s)",
+ event.FormatTimestamp(loc, moira.DefaultTimeFormat),
+ html.EscapeString(event.Metric),
+ html.EscapeString(event.GetMetricsValues(moira.DefaultNotificationSettings)),
+ event.OldState,
+ event.State)
+}
diff --git a/senders/telegram/message_formatter_test.go b/senders/telegram/message_formatter_test.go
new file mode 100644
index 000000000..f6ed08cb2
--- /dev/null
+++ b/senders/telegram/message_formatter_test.go
@@ -0,0 +1,199 @@
+package telegram
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+ "time"
+ "unicode/utf8"
+
+ "github.com/moira-alert/moira"
+ "github.com/moira-alert/moira/senders/msgformat"
+
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+const testFrontURI = "https://moira.uri"
+
+func TestMessageFormatter_Format(t *testing.T) {
+ location, _ := time.LoadLocation("UTC")
+ emojiProvider := telegramEmojiProvider{}
+
+ formatter := NewTelegramMessageFormatter(
+ emojiProvider,
+ true,
+ testFrontURI,
+ location)
+
+ 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,
+ }
+
+ expectedFirstLine := "π£ NODATA Name [tag1][tag2]\n"
+ lenFirstLine := utf8.RuneCountInString(expectedFirstLine) -
+ utf8.RuneCountInString("")
+
+ eventStr := "02:40 (GMT+00:00): Metric
= 123 (OK to NODATA)\n"
+ lenEventStr := utf8.RuneCountInString(eventStr) - utf8.RuneCountInString("
") // 60 - 13 = 47
+
+ Convey("TelegramMessageFormatter", t, func() {
+ Convey("message with one event", func() {
+ events, throttled := moira.NotificationEvents{event}, false
+ expected := expectedFirstLine +
+ shortDesc + "\n" +
+ eventsBlockStart + "\n" +
+ eventStr +
+ eventsBlockEnd
+
+ msg := formatter.Format(getParams(events, trigger, throttled))
+
+ 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 := expectedFirstLine +
+ shortDesc + "\n" +
+ eventsBlockStart + "\n" +
+ eventStr +
+ eventsBlockEnd +
+ throttleMsg
+ So(msg, ShouldEqual, expected)
+ })
+
+ Convey("message with 3 events", func() {
+ events, throttled := moira.NotificationEvents{event, event, event}, false
+ expected := expectedFirstLine +
+ shortDesc + "\n" +
+ eventsBlockStart + "\n" +
+ strings.Repeat(eventStr, 3) +
+ eventsBlockEnd
+
+ msg := formatter.Format(getParams(events, trigger, throttled))
+
+ So(msg, ShouldEqual, expected)
+ })
+
+ Convey("message with complex description", func() {
+ trigger.Desc = "# ΠΠΎΡ ΠΎΠΏΠΈΡΠ°Π½ΠΈΠ΅\n\nΡΠΏΠΈΡΠΎΠΊ:\n- **ΠΆΠΈΡΠ½ΡΠΉ**\n- *ΠΊΡΡΡΠΈΠ²*\n- `ΠΊΠΎΠ΄`\n- ΠΏΠΎΠ΄ΡΡΡΠΊΠ½ΡΡΡΠΉ\n- ~~Π·Π°ΡΡΡΠΊΠ½ΡΡΡΠΉ~~\n" +
+ "\n------\nif a > b do smth\nif c < d do another thing\ntrue && false = false\ntrue || false = true\n" +
+ "\"Hello everybody!\", 'another quots'\nif I use something like ΠΊΠΎΠ΄
\n- <u>ΠΏΠΎΠ΄ΡΡΡΠΊΠ½ΡΡΡΠΉ</u>\n-