Skip to content

Commit

Permalink
Add support for multiple alert channels (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
wanliqun authored May 6, 2024
1 parent 35904d3 commit 57bf8df
Show file tree
Hide file tree
Showing 10 changed files with 366 additions and 102 deletions.
85 changes: 53 additions & 32 deletions alert/alert.go
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)
}
}
62 changes: 32 additions & 30 deletions alert/dingtalk.go
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)
}
30 changes: 30 additions & 0 deletions alert/formatter.go
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
}
62 changes: 62 additions & 0 deletions alert/manager.go
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
}
54 changes: 54 additions & 0 deletions alert/util.go
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
}
21 changes: 14 additions & 7 deletions config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,24 @@
# level: info
# forceColor: false
# disableColor: false
# alertHookLevels: [warn,error,fatal]
# alertHook: # Alert hooking settings
# # Hooked logrus levels for alert notification
# levels: [warn,error,fatal]
# # Default notification channels
# channels: []

# Alert Configurations
# alert:
# # 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: [dev,test]
# dingtalk:
# enabled: false
# webhook: https://oapi.dingtalk.com/robot/send?access_token=${your_access_token}
# secret: ${your_access_secret}
# atMobiles: []
# isAtAll: false
# channels: # Notification channels
# dingrobot:
# platform: dingtalk
# webhook: https://oapi.dingtalk.com/robot/send?access_token=${your_access_token}
# secret: ${your_access_secret}
# atMobiles: []
# isAtAll: false

# REST API Configurations
# api:
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/gin-gonic/gin v1.7.7
github.com/go-playground/validator/v10 v10.4.1
github.com/mcuadros/go-defaults v1.2.0
github.com/mitchellh/mapstructure v1.4.3
github.com/pkg/errors v0.9.1
github.com/royeo/dingrobot v1.0.1-0.20191230075228-c90a788ca8fd
github.com/sirupsen/logrus v1.8.1
Expand Down Expand Up @@ -52,7 +53,6 @@ require (
github.com/magiconair/properties v1.8.5 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
Expand Down
Loading

0 comments on commit 57bf8df

Please sign in to comment.