集成钉钉:基于Webhook的扩展
在某些情况下除了Alertmanager已经内置的集中告警通知方式以外,对于不同的用户和组织而言还需要一些自定义的告知方式支持。通过Alertmanager提供的webhook支持可以轻松实现这一类的扩展。除了用于支持额外的通知方式,webhook还可以与其他第三方系统集成实现运维自动化,或者弹性伸缩等。
在Alertmanager中可以使用如下配置定义基于webhook的告警接收器receiver。一个receiver可以对应一组webhook配置。
1
name: <string>
2
webhook_configs:
3
[ - <webhook_config>, ... ]
Copied!
每一项webhook_config的具体配置格式如下:
1
# Whether or not to notify about resolved alerts.
2
[ send_resolved: <boolean> | default = true ]
3
4
# The endpoint to send HTTP POST requests to.
5
url: <string>
6
7
# The HTTP client's configuration.
8
[ http_config: <http_config> | default = global.http_config ]
Copied!
send_resolved用于指定是否在告警消除时发送回执消息。url则是用于接收webhook请求的地址。http_configs则是在需要对请求进行SSL配置时使用。
当用户定义webhook用于接收告警信息后,当告警被触发时,Alertmanager会按照以下格式向这些url地址发送HTTP Post请求,请求内容如下:
1
{
2
"version": "4",
3
"groupKey": <string>, // key identifying the group of alerts (e.g. to deduplicate)
4
"status": "<resolved|firing>",
5
"receiver": <string>,
6
"groupLabels": <object>,
7
"commonLabels": <object>,
8
"commonAnnotations": <object>,
9
"externalURL": <string>, // backlink to the Alertmanager.
10
"alerts": [
11
{
12
"labels": <object>,
13
"annotations": <object>,
14
"startsAt": "<rfc3339>",
15
"endsAt": "<rfc3339>"
16
}
17
]
18
}
Copied!

使用Golang创建webhook服务

首先我们尝试使用Golang创建用于接收webhook告警通知的服务。首先创建model包,用于映射ALertmanager发送的告警信息,Alertmanager的一个通知中根据配置的group_by规则可能会包含多条告警信息Alert。创建告警通知对应的结构体Notification。
1
package model
2
3
import "time"
4
5
type Alert struct {
6
Labels map[string]string `json:"labels"`
7
Annotations map[string]string `json:annotations`
8
StartsAt time.Time `json:"startsAt"`
9
EndsAt time.Time `json:"endsAt"`
10
}
11
12
type Notification struct {
13
Version string `json:"version"`
14
GroupKey string `json:"groupKey"`
15
Status string `json:"status"`
16
Receiver string `json:receiver`
17
GroupLabels map[string]string `json:groupLabels`
18
CommonLabels map[string]string `json:commonLabels`
19
CommonAnnotations map[string]string `json:commonAnnotations`
20
ExternalURL string `json:externalURL`
21
Alerts []Alert `json:alerts`
22
}
Copied!
这里使用gin-gonic框架创建用于接收Webhook通知的Web服务。定义路由/webhook接收来自Alertmanager的POST请求。
1
package main
2
3
import (
4
"net/http"
5
6
"github.com/gin-gonic/gin"
7
model "github.com/yunlzheng/alertmanaer-dingtalk-webhook/model"
8
)
9
10
func main() {
11
router := gin.Default()
12
router.POST("/webhook", func(c *gin.Context) {
13
var notification model.Notification
14
15
err := c.BindJSON(&notification)
16
17
if err != nil {
18
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
19
return
20
}
21
22
c.JSON(http.StatusOK, gin.H{"message": " successful receive alert notification message!"})
23
24
})
25
router.Run()
26
}
Copied!

与钉钉集成

钉钉,阿里巴巴出品,专为中国企业打造的免费智能移动办公平台,提供了即时通讯以及移动办公等丰富的功能。
钉钉群机器人是钉钉群的高级扩展功能。群机器人可以将第三方服务的信息聚合到群聊中,实现自动化的信息同步。例如:通过聚合GitHub,GitLab等源码管理服务,实现源码更新同步;通过聚合Trello,JIRA等项目协调服务,实现项目信息同步。不仅如此,群机器人支持Webhook协议的自定义接入,支持更多可能性。这里我们将演示如果将Alertmanager运维报警提醒通过自定义机器人聚合到钉钉群。
这里将继续扩展webhook服务,以支持将Alertmanager的告警通知转发到钉钉平台。完整的示例代码可以从github仓库https://github.com/yunlzheng/alertmanaer-dingtalk-webhook中获取。

自定义webhook群机器人

通过钉钉客户端(如:桌面或者手机)进入到群设置后选择“群机器人”。将显示如下界面:
群机器人
选择“自定义机器人”,并且按照提示填写机器人名称,获取机器人webhook地址,如下所示:
获取webhook地址
webhook机器人创建成功后,用户就可以使用任何方式向该地址发起HTTP POST请求,即可实现向该群主发送消息。目前自定义机器人支持文本(text),连接(link),markdown三种消息类型。
例如,可以向webhook地址以POST形式发送以下
1
{
2
"msgtype": "markdown",
3
"markdown": {
4
"title":"Prometheus告警信息",
5
"text": "#### 监控指标\n" +
6
"> 监控描述信息\n\n" +
7
"> ###### 告警时间 \n"
8
},
9
"at": {
10
"atMobiles": [
11
"156xxxx8827",
12
"189xxxx8325"
13
],
14
"isAtAll": false
15
}
16
}
Copied!
可以使用curl验证钉钉webhook是否能够成功调用:
1
$ curl -l -H "Content-type: application/json" -X POST -d '{"msgtype": "markdown","markdown": {"title":"Prometheus告警信息","text": "#### 监控指标\n> 监控描述信息\n\n> ###### 告警时间 \n"},"at": {"isAtAll": false}}' https://oapi.dingtalk.com/robot/send?access_token=xxxx
2
{"errcode":0,"errmsg":"ok"}
Copied!
调用成功后,可以在钉钉应用群消息中接收到类似于如下通知消息:
测试消息

定义转换器将告警通知转化为Dingtalk消息对象

这里定义结构体DingTalkMarkdown用于映射Dingtalk的消息体。
1
package model
2
3
type At struct {
4
AtMobiles []string `json:"atMobiles"`
5
IsAtAll bool `json:"isAtAll"`
6
}
7
8
type DingTalkMarkdown struct {
9
MsgType string `json:"msgtype"`
10
At *At `json:at`
11
Markdown *Markdown `json:"markdown"`
12
}
13
14
type Markdown struct {
15
Title string `json:"title"`
16
Text string `json:"text"`
17
}
Copied!
定义转换器将Alertmanager发送的告警通知转换为Dingtalk的消息体。
1
package transformer
2
3
import (
4
"bytes"
5
"fmt"
6
7
"github.com/yunlzheng/alertmanaer-dingtalk-webhook/model"
8
)
9
10
// TransformToMarkdown transform alertmanager notification to dingtalk markdow message
11
func TransformToMarkdown(notification model.Notification) (markdown *model.DingTalkMarkdown, err error) {
12
13
groupKey := notification.GroupKey
14
status := notification.Status
15
16
annotations := notification.CommonAnnotations
17
18
var buffer bytes.Buffer
19
20
buffer.WriteString(fmt.Sprintf("### 通知组%s(当前状态:%s) \n", groupKey, status))
21
22
buffer.WriteString(fmt.Sprintf("#### 告警项:\n"))
23
24
for _, alert := range notification.Alerts {
25
annotations := alert.Annotations
26
buffer.WriteString(fmt.Sprintf("##### %s\n > %s\n", annotations["summary"], annotations["description"]))
27
buffer.WriteString(fmt.Sprintf("\n> 开始时间:%s\n", alert.StartsAt.Format("15:04:05")))
28
}
29
30
markdown = &model.DingTalkMarkdown{
31
MsgType: "markdown",
32
Markdown: &model.Markdown{
33
Title: fmt.Sprintf("通知组:%s(当前状态:%s)", groupKey, status),
34
Text: buffer.String(),
35
},
36
At: &model.At{
37
IsAtAll: false,
38
},
39
}
40
41
return
42
}
Copied!

创建Dingtalk通知发送包

notifier包中使用golang的net/http包实现与Dingtalk群机器人的交互。Send方法包含两个参数:接收到的告警通知结构体指针,以及Dingtalk群机器人的Webhook地址。
通过包transformer.TransformToMarkdown将Alertmanager告警通知与Dingtalk消息进行映射。
1
package notifier
2
3
import (
4
"bytes"
5
"encoding/json"
6
"fmt"
7
"net/http"
8
9
"github.com/yunlzheng/alertmanaer-dingtalk-webhook/model"
10
"github.com/yunlzheng/alertmanaer-dingtalk-webhook/transformer"
11
)
12
13
func Send(notification model.Notification, dingtalkRobot string) (err error) {
14
15
markdown, err := transformer.TransformToMarkdown(notification)
16
17
if err != nil {
18
return
19
}
20
21
data, err := json.Marshal(markdown)
22
if err != nil {
23
return
24
}
25
26
req, err := http.NewRequest(
27
"POST",
28
dingtalkRobot,
29
bytes.NewBuffer(data))
30
31
if err != nil {
32
return
33
}
34
35
req.Header.Set("Content-Type", "application/json")
36
client := &http.Client{}
37
resp, err := client.Do(req)
38
39
if err != nil {
40
return
41
}
42
43
defer resp.Body.Close()
44
fmt.Println("response Status:", resp.Status)
45
fmt.Println("response Headers:", resp.Header)
46
47
return
48
}
Copied!

扩展启动函数

首先为程序添加命令行参数支持,用于在启动时添加全局的Dingtalk群聊机器人地址。
1
package main
2
3
import (
4
"flag"
5
...
6
"github.com/yunlzheng/alertmanaer-dingtalk-webhook/notifier"
7
)
8
9
var (
10
h bool
11
defaultRobot string
12
)
13
14
func init() {
15
flag.BoolVar(&h, "h", false, "help")
16
flag.StringVar(&defaultRobot, "defaultRobot", "", "global dingtalk robot webhook")
17
}
18
19
func main() {
20
21
flag.Parse()
22
23
if h {
24
flag.Usage()
25
return
26
}
27
28
...
29
30
}
Copied!
同时通过notifier包的Send方法将告警通知发送给Dingtalk群聊机器人
1
func main() {
2
3
...
4
5
err = notifier.Send(notification, defaultRobot)
6
7
if err != nil {
8
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
9
10
}
11
12
c.JSON(http.StatusOK, gin.H{"message": "send to dingtalk successful!"})
13
}
Copied!

使用Dingtalk扩展

运行并启动dingtalk webhook服务之后,修改Alertmanager配置文件, 为default-receiver添加webhook配置,如下所示:
1
receivers:
2
- name: default-receiver
3
email_configs:
5
webhook_configs:
6
- url: http://localhost:8080/webhook
Copied!
重启Alertmanager服务后,手动拉高虚拟机CPU使用率触发告警条件,此时Dingtalk即可接收到相应的告警通知信息:
钉钉群机器人告警信息