diff --git a/senders/emoji_provider/provider.go b/senders/emoji_provider/provider.go new file mode 100644 index 000000000..614408941 --- /dev/null +++ b/senders/emoji_provider/provider.go @@ -0,0 +1,55 @@ +package emoji_provider + +import ( + "fmt" + "maps" + + "github.com/moira-alert/moira" +) + +var defaultStateEmoji = map[moira.State]string{ + moira.StateOK: ":moira-state-ok:", + moira.StateWARN: ":moira-state-warn:", + moira.StateERROR: ":moira-state-error:", + moira.StateNODATA: ":moira-state-nodata:", + moira.StateEXCEPTION: ":moira-state-exception:", + moira.StateTEST: ":moira-state-test:", +} + +// emojiProvider is struct for get emoji by trigger State. +type emojiProvider struct { + defaultValue string + stateEmojiMap map[moira.State]string +} + +// StateEmojiGetter is interface for emojiProvider. +type StateEmojiGetter interface { + GetStateEmoji(subjectState moira.State) string +} + +// NewEmojiProvider is construct for emojiProvider. +func NewEmojiProvider(defaultValue string, stateEmojiMap map[string]string) (StateEmojiGetter, error) { + emojiMap := maps.Clone(defaultStateEmoji) + + for state, emoji := range stateEmojiMap { + converted := moira.State(state) + if _, ok := emojiMap[converted]; !ok { + return nil, fmt.Errorf("undefined Moira's state: %s", state) + } + emojiMap[converted] = emoji + } + + return &emojiProvider{ + defaultValue: defaultValue, + stateEmojiMap: emojiMap, + }, nil +} + +// GetStateEmoji returns corresponding state emoji. +func (em *emojiProvider) GetStateEmoji(subjectState moira.State) string { + if emoji, ok := em.stateEmojiMap[subjectState]; ok { + return emoji + } + + return em.defaultValue +} diff --git a/senders/emoji_provider/provider_test.go b/senders/emoji_provider/provider_test.go new file mode 100644 index 000000000..f4e83e75e --- /dev/null +++ b/senders/emoji_provider/provider_test.go @@ -0,0 +1,67 @@ +package emoji_provider + +import ( + "testing" + + "github.com/moira-alert/moira" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestNewEmojiProvider(t *testing.T) { + Convey("Successful creation, no errors", t, func() { + em, err := NewEmojiProvider( + "default", + map[string]string{ + "OK": "super_ok", + "WARN": "super_warn", + "ERROR": "super_error", + "TEST": "super_test", + "EXCEPTION": "super_exception", + "NODATA": "super_nodata", + }, + ) + So(err, ShouldBeNil) + expected := &emojiProvider{ + defaultValue: "default", + stateEmojiMap: map[moira.State]string{ + "OK": "super_ok", + "WARN": "super_warn", + "ERROR": "super_error", + "TEST": "super_test", + "EXCEPTION": "super_exception", + "NODATA": "super_nodata", + }, + } + So(em, ShouldResemble, expected) + }) + + Convey("Unsuccessful creation, has error", t, func() { + em, err := NewEmojiProvider( + "default", + map[string]string{ + "OK": "super_ok", + "dfgdf": "super_warn", + "ERROR": "super_error", + "TEST": "super_test", + "EXCEPTION": "super_exception", + "NODATA": "super_nodata", + }, + ) + So(err.Error(), ShouldResemble, "undefined Moira's state: dfgdf") + So(em, ShouldBeNil) + }) +} + +func TestEmojiProvider_GetStateEmoji(t *testing.T) { + Convey("Check state emoji", t, func() { + em := &emojiProvider{stateEmojiMap: defaultStateEmoji, defaultValue: "default_value"} + So(em.GetStateEmoji(moira.StateOK), ShouldResemble, ":moira-state-ok:") + So(em.GetStateEmoji(moira.StateWARN), ShouldResemble, ":moira-state-warn:") + So(em.GetStateEmoji(moira.StateERROR), ShouldResemble, ":moira-state-error:") + So(em.GetStateEmoji(moira.StateNODATA), ShouldResemble, ":moira-state-nodata:") + So(em.GetStateEmoji(moira.StateEXCEPTION), ShouldResemble, ":moira-state-exception:") + So(em.GetStateEmoji(moira.StateTEST), ShouldResemble, ":moira-state-test:") + So(em.GetStateEmoji("dfdfdf"), ShouldResemble, "default_value") + }) +} diff --git a/senders/mattermost/sender.go b/senders/mattermost/sender.go index 2c3c3f28a..2a1687214 100644 --- a/senders/mattermost/sender.go +++ b/senders/mattermost/sender.go @@ -10,6 +10,7 @@ import ( "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" "github.com/mitchellh/mapstructure" @@ -17,20 +18,25 @@ import ( // Structure that represents the Mattermost configuration in the YAML file. type config struct { - Url string `mapstructure:"url"` - InsecureTLS bool `mapstructure:"insecure_tls"` - APIToken string `mapstructure:"api_token"` - FrontURI string `mapstructure:"front_uri"` + Url string `mapstructure:"url"` + InsecureTLS bool `mapstructure:"insecure_tls"` + APIToken string `mapstructure:"api_token"` + FrontURI string `mapstructure:"front_uri"` + UseEmoji bool `mapstructure:"use_emoji"` + DefaultEmoji string `mapstructure:"default_emoji"` + EmojiMap map[string]string `mapstructure:"emoji_map"` } // Sender posts messages to Mattermost chat. // It implements moira.Sender. // You must call Init method before SendEvents method. type Sender struct { - frontURI string - logger moira.Logger - location *time.Location - client Client + frontURI string + useEmoji bool + emojiProvider emoji_provider.StateEmojiGetter + logger moira.Logger + location *time.Location + client Client } const ( @@ -70,7 +76,14 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca if cfg.FrontURI == "" { return fmt.Errorf("can not read Mattermost front_uri from config") } + + emojiProvider, err := emoji_provider.NewEmojiProvider(cfg.DefaultEmoji, cfg.EmojiMap) + 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 @@ -101,7 +114,6 @@ 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)) @@ -137,7 +149,12 @@ func (sender *Sender) buildDescription(trigger moira.TriggerData) string { func (sender *Sender) buildTitle(events moira.NotificationEvents, trigger moira.TriggerData, throttled bool) string { state := events.GetCurrentState(throttled) - title := fmt.Sprintf("**%s**", state) + 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) diff --git a/senders/slack/slack.go b/senders/slack/slack.go index 4c2661d50..b5c4f7aac 100644 --- a/senders/slack/slack.go +++ b/senders/slack/slack.go @@ -10,18 +10,12 @@ import ( 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" ) const ( - okEmoji = ":moira-state-ok:" - warnEmoji = ":moira-state-warn:" - errorEmoji = ":moira-state-error:" - nodataEmoji = ":moira-state-nodata:" - exceptionEmoji = ":moira-state-exception:" - testEmoji = ":moira-state-test:" - messageMaxCharacters = 4000 // see errors https://api.slack.com/methods/chat.postMessage @@ -31,29 +25,23 @@ const ( quotes = "```" ) -var stateEmoji = map[moira.State]string{ - moira.StateOK: okEmoji, - moira.StateWARN: warnEmoji, - moira.StateERROR: errorEmoji, - moira.StateNODATA: nodataEmoji, - moira.StateEXCEPTION: exceptionEmoji, - moira.StateTEST: testEmoji, -} - // Structure that represents the Slack configuration in the YAML file. type config struct { - APIToken string `mapstructure:"api_token"` - UseEmoji bool `mapstructure:"use_emoji"` - FrontURI string `mapstructure:"front_uri"` + APIToken string `mapstructure:"api_token"` + UseEmoji bool `mapstructure:"use_emoji"` + FrontURI string `mapstructure:"front_uri"` + DefaultEmoji string `mapstructure:"default_emoji"` + EmojiMap map[string]string `mapstructure:"emoji_map"` } // Sender implements moira sender interface via slack. type Sender struct { - frontURI string - useEmoji bool - logger moira.Logger - location *time.Location - client *slack_client.Client + frontURI string + useEmoji bool + emojiProvider emoji_provider.StateEmojiGetter + logger moira.Logger + location *time.Location + client *slack_client.Client } // Init read yaml config. @@ -67,6 +55,11 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca if cfg.APIToken == "" { return fmt.Errorf("can not read slack api_token from config") } + emojiProvider, err := emoji_provider.NewEmojiProvider(cfg.DefaultEmoji, cfg.EmojiMap) + if err != nil { + return fmt.Errorf("cannot initialize mattermost sender, err: %w", err) + } + sender.emojiProvider = emojiProvider sender.useEmoji = cfg.UseEmoji sender.logger = logger sender.frontURI = cfg.FrontURI @@ -81,7 +74,7 @@ func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira. useDirectMessaging := useDirectMessaging(contact.Value) state := events.GetCurrentState(throttled) - emoji := sender.getStateEmoji(state) + emoji := sender.emojiProvider.GetStateEmoji(state) channelID, threadTimestamp, err := sender.sendMessage(message, contact.Value, trigger.ID, useDirectMessaging, emoji) if err != nil { @@ -252,16 +245,6 @@ func (sender *Sender) sendPlots(plots [][]byte, channelID, threadTimestamp, trig return nil } -// getStateEmoji returns corresponding state emoji. -func (sender *Sender) getStateEmoji(subjectState moira.State) string { - if sender.useEmoji { - if emoji, ok := stateEmoji[subjectState]; ok { - return emoji - } - } - return slack_client.DEFAULT_MESSAGE_ICON_EMOJI -} - // useDirectMessaging returns true if user contact is provided. func useDirectMessaging(contactValue string) bool { return len(contactValue) > 0 && contactValue[0:1] == "@" diff --git a/senders/slack/slack_test.go b/senders/slack/slack_test.go index a060df00d..35c9e2a97 100644 --- a/senders/slack/slack_test.go +++ b/senders/slack/slack_test.go @@ -63,23 +63,6 @@ func TestUseDirectMessaging(t *testing.T) { }) } -func TestGetStateEmoji(t *testing.T) { - sender := Sender{} - Convey("Use emoji is false", t, func() { - So(sender.getStateEmoji(moira.StateERROR), ShouldResemble, "") - }) - - Convey("Use emoji is true", t, func() { - sender := Sender{useEmoji: true} - So(sender.getStateEmoji(moira.StateOK), ShouldResemble, okEmoji) - So(sender.getStateEmoji(moira.StateWARN), ShouldResemble, warnEmoji) - So(sender.getStateEmoji(moira.StateERROR), ShouldResemble, errorEmoji) - So(sender.getStateEmoji(moira.StateNODATA), ShouldResemble, nodataEmoji) - So(sender.getStateEmoji(moira.StateEXCEPTION), ShouldResemble, exceptionEmoji) - So(sender.getStateEmoji(moira.StateTEST), ShouldResemble, testEmoji) - }) -} - func TestBuildMessage(t *testing.T) { location, _ := time.LoadLocation("UTC") sender := Sender{location: location, frontURI: "http://moira.url"}