diff --git a/.gitignore b/.gitignore index aa4b780003..4b913c87e1 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ !/.travis.yml !/.promu.yml !/api/v2/openapi.yaml +!examples/webhook/dingtalk/alertmanager.yml +!examples/webhook/feishu/alertmanager.yml diff --git a/config/notifiers.go b/config/notifiers.go index 668210de30..50c2d392b5 100644 --- a/config/notifiers.go +++ b/config/notifiers.go @@ -480,6 +480,10 @@ type WebhookConfig struct { // Alerts exceeding this threshold will be truncated. Setting this to 0 // allows an unlimited number of alerts. MaxAlerts uint64 `yaml:"max_alerts" json:"max_alerts"` + + // Webhook content format + JSON string `yaml:"json,omitempty" json:"json,omitempty"` + Text string `yaml:"text,omitempty" json:"text,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface. diff --git a/examples/webhook/dingtalk/README.md b/examples/webhook/dingtalk/README.md new file mode 100644 index 0000000000..d97157481d --- /dev/null +++ b/examples/webhook/dingtalk/README.md @@ -0,0 +1,93 @@ +### Alert +```json +[ + { + "labels": { + "alertname": "Test Alert Cvvtintokg.", + "env": "Test", + "group": "Ops", + "name": "Hbcqqq dzrvxi dxvftrprop dotm.", + "severity": "critical" + }, + "annotations": { + "message": "Yqlvc qdxjqdw kmedspse adfbai rmw klqeosrv fgcronx usjp embhqt.\"\nNpctkymw mkezjf iynfo qae wdvfw fmxev.Jyhbt kcfepqp dhesgstrt vpvashto yfv wvpd gadptu hbzgjwi ctt rbg." + }, + "startsAt": "2023-05-04T06:42:15.904Z", + "endsAt": "2023-05-04T07:42:15.904Z", + "generatorURL": "https://www.github.com" + } +] +``` +### Template +```gotemplate +{{ define "dingTalk.json" }} + {{ $key_map := (`{"alertname":"Alert Name","name":"Service Name","group":"Group","service":"Service","severity":"Severity","message":"Message","instance":"Instance Name"}`|toMap) }} + {{ $alert_name := "" }} + {{ $alert_status := "" }} + {{ if eq .Status "firing" }} + {{ $alert_status = printf "[Firing: %d]" (.Alerts.Firing | len) }} + {{ else if eq .Status "resolved" }} + {{ $alert_status = printf "[Resolved: %d]" (.Alerts.Resolved | len) }} + {{ else }} + {{ $alert_status =(.Status | toUpper) }} + {{ end }} + {{ range .GroupLabels.SortedPairs }} {{ if eq .Name "alertname" }}{{ $alert_name = (.Value) }}{{end}} {{ end }} + {{ $alert_title := (printf "%s %s" $alert_status $alert_name ) }} + {{ $alert_msg := (printf "### %s %s" $alert_status $alert_name ) }} + {{ range .Alerts.Firing }} + {{ range .Labels.SortedPairs }} + {{ if (ne (index $key_map .Name) "") }} + {{ $alert_msg = (printf "%s\n\n> %s: %s" $alert_msg (index $key_map .Name) .Value ) }} + {{else}} + {{ $alert_msg = (printf "%s\n\n> %s: %s" $alert_msg .Name .Value ) }} + {{end}} + {{ end }} + {{ range .Annotations.SortedPairs }} + {{ if (ne (index $key_map .Name) "") }} + {{ $alert_msg = (printf "%s\n\n> %s: %s" $alert_msg (index $key_map .Name) .Value ) }} + {{else}} + {{ $alert_msg = (printf "%s\n\n> %s: %s" $alert_msg .Name .Value ) }} + {{end}} + {{ end }} + {{ $alert_msg = (printf "%s\n\n> [View Data](%s)" $alert_msg .GeneratorURL ) }} + {{ end }} + { + "msgtype": "markdown", + "markdown": { + "title":{{ $alert_title|toJson }}, + "text": {{ $alert_msg|toJson }} + }, + "at": { + "atMobiles": [ + ], + "isAtAll": false + } + } +{{ end }} +``` +### Alertmanager Config +```yaml +receivers: + - name: 'web.hook' + webhook_configs: + - url: 'https://oapi.dingtalk.com/robot/send?access_token=' + json: '{{ template "dingTalk.json" . }}' +``` + +### Webhook Content +``` +{ + "msgtype": "markdown", + "markdown": { + "title": "[Firing: 1] Test Alert Eijwywi wtuplkvno uix.", + "text": "### [Firing: 1] Test Alert Eijwywi wtuplkvno uix.\n\n\u003e Alert Name: Test Alert Eijwywi wtuplkvno uix.\n\n\u003e env: Test\n\n\u003e Group: Ops\n\n\u003e Service Name: Tdjafh qbhhpmyt aeekyhrhr tdjdbbap.\n\n\u003e Severity: critical\n\n\u003e Message: Bstg aenfwxmvk qbihjaxo.\"\nEoog xzrgupeeel yqyndiw appvaw wkkbre dhemrscsg egvofxxvs xqxj.Egwmievu xysgytr idwufuw.\n\n\u003e [View Data](https://www.github.com)" + }, + "at": { + "atMobiles": [], + "isAtAll": false + } +} +``` + +### Feishu message +![image](./dingTalk_message.png) \ No newline at end of file diff --git a/examples/webhook/dingtalk/alertmanager.yml b/examples/webhook/dingtalk/alertmanager.yml new file mode 100644 index 0000000000..033f89bc89 --- /dev/null +++ b/examples/webhook/dingtalk/alertmanager.yml @@ -0,0 +1,22 @@ +global: + resolve_timeout: 5m + +route: + group_by: ['alertname'] + group_wait: 10s + group_interval: 10s + repeat_interval: 1m + receiver: 'web.hook' +receivers: + - name: 'web.hook' + webhook_configs: + - url: 'https://oapi.dingtalk.com/robot/send?access_token=' + json: '{{ template "dingTalk.json" . }}' +inhibit_rules: + - source_match: + severity: 'critical' + target_match: + severity: 'warning' + equal: ['alertname', 'dev', 'instance'] +templates: + - "json.tmpl" diff --git a/examples/webhook/dingtalk/dingTalk_message.png b/examples/webhook/dingtalk/dingTalk_message.png new file mode 100644 index 0000000000..3a3e16ee56 Binary files /dev/null and b/examples/webhook/dingtalk/dingTalk_message.png differ diff --git a/examples/webhook/dingtalk/json.tmpl b/examples/webhook/dingtalk/json.tmpl new file mode 100644 index 0000000000..01a9134354 --- /dev/null +++ b/examples/webhook/dingtalk/json.tmpl @@ -0,0 +1,44 @@ +{{ define "dingTalk.json" }} + {{ $key_map := (`{"alertname":"Alert Name","name":"Service Name","group":"Group","service":"Service","severity":"Severity","message":"Message","instance":"Instance Name"}`|toMap) }} + {{ $alert_name := "" }} + {{ $alert_status := "" }} + {{ if eq .Status "firing" }} + {{ $alert_status = printf "[Firing: %d]" (.Alerts.Firing | len) }} + {{ else if eq .Status "resolved" }} + {{ $alert_status = printf "[Resolved: %d]" (.Alerts.Resolved | len) }} + {{ else }} + {{ $alert_status =(.Status | toUpper) }} + {{ end }} + {{ range .GroupLabels.SortedPairs }} {{ if eq .Name "alertname" }}{{ $alert_name = (.Value) }}{{end}} {{ end }} + {{ $alert_title := (printf "%s %s" $alert_status $alert_name ) }} + {{ $alert_msg := (printf "### %s %s" $alert_status $alert_name ) }} + {{ range .Alerts.Firing }} + {{ range .Labels.SortedPairs }} + {{ if (ne (index $key_map .Name) "") }} + {{ $alert_msg = (printf "%s\n\n> %s: %s" $alert_msg (index $key_map .Name) .Value ) }} + {{else}} + {{ $alert_msg = (printf "%s\n\n> %s: %s" $alert_msg .Name .Value ) }} + {{end}} + {{ end }} + {{ range .Annotations.SortedPairs }} + {{ if (ne (index $key_map .Name) "") }} + {{ $alert_msg = (printf "%s\n\n> %s: %s" $alert_msg (index $key_map .Name) .Value ) }} + {{else}} + {{ $alert_msg = (printf "%s\n\n> %s: %s" $alert_msg .Name .Value ) }} + {{end}} + {{ end }} + {{ $alert_msg = (printf "%s\n\n> [View Data](%s)" $alert_msg .GeneratorURL ) }} + {{ end }} + { + "msgtype": "markdown", + "markdown": { + "title":{{ $alert_title|toJson }}, + "text": {{ $alert_msg|toJson }} + }, + "at": { + "atMobiles": [ + ], + "isAtAll": false + } + } +{{ end }} diff --git a/examples/webhook/feishu/README.md b/examples/webhook/feishu/README.md new file mode 100644 index 0000000000..9a8b65fb6b --- /dev/null +++ b/examples/webhook/feishu/README.md @@ -0,0 +1,96 @@ +### Alert +```json +[ + { + "labels": { + "alertname": "Test Alert Temiydom wgatpelzn eiesvt.", + "env": "Test", + "group": "Ops", + "name": "Teft dwqokj yqka jbroocrw xwz.", + "severity": "critical" + }, + "annotations": { + "message": "Zxoiyg mnukiqqa imgxyh gwahxoe rixwdzgfn.\"\nLcxdl dpcmu gcrwhvyb qbipfn ifipq omumebq whwmupxag rnbryqneyu gih mszuxf.Tfwbmtj dxuwskbctr ostv kpmxujo tlydykp." + }, + "startsAt": "2023-05-04T03:45:54.173Z", + "endsAt": "2023-05-04T04:45:54.173Z", + "generatorURL": "https://www.github.com" + } +] +``` +### Template +```gotemplate +{{- define "feishu.title" -}} + {{- if eq .Status "firing" -}} + [Firing {{- .Alerts.Firing | len -}}/{{- .Alerts | len -}}] + {{- else if eq .Status "resolved" -}} + [Resolved {{- .Alerts.Resolved | len -}}] + {{- else -}} + [{{- .Status | toUpper | safeJson -}}] + {{- end -}} + {{" "}}{{- range .GroupLabels.SortedPairs -}} {{- if eq .Name "alertname" -}}{{- .Value | safeJson -}}{{- end -}} {{- end -}} +{{- end -}} + +{{- define "feishu.msg" -}} + ### {{ template "feishu.title" . -}}\n + {{- range $idx,$elem := .Alerts.Firing -}} + {{- range .Labels.SortedPairs -}} + **{{- .Name | safeJson -}}**:{{" "}}{{- .Value | safeJson -}}\n + {{- end -}} + {{- range .Annotations.SortedPairs -}} + **{{- .Name | safeJson -}}**:{{" "}}{{- .Value | safeJson -}}\n + {{- end -}} + **Srart Time**:{{" "}}{{- .StartsAt -}}\n + {{- if eq .Status "firing" -}} + [View data]({{.GeneratorURL | safeJson}}) + {{- end -}} + {{- end -}} +{{- end -}} +{{- define "feishu.json" -}} + { + "msg_type": "interactive", + "card": { + "elements": [ + { + "content": "{{- template "feishu.msg" . -}}", + "tag": "markdown" + } + ], + "config": { + "wide_screen_mode": true + } + } + } +{{- end -}} + +``` +### Alertmanager Config +```yaml +receivers: + - name: "web.hook" + webhook_configs: + - url: 'https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxxxxxxxxx' + json: '{{ template "feishu.json" . }}' + send_resolved: true +``` + +### Webhook Content +``` +{ + "msg_type": "interactive", + "card": { + "elements": [ + { + "content": "### [Firing1/1] Test Alert Temiydom wgatpelzn eiesvt.\n**alertname**: Test Alert Temiydom wgatpelzn eiesvt.\n**env**: Test\n**group**: Ops\n**name**: Teft dwqokj yqka jbroocrw xwz.\n**severity**: critical\n**message**: Zxoiyg mnukiqqa imgxyh gwahxoe rixwdzgfn.\"\nLcxdl dpcmu gcrwhvyb qbipfn ifipq omumebq whwmupxag rnbryqneyu gih mszuxf.Tfwbmtj dxuwskbctr ostv kpmxujo tlydykp.\n**Srart Time**: 2023-05-04 03:45:54.173 +0000 UTC\n[View data](https://www.github.com)", + "tag": "markdown" + } + ], + "config": { + "wide_screen_mode": true + } + } +} +``` + +### Feishu message +![image](./feishu_message.png) \ No newline at end of file diff --git a/examples/webhook/feishu/alertmanager.yml b/examples/webhook/feishu/alertmanager.yml new file mode 100644 index 0000000000..63c0724ba5 --- /dev/null +++ b/examples/webhook/feishu/alertmanager.yml @@ -0,0 +1,26 @@ +global: + resolve_timeout: 5m +route: + group_by: ['alertname'] + group_wait: 10s + group_interval: 10s + repeat_interval: 24h + + receiver: 'web.hook' + +receivers: + - name: 'web.hook' + webhook_configs: + - url: 'https://open.feishu.cn/open-apis/bot/v2/hook/' + json: '{{ template "feishu.json" . }}' + send_resolved: true + +inhibit_rules: + - source_match: + severity: 'critical' + target_match: + severity: 'warning' + equal: ['alertname', 'dev', 'instance'] + +templates: + - "json.tmpl" diff --git a/examples/webhook/feishu/feishu_message.png b/examples/webhook/feishu/feishu_message.png new file mode 100644 index 0000000000..552c168021 Binary files /dev/null and b/examples/webhook/feishu/feishu_message.png differ diff --git a/examples/webhook/feishu/json.tmpl b/examples/webhook/feishu/json.tmpl new file mode 100644 index 0000000000..6a233016b2 --- /dev/null +++ b/examples/webhook/feishu/json.tmpl @@ -0,0 +1,42 @@ +{{- define "feishu.title" -}} + {{- if eq .Status "firing" -}} + [Firing {{- .Alerts.Firing | len -}}/{{- .Alerts | len -}}] + {{- else if eq .Status "resolved" -}} + [Resolved {{- .Alerts.Resolved | len -}}] + {{- else -}} + [{{- .Status | toUpper | safeJson -}}] + {{- end -}} + {{" "}}{{- range .GroupLabels.SortedPairs -}} {{- if eq .Name "alertname" -}}{{- .Value | safeJson -}}{{- end -}} {{- end -}} +{{- end -}} + +{{- define "feishu.msg" -}} + ### {{ template "feishu.title" . -}}\n + {{- range $idx,$elem := .Alerts.Firing -}} + {{- range .Labels.SortedPairs -}} + **{{- .Name | safeJson -}}**:{{" "}}{{- .Value | safeJson -}}\n + {{- end -}} + {{- range .Annotations.SortedPairs -}} + **{{- .Name | safeJson -}}**:{{" "}}{{- .Value | safeJson -}}\n + {{- end -}} + **Srart Time**:{{" "}}{{- .StartsAt -}}\n + {{- if eq .Status "firing" -}} + [View data]({{.GeneratorURL | safeJson}}) + {{- end -}} + {{- end -}} +{{- end -}} +{{- define "feishu.json" -}} + { + "msg_type": "interactive", + "card": { + "elements": [ + { + "content": "{{- template "feishu.msg" . -}}", + "tag": "markdown" + } + ], + "config": { + "wide_screen_mode": true + } + } + } +{{- end -}} diff --git a/notify/webhook/webhook.go b/notify/webhook/webhook.go index a1f8c37dbf..ac04893aae 100644 --- a/notify/webhook/webhook.go +++ b/notify/webhook/webhook.go @@ -98,8 +98,27 @@ func (n *Notifier) Notify(ctx context.Context, alerts ...*types.Alert) (bool, er } var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(msg); err != nil { - return false, err + if n.conf.JSON != "" { + body, err := n.tmpl.ExecuteTextString(n.conf.JSON, data) + if err != nil { + return false, err + } + if !json.Valid([]byte(body)) { + return false, fmt.Errorf("json format error: %s ", body) + } + if err := json.Indent(&buf, []byte(body), "", " "); err != nil { + return false, fmt.Errorf("json format error: %s ", body) + } + } else if n.conf.Text != "" { + if body, err := n.tmpl.ExecuteTextString(n.conf.Text, data); err != nil { + return false, err + } else if _, err = buf.WriteString(body); err != nil { + return false, err + } + } else { + if err := json.NewEncoder(&buf).Encode(msg); err != nil { + return false, err + } } var url string diff --git a/template/json.tmpl b/template/json.tmpl new file mode 100644 index 0000000000..6a233016b2 --- /dev/null +++ b/template/json.tmpl @@ -0,0 +1,42 @@ +{{- define "feishu.title" -}} + {{- if eq .Status "firing" -}} + [Firing {{- .Alerts.Firing | len -}}/{{- .Alerts | len -}}] + {{- else if eq .Status "resolved" -}} + [Resolved {{- .Alerts.Resolved | len -}}] + {{- else -}} + [{{- .Status | toUpper | safeJson -}}] + {{- end -}} + {{" "}}{{- range .GroupLabels.SortedPairs -}} {{- if eq .Name "alertname" -}}{{- .Value | safeJson -}}{{- end -}} {{- end -}} +{{- end -}} + +{{- define "feishu.msg" -}} + ### {{ template "feishu.title" . -}}\n + {{- range $idx,$elem := .Alerts.Firing -}} + {{- range .Labels.SortedPairs -}} + **{{- .Name | safeJson -}}**:{{" "}}{{- .Value | safeJson -}}\n + {{- end -}} + {{- range .Annotations.SortedPairs -}} + **{{- .Name | safeJson -}}**:{{" "}}{{- .Value | safeJson -}}\n + {{- end -}} + **Srart Time**:{{" "}}{{- .StartsAt -}}\n + {{- if eq .Status "firing" -}} + [View data]({{.GeneratorURL | safeJson}}) + {{- end -}} + {{- end -}} +{{- end -}} +{{- define "feishu.json" -}} + { + "msg_type": "interactive", + "card": { + "elements": [ + { + "content": "{{- template "feishu.msg" . -}}", + "tag": "markdown" + } + ], + "config": { + "wide_screen_mode": true + } + } + } +{{- end -}} diff --git a/template/template.go b/template/template.go index 5172ba92a7..185dfb6f19 100644 --- a/template/template.go +++ b/template/template.go @@ -15,6 +15,7 @@ package template import ( "bytes" + "encoding/json" tmplhtml "html/template" "io" "net/url" @@ -192,6 +193,22 @@ var DefaultFuncs = FuncMap{ "stringSlice": func(s ...string) []string { return s }, + "toMap": func(s string) map[string]string { + m := make(map[string]string) + if err := json.Unmarshal([]byte(s), &m); err != nil { + panic(err) + } else { + return m + } + }, + "toJson": func(o interface{}) string { + data, _ := json.Marshal(o) + return string(data) + }, + "safeJson": func(o interface{}) string { + data, _ := json.Marshal(o) + return strings.Trim(string(data), "\"") + }, } // Pair is a key/value string pair. diff --git a/template/template_test.go b/template/template_test.go index 97d36fb173..d8fea4a09c 100644 --- a/template/template_test.go +++ b/template/template_test.go @@ -506,6 +506,18 @@ func TestTemplateFuncs(t *testing.T) { title: "Template using reReplaceAll", in: `{{ reReplaceAll "ab" "AB" "abc" }}`, exp: "ABc", + }, { + title: "Template using toMap", + in: `{{ toMap "{}" }}`, + exp: "map[]", + }, { + title: "Template using toJson", + in: "{\"content\": {{ toJson `ab\n\"c` }}}", + exp: `{"content": "ab\n\"c"}`, + }, { + title: "Template using safeJson", + in: "{\"content\": \"{{ safeJson `ab\n\"c` }}\"}", + exp: `{"content": "ab\n\"c"}`, }} { tc := tc t.Run(tc.title, func(t *testing.T) {