mirror of
https://github.com/ccfos/nightingale.git
synced 2026-03-02 22:19:10 +00:00
Compare commits
2 Commits
v8.2.2
...
lark-notif
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a15d25ddc | ||
|
|
4a7a5a624a |
@@ -100,6 +100,8 @@ func (e *Dispatch) relaodTpls() error {
|
||||
models.Mm: sender.NewSender(models.Mm, tmpTpls),
|
||||
models.Telegram: sender.NewSender(models.Telegram, tmpTpls),
|
||||
models.FeishuCard: sender.NewSender(models.FeishuCard, tmpTpls),
|
||||
models.Lark: sender.NewSender(models.Lark, tmpTpls),
|
||||
models.LarkCard: sender.NewSender(models.LarkCard, tmpTpls),
|
||||
}
|
||||
|
||||
// domain -> Callback()
|
||||
@@ -110,7 +112,9 @@ func (e *Dispatch) relaodTpls() error {
|
||||
models.TelegramDomain: sender.NewCallBacker(models.TelegramDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
|
||||
models.FeishuCardDomain: sender.NewCallBacker(models.FeishuCardDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
|
||||
models.IbexDomain: sender.NewCallBacker(models.IbexDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
|
||||
models.LarkDomain: sender.NewCallBacker(models.LarkDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
|
||||
models.DefaultDomain: sender.NewCallBacker(models.DefaultDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
|
||||
models.LarkCardDomain: sender.NewCallBacker(models.LarkCardDomain, e.targetCache, e.userCache, e.taskTplsCache, tmpTpls),
|
||||
}
|
||||
|
||||
e.RwLock.RLock()
|
||||
@@ -299,6 +303,12 @@ func (e *Dispatch) SendCallbacks(rule *models.AlertRule, notifyTarget *NotifyTar
|
||||
continue
|
||||
}
|
||||
|
||||
// process lark card
|
||||
if parsedURL.Host == models.LarkDomain && parsedURL.Query().Get("card") == "1" {
|
||||
e.CallBacks[models.LarkCardDomain].CallBack(cbCtx)
|
||||
continue
|
||||
}
|
||||
|
||||
callBacker, ok := e.CallBacks[parsedURL.Host]
|
||||
if ok {
|
||||
callBacker.CallBack(cbCtx)
|
||||
|
||||
@@ -96,6 +96,10 @@ func NewCallBacker(
|
||||
// return &MmSender{tpl: tpls[models.Mm]}
|
||||
case models.TelegramDomain:
|
||||
return &TelegramSender{tpl: tpls[models.Telegram]}
|
||||
case models.LarkDomain:
|
||||
return &LarkSender{tpl: tpls[models.Lark]}
|
||||
case models.LarkCardDomain:
|
||||
return &LarkCardSender{tpl: tpls[models.LarkCard]}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
64
alert/sender/lark.go
Normal file
64
alert/sender/lark.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
)
|
||||
|
||||
var (
|
||||
_ CallBacker = (*LarkSender)(nil)
|
||||
)
|
||||
|
||||
type LarkSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func (lk *LarkSender) CallBack(ctx CallBackContext) {
|
||||
if len(ctx.Events) == 0 || len(ctx.CallBackURL) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
body := feishu{
|
||||
Msgtype: "text",
|
||||
Content: feishuContent{
|
||||
Text: BuildTplMessage(models.Lark, lk.tpl, ctx.Events),
|
||||
},
|
||||
}
|
||||
|
||||
doSend(ctx.CallBackURL, body, models.Lark, ctx.Stats)
|
||||
ctx.Stats.AlertNotifyTotal.WithLabelValues("rule_callback").Inc()
|
||||
}
|
||||
|
||||
func (lk *LarkSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
urls := lk.extract(ctx.Users)
|
||||
message := BuildTplMessage(models.Lark, lk.tpl, ctx.Events)
|
||||
for _, url := range urls {
|
||||
body := feishu{
|
||||
Msgtype: "text",
|
||||
Content: feishuContent{
|
||||
Text: message,
|
||||
},
|
||||
}
|
||||
doSend(url, body, models.Lark, ctx.Stats)
|
||||
}
|
||||
}
|
||||
|
||||
func (lk *LarkSender) extract(users []*models.User) []string {
|
||||
urls := make([]string, 0, len(users))
|
||||
|
||||
for _, user := range users {
|
||||
if token, has := user.ExtractToken(models.Lark); has {
|
||||
url := token
|
||||
if !strings.HasPrefix(token, "https://") && !strings.HasPrefix(token, "http://") {
|
||||
url = "https://open.larksuite.com/open-apis/bot/v2/hook/" + token
|
||||
}
|
||||
urls = append(urls, url)
|
||||
}
|
||||
}
|
||||
return urls
|
||||
}
|
||||
98
alert/sender/larkcard.go
Normal file
98
alert/sender/larkcard.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/ccfos/nightingale/v6/models"
|
||||
)
|
||||
|
||||
type LarkCardSender struct {
|
||||
tpl *template.Template
|
||||
}
|
||||
|
||||
func (fs *LarkCardSender) CallBack(ctx CallBackContext) {
|
||||
if len(ctx.Events) == 0 || len(ctx.CallBackURL) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ats := ExtractAtsParams(ctx.CallBackURL)
|
||||
message := BuildTplMessage(models.LarkCard, fs.tpl, ctx.Events)
|
||||
|
||||
if len(ats) > 0 {
|
||||
atTags := ""
|
||||
for _, at := range ats {
|
||||
if strings.Contains(at, "@") {
|
||||
atTags += fmt.Sprintf("<at email=\"%s\" ></at>", at)
|
||||
} else {
|
||||
atTags += fmt.Sprintf("<at id=\"%s\" ></at>", at)
|
||||
}
|
||||
}
|
||||
message = atTags + message
|
||||
}
|
||||
|
||||
color := "red"
|
||||
lowerUnicode := strings.ToLower(message)
|
||||
if strings.Count(lowerUnicode, Recovered) > 0 && strings.Count(lowerUnicode, Triggered) > 0 {
|
||||
color = "orange"
|
||||
} else if strings.Count(lowerUnicode, Recovered) > 0 {
|
||||
color = "green"
|
||||
}
|
||||
|
||||
SendTitle := fmt.Sprintf("🔔 %s", ctx.Events[0].RuleName)
|
||||
body.Card.Header.Title.Content = SendTitle
|
||||
body.Card.Header.Template = color
|
||||
body.Card.Elements[0].Text.Content = message
|
||||
body.Card.Elements[2].Elements[0].Content = SendTitle
|
||||
|
||||
// This is to be compatible with the Larkcard interface, if with query string parameters, the request will fail
|
||||
// Remove query parameters from the URL,
|
||||
parsedURL, err := url.Parse(ctx.CallBackURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
parsedURL.RawQuery = ""
|
||||
|
||||
doSend(parsedURL.String(), body, models.LarkCard, ctx.Stats)
|
||||
}
|
||||
|
||||
func (fs *LarkCardSender) Send(ctx MessageContext) {
|
||||
if len(ctx.Users) == 0 || len(ctx.Events) == 0 {
|
||||
return
|
||||
}
|
||||
urls, _ := fs.extract(ctx.Users)
|
||||
message := BuildTplMessage(models.LarkCard, fs.tpl, ctx.Events)
|
||||
color := "red"
|
||||
lowerUnicode := strings.ToLower(message)
|
||||
if strings.Count(lowerUnicode, Recovered) > 0 && strings.Count(lowerUnicode, Triggered) > 0 {
|
||||
color = "orange"
|
||||
} else if strings.Count(lowerUnicode, Recovered) > 0 {
|
||||
color = "green"
|
||||
}
|
||||
|
||||
SendTitle := fmt.Sprintf("🔔 %s", ctx.Events[0].RuleName)
|
||||
body.Card.Header.Title.Content = SendTitle
|
||||
body.Card.Header.Template = color
|
||||
body.Card.Elements[0].Text.Content = message
|
||||
body.Card.Elements[2].Elements[0].Content = SendTitle
|
||||
for _, url := range urls {
|
||||
doSend(url, body, models.LarkCard, ctx.Stats)
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *LarkCardSender) extract(users []*models.User) ([]string, []string) {
|
||||
urls := make([]string, 0, len(users))
|
||||
ats := make([]string, 0)
|
||||
for i := range users {
|
||||
if token, has := users[i].ExtractToken(models.Lark); has {
|
||||
url := token
|
||||
if !strings.HasPrefix(token, "https://") && !strings.HasPrefix(token, "http://") {
|
||||
url = "https://open.larksuite.com/open-apis/bot/v2/hook/" + strings.TrimSpace(token)
|
||||
}
|
||||
urls = append(urls, url)
|
||||
}
|
||||
}
|
||||
return urls, ats
|
||||
}
|
||||
@@ -41,6 +41,10 @@ func NewSender(key string, tpls map[string]*template.Template, smtp ...aconf.SMT
|
||||
return &MmSender{tpl: tpls[models.Mm]}
|
||||
case models.Telegram:
|
||||
return &TelegramSender{tpl: tpls[models.Telegram]}
|
||||
case models.Lark:
|
||||
return &LarkSender{tpl: tpls[models.Lark]}
|
||||
case models.LarkCard:
|
||||
return &LarkCardSender{tpl: tpls[models.LarkCard]}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -90,7 +90,8 @@ func (rt *Router) notifyChannelPuts(c *gin.Context) {
|
||||
var notifyChannels []models.NotifyChannel
|
||||
ginx.BindJSON(c, ¬ifyChannels)
|
||||
|
||||
channels := []string{models.Dingtalk, models.Wecom, models.Feishu, models.Mm, models.Telegram, models.Email}
|
||||
channels := []string{models.Dingtalk, models.Wecom, models.Feishu, models.Mm, models.Telegram,
|
||||
models.Email, models.Lark, models.LarkCard}
|
||||
|
||||
m := make(map[string]struct{})
|
||||
for _, v := range notifyChannels {
|
||||
@@ -126,7 +127,8 @@ func (rt *Router) notifyContactPuts(c *gin.Context) {
|
||||
var notifyContacts []models.NotifyContact
|
||||
ginx.BindJSON(c, ¬ifyContacts)
|
||||
|
||||
keys := []string{models.DingtalkKey, models.WecomKey, models.FeishuKey, models.MmKey, models.TelegramKey}
|
||||
keys := []string{models.DingtalkKey, models.WecomKey, models.FeishuKey, models.MmKey,
|
||||
models.TelegramKey, models.LarkKey}
|
||||
|
||||
m := make(map[string]struct{})
|
||||
for _, v := range notifyContacts {
|
||||
|
||||
@@ -171,8 +171,7 @@ func InitNotifyConfig(c *ctx.Context, tplDir string) {
|
||||
|
||||
if cval == "" {
|
||||
var notifyContacts []NotifyContact
|
||||
contacts := []string{DingtalkKey, WecomKey, FeishuKey, MmKey, TelegramKey}
|
||||
for _, contact := range contacts {
|
||||
for _, contact := range DefaultContacts {
|
||||
notifyContacts = append(notifyContacts, NotifyContact{Ident: contact, Name: contact, BuiltIn: true})
|
||||
}
|
||||
|
||||
@@ -182,6 +181,35 @@ func InitNotifyConfig(c *ctx.Context, tplDir string) {
|
||||
logger.Errorf("failed to set notify contact config: %v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
var contacts []NotifyContact
|
||||
if err = json.Unmarshal([]byte(cval), &contacts); err != nil {
|
||||
logger.Errorf("failed to unmarshal notify channel config: %v", err)
|
||||
return
|
||||
}
|
||||
contactMap := make(map[string]struct{})
|
||||
for _, contact := range contacts {
|
||||
contactMap[contact.Ident] = struct{}{}
|
||||
}
|
||||
|
||||
var newContacts []NotifyContact
|
||||
for _, contact := range DefaultContacts {
|
||||
if _, ok := contactMap[contact]; !ok {
|
||||
newContacts = append(newContacts, NotifyContact{Ident: contact, Name: contact, BuiltIn: true})
|
||||
}
|
||||
}
|
||||
if len(newContacts) > 0 {
|
||||
contacts = append(contacts, newContacts...)
|
||||
data, err := json.Marshal(contacts)
|
||||
if err != nil {
|
||||
logger.Errorf("failed to marshal contacts: %v", err)
|
||||
return
|
||||
}
|
||||
if err = ConfigsSet(c, NOTIFYCONTACT, string(data)); err != nil {
|
||||
logger.Errorf("failed to set notify contact config: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// init notify tpl
|
||||
@@ -254,7 +282,8 @@ var TplMap = map[string]string{
|
||||
- {{$key}}: {{$val}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
`,
|
||||
{{$domain := "http://请联系管理员修改通知模板将域名替换为实际的域名" }}
|
||||
[事件详情]({{$domain}}/alert-his-events/{{.Id}})|[屏蔽1小时]({{$domain}}/alert-mutes/add?busiGroup={{.GroupId}}&cate={{.Cate}}&datasource_ids={{.DatasourceId}}&prod={{.RuleProd}}{{range $key, $value := .TagsMap}}&tags={{$key}}%3D{{$value}}{{end}})|[查看曲线]({{$domain}}/metric/explorer?data_source_id={{.DatasourceId}}&data_source_name=prometheus&mode=graph&prom_ql={{.PromQl}})`,
|
||||
Email: `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -478,7 +507,10 @@ var TplMap = map[string]string{
|
||||
监控指标: {{.TagsJSON}}
|
||||
{{if .IsRecovered}}恢复时间:{{timeformat .LastEvalTime}}{{else}}触发时间: {{timeformat .TriggerTime}}
|
||||
触发时值: {{.TriggerValue}}{{end}}
|
||||
发送时间: {{timestamp}}`,
|
||||
发送时间: {{timestamp}}
|
||||
{{$domain := "http://请联系管理员修改通知模板将域名替换为实际的域名" }}
|
||||
事件详情: {{$domain}}/alert-his-events/{{.Id}}
|
||||
屏蔽1小时: {{$domain}}/alert-mutes/add?busiGroup={{.GroupId}}&cate={{.Cate}}&datasource_ids={{.DatasourceId}}&prod={{.RuleProd}}{{range $key, $value := .TagsMap}}&tags={{$key}}%3D{{$value}}{{end}}`,
|
||||
FeishuCard: `{{ if .IsRecovered }}
|
||||
{{- if ne .Cate "host"}}
|
||||
**告警集群:** {{.Cluster}}{{end}}
|
||||
@@ -495,7 +527,9 @@ var TplMap = map[string]string{
|
||||
**发送时间:** {{timestamp}}
|
||||
**触发时值:** {{.TriggerValue}}
|
||||
{{if .RuleNote }}**告警描述:** **{{.RuleNote}}**{{end}}
|
||||
{{- end -}}`,
|
||||
{{- end -}}
|
||||
{{$domain := "http://请联系管理员修改通知模板将域名替换为实际的域名" }}
|
||||
[事件详情]({{$domain}}/alert-his-events/{{.Id}})|[屏蔽1小时]({{$domain}}/alert-mutes/add?busiGroup={{.GroupId}}&cate={{.Cate}}&datasource_ids={{.DatasourceId}}&prod={{.RuleProd}}{{range $key, $value := .TagsMap}}&tags={{$key}}%3D{{$value}}{{end}})|[查看曲线]({{$domain}}/metric/explorer?data_source_id={{.DatasourceId}}&data_source_name=prometheus&mode=graph&prom_ql={{.PromQl}})`,
|
||||
EmailSubject: `{{if .IsRecovered}}Recovered{{else}}Triggered{{end}}: {{.RuleName}} {{.TagsJSON}}`,
|
||||
Mm: `级别状态: S{{.Severity}} {{if .IsRecovered}}Recovered{{else}}Triggered{{end}}
|
||||
规则名称: {{.RuleName}}{{if .RuleNote}}
|
||||
@@ -521,5 +555,38 @@ var TplMap = map[string]string{
|
||||
**触发时值**: {{.TriggerValue}}{{end}}
|
||||
{{if .IsRecovered}}**恢复时间**: {{timeformat .LastEvalTime}}{{else}}**首次触发时间**: {{timeformat .FirstTriggerTime}}{{end}}
|
||||
{{$time_duration := sub now.Unix .FirstTriggerTime }}{{if .IsRecovered}}{{$time_duration = sub .LastEvalTime .FirstTriggerTime }}{{end}}**距离首次告警**: {{humanizeDurationInterface $time_duration}}
|
||||
**发送时间**: {{timestamp}}`,
|
||||
**发送时间**: {{timestamp}}
|
||||
{{$domain := "http://请联系管理员修改通知模板将域名替换为实际的域名" }}
|
||||
[事件详情]({{$domain}}/alert-his-events/{{.Id}})|[屏蔽1小时]({{$domain}}/alert-mutes/add?busiGroup={{.GroupId}}&cate={{.Cate}}&datasource_ids={{.DatasourceId}}&prod={{.RuleProd}}{{range $key, $value := .TagsMap}}&tags={{$key}}%3D{{$value}}{{end}})|[查看曲线]({{$domain}}/metric/explorer?data_source_id={{.DatasourceId}}&data_source_name=prometheus&mode=graph&prom_ql={{.PromQl}})`,
|
||||
Lark: `级别状态: S{{.Severity}} {{if .IsRecovered}}Recovered{{else}}Triggered{{end}}
|
||||
规则名称: {{.RuleName}}{{if .RuleNote}}
|
||||
规则备注: {{.RuleNote}}{{end}}
|
||||
监控指标: {{.TagsJSON}}
|
||||
{{if .IsRecovered}}恢复时间:{{timeformat .LastEvalTime}}{{else}}触发时间: {{timeformat .TriggerTime}}
|
||||
触发时值: {{.TriggerValue}}{{end}}
|
||||
发送时间: {{timestamp}}
|
||||
{{$domain := "http://请联系管理员修改通知模板将域名替换为实际的域名" }}
|
||||
事件详情: {{$domain}}/alert-his-events/{{.Id}}
|
||||
屏蔽1小时: {{$domain}}/alert-mutes/add?busiGroup={{.GroupId}}&cate={{.Cate}}&datasource_ids={{.DatasourceId}}&prod={{.RuleProd}}{{range $key, $value := .TagsMap}}&tags={{$key}}%3D{{$value}}{{end}}`,
|
||||
LarkCard: `{{ if .IsRecovered }}
|
||||
{{- if ne .Cate "host"}}
|
||||
**告警集群:** {{.Cluster}}{{end}}
|
||||
**级别状态:** S{{.Severity}} Recovered
|
||||
**告警名称:** {{.RuleName}}
|
||||
**恢复时间:** {{timeformat .LastEvalTime}}
|
||||
{{$time_duration := sub now.Unix .FirstTriggerTime }}{{if .IsRecovered}}{{$time_duration = sub .LastEvalTime .FirstTriggerTime }}{{end}}**持续时长**: {{humanizeDurationInterface $time_duration}}
|
||||
**告警描述:** **服务已恢复**
|
||||
{{- else }}
|
||||
{{- if ne .Cate "host"}}
|
||||
**告警集群:** {{.Cluster}}{{end}}
|
||||
**级别状态:** S{{.Severity}} Triggered
|
||||
**告警名称:** {{.RuleName}}
|
||||
**触发时间:** {{timeformat .TriggerTime}}
|
||||
**发送时间:** {{timestamp}}
|
||||
**触发时值:** {{.TriggerValue}}
|
||||
{{$time_duration := sub now.Unix .FirstTriggerTime }}{{if .IsRecovered}}{{$time_duration = sub .LastEvalTime .FirstTriggerTime }}{{end}}**持续时长**: {{humanizeDurationInterface $time_duration}}
|
||||
{{if .RuleNote }}**告警描述:** **{{.RuleNote}}**{{end}}
|
||||
{{- end -}}
|
||||
{{$domain := "http://请联系管理员修改通知模板将域名替换为实际的域名" }}
|
||||
[事件详情]({{$domain}}/alert-his-events/{{.Id}})|[屏蔽1小时]({{$domain}}/alert-mutes/add?busiGroup={{.GroupId}}&cate={{.Cate}}&datasource_ids={{.DatasourceId}}&prod={{.RuleProd}}{{range $key, $value := .TagsMap}}&tags={{$key}}%3D{{$value}}{{end}})|[查看曲线]({{$domain}}/metric/explorer?data_source_id={{.DatasourceId}}&data_source_name=prometheus&mode=graph&prom_ql={{.PromQl}})`,
|
||||
}
|
||||
|
||||
@@ -30,26 +30,32 @@ const (
|
||||
Telegram = "telegram"
|
||||
Email = "email"
|
||||
EmailSubject = "mailsubject"
|
||||
Lark = "lark"
|
||||
LarkCard = "larkcard"
|
||||
|
||||
DingtalkKey = "dingtalk_robot_token"
|
||||
WecomKey = "wecom_robot_token"
|
||||
FeishuKey = "feishu_robot_token"
|
||||
MmKey = "mm_webhook_url"
|
||||
TelegramKey = "telegram_robot_token"
|
||||
LarkKey = "lark_robot_token"
|
||||
|
||||
DingtalkDomain = "oapi.dingtalk.com"
|
||||
WecomDomain = "qyapi.weixin.qq.com"
|
||||
FeishuDomain = "open.feishu.cn"
|
||||
LarkDomain = "open.larksuite.com"
|
||||
|
||||
// FeishuCardDomain The domain name of the feishu card is the same as the feishu,distinguished by the parameter
|
||||
FeishuCardDomain = "open.feishu.cn?card=1"
|
||||
LarkCardDomain = "open.larksuite.com?card=1"
|
||||
TelegramDomain = "api.telegram.org"
|
||||
IbexDomain = "ibex"
|
||||
DefaultDomain = "default"
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultChannels = []string{Dingtalk, Wecom, Feishu, Mm, Telegram, Email, FeishuCard}
|
||||
DefaultChannels = []string{Dingtalk, Wecom, Feishu, Mm, Telegram, Email, FeishuCard, Lark, LarkCard}
|
||||
DefaultContacts = []string{DingtalkKey, WecomKey, FeishuKey, MmKey, TelegramKey, LarkKey}
|
||||
)
|
||||
|
||||
type User struct {
|
||||
@@ -825,6 +831,9 @@ func (u *User) ExtractToken(key string) (string, bool) {
|
||||
return ret.String(), ret.Exists()
|
||||
case Email:
|
||||
return u.Email, u.Email != ""
|
||||
case Lark, LarkCard:
|
||||
ret := gjson.GetBytes(bs, LarkKey)
|
||||
return ret.String(), ret.Exists()
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user