diff --git a/alert/alert.go b/alert/alert.go index 8ffc075..a36494d 100644 --- a/alert/alert.go +++ b/alert/alert.go @@ -47,25 +47,24 @@ type Channel interface { // Notification represents core information for an alert. type Notification struct { - Title string // message title - Content string // message content - Severity Severity // severity level + Title string // message title + Content interface{} // message content + Severity Severity // severity level } // MustInitFromViper inits alert from viper settings or panic on error. func MustInitFromViper() { var conf struct { - CustomTags []string `default:"[dev,test]"` + CustomTags []string `default:"[dev]"` Channels map[string]interface{} } viperutil.MustUnmarshalKey("alert", &conf) - formatter := NewSimpleTextFormatter(conf.CustomTags) for chID, chmap := range conf.Channels { - ch, err := parseAlertChannel(chID, chmap.(map[string]interface{}), formatter) + ch, err := parseAlertChannel(chID, chmap.(map[string]interface{}), conf.CustomTags) if err != nil { - logrus.WithField("channelId", chID).Fatal("Failed to parse alert channel") + logrus.WithField("channelId", chID).WithError(err).Fatal("Failed to parse alert channel") } DefaultManager().Add(ch) diff --git a/alert/dingtalk.go b/alert/dingtalk.go index beaecdc..58b2019 100644 --- a/alert/dingtalk.go +++ b/alert/dingtalk.go @@ -43,5 +43,5 @@ func (dtc *DingTalkChannel) Send(note *Notification) error { dingRobot := dingrobot.NewRobot(dtc.Config.Webhook) dingRobot.SetSecret(dtc.Config.Secret) - return dingRobot.SendText(msg, dtc.Config.AtMobiles, dtc.Config.IsAtAll) + return dingRobot.SendMarkdown(note.Title, msg, dtc.Config.AtMobiles, dtc.Config.IsAtAll) } diff --git a/alert/formatter.go b/alert/formatter.go index 56a6548..e5b1683 100644 --- a/alert/formatter.go +++ b/alert/formatter.go @@ -1,9 +1,15 @@ package alert import ( + "bytes" "fmt" "strings" + "text/template" "time" + + "github.com/go-telegram/bot" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" ) // Formatter defines how messages are formatted. @@ -11,6 +17,121 @@ type Formatter interface { Format(note *Notification) (string, error) } +type markdownFormatter struct { + tags []string + + defaultTpl *template.Template + logEntryTpl *template.Template +} + +func newMarkdownFormatter( + tags []string, funcMap template.FuncMap, strTemplates [2]string) (f *markdownFormatter, err error) { + var tpls [2]*template.Template + for i := range strTemplates { + tpls[i], err = template.New("markdown").Funcs(funcMap).Parse(strTemplates[i]) + if err != nil { + return nil, errors.WithMessage(err, "bad template") + } + } + + return &markdownFormatter{ + tags: tags, + defaultTpl: tpls[0], + logEntryTpl: tpls[1], + }, nil +} + +func (f *markdownFormatter) Format(note *Notification) (string, error) { + if _, ok := note.Content.(*logrus.Entry); ok { + return f.formatLogrusEntry(note) + } + + return f.formatDefault(note) +} + +func (f *markdownFormatter) formatLogrusEntry(note *Notification) (string, error) { + entry := note.Content.(*logrus.Entry) + entryError, _ := entry.Data[logrus.ErrorKey].(error) + + ctxFields := make(map[string]interface{}) + for k, v := range entry.Data { + if k == logrus.ErrorKey { + continue + } + ctxFields[k] = v + } + + buffer := bytes.Buffer{} + err := f.logEntryTpl.Execute(&buffer, struct { + Level logrus.Level + Tags []string + Time time.Time + Msg string + Error error + CtxFields map[string]interface{} + }{entry.Level, f.tags, entry.Time, entry.Message, entryError, ctxFields}) + if err != nil { + return "", errors.WithMessage(err, "template exec error") + } + + return buffer.String(), nil +} + +func (f *markdownFormatter) formatDefault(note *Notification) (string, error) { + buffer := bytes.Buffer{} + err := f.defaultTpl.Execute(&buffer, struct { + Title string + Tags []string + Severity Severity + Time time.Time + Content interface{} + }{note.Title, f.tags, note.Severity, time.Now(), note.Content}) + if err != nil { + return "", errors.WithMessage(err, "template exec error") + } + + return buffer.String(), nil +} + +type DingTalkMarkdownFormatter struct { + *markdownFormatter +} + +func NewDingtalkMarkdownFormatter(tags []string) (*DingTalkMarkdownFormatter, error) { + funcMap := template.FuncMap{"formatRFC3339": formatRFC3339} + mf, err := newMarkdownFormatter(tags, funcMap, [2]string(dingTalkMarkdownTemplates)) + if err != nil { + return nil, err + } + + return &DingTalkMarkdownFormatter{markdownFormatter: mf}, nil +} + +func formatRFC3339(t time.Time) string { + return t.Format(time.RFC3339) +} + +type TelegramMarkdownFormatter struct { + *markdownFormatter +} + +func NewTelegramMarkdownFormatter(tags []string) (f *TelegramMarkdownFormatter, err error) { + funcMap := template.FuncMap{ + "escapeMarkdown": escapeMarkdown, + "formatRFC3339": formatRFC3339, + } + mf, err := newMarkdownFormatter(tags, funcMap, [2]string(telegramMarkdownTemplates)) + if err != nil { + return nil, err + } + + return &TelegramMarkdownFormatter{markdownFormatter: mf}, nil +} + +func escapeMarkdown(v interface{}) string { + return bot.EscapeMarkdown(fmt.Sprintf("%v", v)) +} + type SimpleTextFormatter struct { tags []string } diff --git a/alert/telegram.go b/alert/telegram.go index dfd5c46..14ee90b 100644 --- a/alert/telegram.go +++ b/alert/telegram.go @@ -4,6 +4,7 @@ import ( "context" "github.com/go-telegram/bot" + "github.com/go-telegram/bot/models" "github.com/pkg/errors" ) @@ -50,8 +51,9 @@ func (tc *TelegramChannel) Send(note *Notification) error { } _, err = tc.bot.SendMessage(context.Background(), &bot.SendMessageParams{ - ChatID: tc.Config.ChatId, - Text: msg, + ChatID: tc.Config.ChatId, + Text: msg, + ParseMode: models.ParseModeMarkdown, }) return err diff --git a/alert/templates.go b/alert/templates.go new file mode 100644 index 0000000..d609f9f --- /dev/null +++ b/alert/templates.go @@ -0,0 +1,67 @@ +package alert + +var ( + dingTalkMarkdownTemplates = []string{ + `{{- /* default markdown template */ -}} +# {{.Title}} + +- **Tags**: {{.Tags}} +- **Severity**: {{.Severity}} +- **Time**: {{.Time | formatRFC3339}} + +**{{.Content}}** +`, + `{{- /* logrus entry markdown template */ -}} +# {{.Level}} + +- **Tags**: {{.Tags}} +- **Time**: {{.Time | formatRFC3339}} + +--- + +## Message +{{.Msg}} + +{{with .Error}} +--- + +## Reason +{{.Error}} +{{ end }} + +{{ if .CtxFields }} +--- + +## Context Fields + +{{ range $Key, $Val := .CtxFields }} +- **{{$Key}}**: {{$Val}} +{{ end }} +{{ end }} +`, + } + + telegramMarkdownTemplates = []string{ + `{{- /* default markdown template */ -}} +*{{.Title | escapeMarkdown}}* +*Tags*: {{.Tags | escapeMarkdown}} +*Severity*: {{.Severity | escapeMarkdown}} +*Time*: {{.Time | formatRFC3339 | escapeMarkdown}} +*{{.Content | escapeMarkdown}}* +`, + `{{- /* logrus entry markdown template */ -}} +*{{.Level | escapeMarkdown}}* +*Tags*: {{.Tags | escapeMarkdown}} +*Time*: {{.Time | formatRFC3339 | escapeMarkdown}} + +*Message* +{{.Msg | escapeMarkdown}} + +{{with .Error}}*Reason* +{{.Error | escapeMarkdown}}{{ end }} + +{{ if .CtxFields }}*Context Fields*:{{ range $Key, $Val := .CtxFields }} + *{{$Key | escapeMarkdown}}*: {{$Val | escapeMarkdown}}{{ end }}{{ end }} +`, + } +) diff --git a/alert/util.go b/alert/util.go index cd1b0e5..1154fed 100644 --- a/alert/util.go +++ b/alert/util.go @@ -13,7 +13,7 @@ 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) { +func parseAlertChannel(chID string, chmap map[string]interface{}, tags []string) (Channel, error) { cht, ok := chmap["platform"].(string) if !ok { return nil, ErrChannelTypeNotSupported(cht) @@ -26,6 +26,11 @@ func parseAlertChannel(chID string, chmap map[string]interface{}, fmt Formatter) return nil, err } + fmt, err := NewDingtalkMarkdownFormatter(tags) + if err != nil { + return nil, err + } + return NewDingTalkChannel(chID, fmt, dtconf), nil case ChannelTypeTelegram: var tgconf TelegramConfig @@ -33,6 +38,11 @@ func parseAlertChannel(chID string, chmap map[string]interface{}, fmt Formatter) return nil, err } + fmt, err := NewTelegramMarkdownFormatter(tags) + if err != nil { + return nil, err + } + return NewTelegramChannel(chID, fmt, tgconf) // NOTE: add more channel types support here if needed diff --git a/config/config.yaml b/config/config.yaml index ed7a429..46da75f 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -13,7 +13,7 @@ # 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] +# customTags: [dev] # # Channels: Notification channels # # Key: Unique identifier for the channel (e.g., channel ID) # # Value: Configuration for the channel diff --git a/log/hook/hook.go b/log/hook/hook.go index 3ddc8e9..37bbe75 100644 --- a/log/hook/hook.go +++ b/log/hook/hook.go @@ -2,8 +2,6 @@ package hook import ( stderr "errors" - "fmt" - "strings" "github.com/Conflux-Chain/go-conflux-util/alert" "github.com/pkg/errors" @@ -14,9 +12,8 @@ const ( // logrus entry field configured for alert channels chLogEntryField = "@channel" - // alert message template + // alert message title alertMsgTitle = "logrus alert notification" - alertMsgTpl = "level:\t%v;\nbrief:\t%v;\ndetail:\t%v" ) // AddAlertHook adds logrus hook for alert notification with specified log levels. @@ -58,7 +55,7 @@ func (hook *AlertHook) Fire(logEntry *logrus.Entry) (err error) { note := &alert.Notification{ Title: alertMsgTitle, - Content: hook.formatMsg(logEntry), + Content: logEntry, } for _, ch := range notifyChans { @@ -68,18 +65,6 @@ func (hook *AlertHook) Fire(logEntry *logrus.Entry) (err error) { return errors.WithMessage(err, "failed to notify channel message") } -func (hook *AlertHook) formatMsg(logEntry *logrus.Entry) string { - level := logEntry.Level.String() - brief := logEntry.Message - - formatter := &logrus.JSONFormatter{} - detailBytes, _ := formatter.Format(logEntry) - // Trim last newline char to uniform message format - detail := strings.TrimSuffix(string(detailBytes), "\n") - - return fmt.Sprintf(alertMsgTpl, level, brief, detail) -} - func (hook *AlertHook) getAlertChannels(logEntry *logrus.Entry) (chs []alert.Channel, err error) { v, ok := logEntry.Data[chLogEntryField] if !ok { // notify channel not configured, use default