Skip to content

Commit

Permalink
feat: markdown description to telegram senders (#1055)
Browse files Browse the repository at this point in the history
  • Loading branch information
AleksandrMatsko committed Jul 29, 2024
1 parent 6558fad commit c15c44d
Show file tree
Hide file tree
Showing 13 changed files with 618 additions and 563 deletions.
8 changes: 5 additions & 3 deletions datatypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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(".")
Expand Down
174 changes: 53 additions & 121 deletions senders/mattermost/sender.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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) {
Expand Down
126 changes: 0 additions & 126 deletions senders/mattermost/sender_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ package mattermost
import (
"context"
"errors"
"strings"
"testing"
"time"

"github.com/mattermost/mattermost/server/public/model"

Expand Down Expand Up @@ -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)
})
})
})
}
Loading

0 comments on commit c15c44d

Please sign in to comment.