-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for multiple alert channels (#33)
- Loading branch information
Showing
10 changed files
with
366 additions
and
102 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,51 +1,72 @@ | ||
package alert | ||
|
||
import ( | ||
"fmt" | ||
|
||
viperutil "github.com/Conflux-Chain/go-conflux-util/viper" | ||
"github.com/sirupsen/logrus" | ||
"github.com/spf13/viper" | ||
) | ||
|
||
// AlertConfig alert configuration such as DingTalk settings etc. | ||
type AlertConfig struct { | ||
// Custom tags are usually used to differentiate between different networks and enviroments | ||
// such as mainnet/testnet, prod/test/dev or any custom info for more details. | ||
CustomTags []string `default:"[dev,test]"` | ||
// Alert severity level. | ||
type Severity int | ||
|
||
const ( | ||
SeverityLow Severity = iota | ||
SeverityMedium | ||
SeverityHigh | ||
SeverityCritical | ||
) | ||
|
||
// DingTalk settings | ||
DingTalk DingTalkConfig | ||
func (s Severity) String() string { | ||
switch s { | ||
case SeverityLow: | ||
return "low" | ||
case SeverityMedium: | ||
return "medium" | ||
case SeverityHigh: | ||
return "high" | ||
case SeverityCritical: | ||
return "critical" | ||
default: | ||
return "unknown" | ||
} | ||
} | ||
|
||
// DingTalkConfig DingTalk configurations | ||
type DingTalkConfig struct { | ||
Enabled bool // switch to turn on or off DingTalk | ||
AtMobiles []string // mobiles for @ members | ||
IsAtAll bool // whether to @ all members | ||
Webhook string // webhook url | ||
Secret string // secret token | ||
// Notification channel type. | ||
type ChannelType string | ||
|
||
const ( | ||
ChannelTypeDingTalk ChannelType = "dingtalk" | ||
) | ||
|
||
// Notification channel interface. | ||
type Channel interface { | ||
Name() string | ||
Type() ChannelType | ||
Send(note *Notification) error | ||
} | ||
|
||
// Notification represents core information for an alert. | ||
type Notification struct { | ||
Title string // message title | ||
Content string // message content | ||
Severity Severity // severity level | ||
} | ||
|
||
// MustInitFromViper inits alert from viper settings or panic on error. | ||
func MustInitFromViper() { | ||
var config AlertConfig | ||
viperutil.MustUnmarshalKey("alert", &config, func(key string) (interface{}, bool) { | ||
switch key { | ||
case "alert.customTags", "alert.dingtalk.atMobiles": | ||
return viper.GetStringSlice(key), true | ||
} | ||
var conf struct { | ||
CustomTags []string `default:"[dev,test]"` | ||
Channels map[string]interface{} | ||
} | ||
|
||
return nil, false | ||
}) | ||
viperutil.MustUnmarshalKey("alert", &conf) | ||
|
||
Init(config) | ||
} | ||
formatter := NewSimpleTextFormatter(conf.CustomTags) | ||
for chID, chmap := range conf.Channels { | ||
ch, err := parseAlertChannel(chID, chmap.(map[string]interface{}), formatter) | ||
if err != nil { | ||
logrus.WithField("channelId", chID).Fatal("Failed to parse alert channel") | ||
} | ||
|
||
// Init inits alert with provided configurations. | ||
func Init(config AlertConfig) { | ||
if config.DingTalk.Enabled { | ||
InitDingTalk(&config.DingTalk, config.CustomTags) | ||
logrus.WithField("config", fmt.Sprintf("%+v", config)).Debug("Alert (dingtalk) initialized") | ||
DefaultManager().Add(ch) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,46 +1,48 @@ | ||
package alert | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
"time" | ||
|
||
"github.com/pkg/errors" | ||
"github.com/royeo/dingrobot" | ||
) | ||
|
||
const ( | ||
// DingTalk alert message template | ||
dingTalkAlertMsgTpl = "logrus alert notification\ntags:\t%v;\nlevel:\t%v;\nbrief:\t%v;\ndetail:\t%v;\ntime:\t%v\n" | ||
) | ||
|
||
var ( | ||
dingTalkCustomTagsStr string | ||
dingTalkConfig *DingTalkConfig | ||
dingRobot dingrobot.Roboter | ||
_ Channel = (*DingTalkChannel)(nil) | ||
) | ||
|
||
// InitDingTalk inits DingTalk with provided configurations. | ||
func InitDingTalk(config *DingTalkConfig, customTags []string) { | ||
if !config.Enabled { | ||
return | ||
} | ||
type DingTalkConfig struct { | ||
Platform string // notify platform | ||
AtMobiles []string // mobiles for @ members | ||
IsAtAll bool // whether to @ all members | ||
Webhook string // webhook url | ||
Secret string // secret token | ||
} | ||
|
||
dingTalkCustomTagsStr = strings.Join(customTags, "/") | ||
dingTalkConfig = config | ||
// DingTalkChannel DingTalk notification channel | ||
type DingTalkChannel struct { | ||
Formatter Formatter // message formatter | ||
ID string // channel id | ||
Config DingTalkConfig // channel config | ||
} | ||
|
||
// init DingTalk robots | ||
dingRobot = dingrobot.NewRobot(config.Webhook) | ||
dingRobot.SetSecret(config.Secret) | ||
func NewDingTalkChannel(chID string, fmt Formatter, conf DingTalkConfig) *DingTalkChannel { | ||
return &DingTalkChannel{ID: chID, Formatter: fmt, Config: conf} | ||
} | ||
|
||
// SendDingTalkTextMessage sends text message to DingTalk group chat. | ||
func SendDingTalkTextMessage(level, brief, detail string) error { | ||
if dingRobot == nil { // robot not set | ||
return nil | ||
} | ||
func (dtc *DingTalkChannel) Name() string { | ||
return dtc.ID | ||
} | ||
|
||
nowStr := time.Now().Format("2006-01-02T15:04:05-0700") | ||
msg := fmt.Sprintf(dingTalkAlertMsgTpl, dingTalkCustomTagsStr, level, brief, detail, nowStr) | ||
func (dtc *DingTalkChannel) Type() ChannelType { | ||
return ChannelType(dtc.Config.Platform) | ||
} | ||
|
||
func (dtc *DingTalkChannel) Send(note *Notification) error { | ||
msg, err := dtc.Formatter.Format(note) | ||
if err != nil { | ||
return errors.WithMessage(err, "failed to format alert msg") | ||
} | ||
|
||
return dingRobot.SendText(msg, dingTalkConfig.AtMobiles, dingTalkConfig.IsAtAll) | ||
dingRobot := dingrobot.NewRobot(dtc.Config.Webhook) | ||
dingRobot.SetSecret(dtc.Config.Secret) | ||
return dingRobot.SendText(msg, dtc.Config.AtMobiles, dtc.Config.IsAtAll) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package alert | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
"time" | ||
) | ||
|
||
// Formatter defines how messages are formatted. | ||
type Formatter interface { | ||
Format(note *Notification) (string, error) | ||
} | ||
|
||
type SimpleTextFormatter struct { | ||
tags []string | ||
} | ||
|
||
func NewSimpleTextFormatter(tags []string) *SimpleTextFormatter { | ||
return &SimpleTextFormatter{tags: tags} | ||
} | ||
|
||
func (f *SimpleTextFormatter) Format(note *Notification) (string, error) { | ||
tagStr := strings.Join(f.tags, "/") | ||
nowStr := time.Now().Format("2006-01-02T15:04:05-0700") | ||
str := fmt.Sprintf( | ||
"%v\nseverity:\t%s;\ntags:\t%v;\n%v;\ntime:\t%v", | ||
note.Title, note.Severity, tagStr, note.Content, nowStr, | ||
) | ||
return str, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package alert | ||
|
||
import "sync" | ||
|
||
var ( | ||
stdMgr *Manager | ||
oncer sync.Once | ||
) | ||
|
||
func DefaultManager() *Manager { | ||
oncer.Do(func() { | ||
stdMgr = NewManager() | ||
}) | ||
return stdMgr | ||
} | ||
|
||
type Manager struct { | ||
mu sync.Mutex | ||
allChannels map[string]Channel | ||
} | ||
|
||
func NewManager() *Manager { | ||
return &Manager{ | ||
allChannels: make(map[string]Channel), | ||
} | ||
} | ||
|
||
func (m *Manager) Add(ch Channel) Channel { | ||
m.mu.Lock() | ||
defer m.mu.Unlock() | ||
|
||
old := m.allChannels[ch.Name()] | ||
m.allChannels[ch.Name()] = ch | ||
|
||
return old | ||
} | ||
|
||
func (m *Manager) Del(name string) { | ||
m.mu.Lock() | ||
defer m.mu.Unlock() | ||
|
||
delete(m.allChannels, name) | ||
} | ||
|
||
func (m *Manager) Channel(name string) (Channel, bool) { | ||
m.mu.Lock() | ||
defer m.mu.Unlock() | ||
|
||
ch, ok := m.allChannels[name] | ||
return ch, ok | ||
} | ||
|
||
func (m *Manager) All(name string) (chs []Channel) { | ||
m.mu.Lock() | ||
defer m.mu.Unlock() | ||
|
||
for _, v := range m.allChannels { | ||
chs = append(chs, v) | ||
} | ||
|
||
return chs | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package alert | ||
|
||
import ( | ||
"github.com/mitchellh/mapstructure" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
func ErrChannelTypeNotSupported(chType string) error { | ||
return errors.Errorf("channel type %s not supported", chType) | ||
} | ||
|
||
func ErrChannelNotFound(ch string) error { | ||
return errors.Errorf("channel %s not found", ch) | ||
} | ||
|
||
func parseAlertChannel(chID string, chmap map[string]interface{}, fmt Formatter) (Channel, error) { | ||
cht, ok := chmap["platform"].(string) | ||
if !ok { | ||
return nil, ErrChannelTypeNotSupported(cht) | ||
} | ||
|
||
switch ChannelType(cht) { | ||
case ChannelTypeDingTalk: | ||
var dtconf DingTalkConfig | ||
if err := decodeChannelConfig(chmap, &dtconf); err != nil { | ||
return nil, err | ||
} | ||
|
||
return NewDingTalkChannel(chID, fmt, dtconf), nil | ||
// NOTE: add more channel types support here if needed | ||
default: | ||
return nil, ErrChannelTypeNotSupported(cht) | ||
} | ||
} | ||
|
||
func decodeChannelConfig(chmap map[string]interface{}, valPtr interface{}) error { | ||
decoderConfig := mapstructure.DecoderConfig{ | ||
TagName: "json", | ||
Result: valPtr, | ||
} | ||
|
||
// Create a new Decoder instance | ||
decoder, err := mapstructure.NewDecoder(&decoderConfig) | ||
if err != nil { | ||
return errors.WithMessage(err, "failed to new mapstructure decoder") | ||
} | ||
|
||
// Decode the map into the struct | ||
if err := decoder.Decode(chmap); err != nil { | ||
return errors.WithMessage(err, "failed to decode mapstructure") | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.