Skip to content

Commit

Permalink
Use markdown for alert message for more clarity
Browse files Browse the repository at this point in the history
  • Loading branch information
wanliqun committed May 9, 2024
1 parent 1c09c88 commit b41bcd1
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 29 deletions.
13 changes: 6 additions & 7 deletions alert/alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion alert/dingtalk.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
121 changes: 121 additions & 0 deletions alert/formatter.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,137 @@
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.
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
}
Expand Down
6 changes: 4 additions & 2 deletions alert/telegram.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"github.com/pkg/errors"
)

Expand Down Expand Up @@ -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
Expand Down
67 changes: 67 additions & 0 deletions alert/templates.go
Original file line number Diff line number Diff line change
@@ -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 }}
`,
}
)
12 changes: 11 additions & 1 deletion alert/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -26,13 +26,23 @@ 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
if err := decodeChannelConfig(chmap, &tgconf); err != nil {
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
Expand Down
2 changes: 1 addition & 1 deletion config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 2 additions & 17 deletions log/hook/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package hook

import (
stderr "errors"
"fmt"
"strings"

"github.com/Conflux-Chain/go-conflux-util/alert"
"github.com/pkg/errors"
Expand All @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down

0 comments on commit b41bcd1

Please sign in to comment.