用户可以在魔方云中定义告警,当告警被触发时,魔方云。这样的告警流程是基于Prometheus和Alertmanager的,具体的流程如图1所示。
图1 告警流程
其中Prometheus是监控系统,负责对集群的监控。Alertmanager负责告警的发送。
用户在魔方云中设置了具体的告警规则,每个告警规则对应接收对象,魔方云会把告警规则写入Prometheus的配置文件,并把告警规则对应的接收对象的信息写入Alertmanager。Prometheus会监控告警规则描述的值,当告警被触发时,Prometheus将告警的内容发送给Alertmanager,Alertmanager则将告警信息与的接受者信息对应起来,将信息发送给接收者。
Alertmanager本身支持以下几种类型的接收对象:
电子邮件
Slack
PagerDuty
微信
Webhook
其中前4种是主流的IT服务对象。Webhook是通用接收对象,可以用于扩展其他原本不支持的服务对象,钉钉的告警服务就是通过webhook来扩展的。扩展的思路为:首先编写一个http服务端,用于接收钉钉的告警信息。随后在魔方云中添加一个webhook配置,指向部署的服务端的地址,并把钉钉告警的配置作为参数添加在url中。告警触发后按照上图的流程由alertmanager转发到部署的服务端,服务端接收到告警信息后,读取url中的相关参数,最后将告警发送至钉钉。图2是添加了告警转发服务后的流程图。
图2 钉钉告警流程
钉钉告警扩展方法
1.编写钉钉告警转发服务端程序
服务端首先需要做的事是接收魔方云发送的webhook告警信息,并从URL中读取钉钉告警的配置:钉钉webhook、需要at用户的账号和是否at所有人。
对于webhook告警,alertmanager会以json形式发送如下的结构体
type Alert struct {
Status string `json:"status"`
Labels map[string]string `json:"labels"`
Annotations map[string]string `json:"annotations"`
StartsAt time.Time `json:"startsAt"`
EndsAt time.Time `json:"endsAt"`
GeneratorURL string `json:"generatorURL"`
}
type Message struct {
Version string `json:"version"`
GroupKey string `json:"groupKey"`
Status string `json:"status"`
Receiver string `json:"receiver"`
GroupLabels map[string]string `json:"groupLabels"`
CommonLabels map[string]string `json:"commonLabels"`
CommonAnnotations map[string]string `json:"commonAnnotations"`
ExternalURL string `json:"externalURL"`
Alerts []Alert `json:"alerts"`
}
服务端接收告警信息并读取url中的参数。
func ReceiveAndSend(w http.ResponseWriter, req *http.Request) {
log.SetFlags(log.LstdFlags | log.Lshortfile)
body, err := ioutil.ReadAll(req.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = fmt.Fprint(w, err)
log.Printf("[ERROR] %s", err)
return
}
alertMessage := Message{}
_ = json.Unmarshal(body, &alertMessage)
err = req.ParseForm()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = fmt.Fprint(w, err)
return
}
if _, ok := req.Form["webhook"]; !ok {
log.Print("[ERROR] url argument \"webhook\" is null")
return
}
if _, ok := req.Form["atmobiles"]; !ok {
log.Print("[ERROR] url argument \"atmobiles\" is null")
return
}
if _, ok := req.Form["isatall"]; !ok {
log.Print("[ERROR] url argument \"isatall\" is null")
return
}
webhook := req.Form["webhook"][0]
atmobiles := req.Form["atmobiles"]
isatall, _ := strconv.ParseBool(req.Form["isatall"][0])
err = SendToDingtalk(alertMessage, webhook, atmobiles, isatall)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, _ = fmt.Fprint(w, err)
log.Printf("[ERROR] %s", err)
return
}
_, _ = fmt.Fprint(w, "Alert sent successfully")
}
向从url中读取的地址发送钉钉告警信息。
type At struct {
AtMobiles []string `json:"atMobiles"`
IsAtAll bool `json:"isAtAll"`
}
type DingTalkMarkdown struct {
MsgType string `json:"msgtype"`
At At `json:"at"`
Markdown Markdown `json:"markdown"`
}
type Markdown struct {
Title string `json:"title"`
Text string `json:"text"`
}
const layout = "Jan 2, 2006 at 3:04pm (MST)"
func SendToDingtalk(alertMessage Message, webhook string, atMobiles []string, isAtAll bool) error {
groupKey := alertMessage.CommonLabels["group_id"]
status := alertMessage.Status
message := fmt.Sprintf("### 通知组:%s(状态:%s)\n\n", groupKey, status)
if _, ok := alertMessage.CommonLabels["alert_type"]; !ok {
return errors.New("alert type is null")
}
var description string
switch alertMessage.CommonLabels["alert_type"] {
case "event":
if _, ok := alertMessage.CommonLabels["event_type"]; !ok {
return errors.New("event_type is null in commonLabels")
}
if _, ok := alertMessage.GroupLabels["resource_kind"]; !ok {
return errors.New("resource kind is null in groupLabels")
}
description = fmt.Sprintf("\n > %s event of %s occuored\n\n", alertMessage.CommonLabels["event_type"], alertMessage.GroupLabels["resource_kind"])
case "systemService":
// ...
default:
return errors.New("invalid alert type")
}
message += description
for _, alert := range alertMessage.Alerts {
if alert.Status != "firing" {
continue
}
message += "-----\n"
for k, v := range alert.Labels {
message += fmt.Sprintf("- %s : %s\n", k, v)
}
message += fmt.Sprintf("- 起始时间:%s\n", alert.StartsAt.Format(layout))
}
dingtalkText := DingTalkMarkdown{
MsgType: "markdown",
At: At{
AtMobiles: atMobiles,
IsAtAll: isAtAll,
},
Markdown: Markdown{
Title: fmt.Sprintf("通知组:%s(当前状态:%s)", groupKey, status),
Text: message,
},
}
data, err := json.Marshal(dingtalkText)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, webhook, bytes.NewBuffer(data))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := http.Client{Transport:tr}
resp, err := client.Do(req)
if err != nil {
return err
}
if resp.StatusCode != 200 {
log.Printf("[ERROR] %s", resp.Header)
}
log.Printf("[INFO] Alert message sent to %s successfully", webhook)
_ = resp.Body.Close()
return nil
}
2.部署服务端
将服务端程序制作成docker镜像,上传至镜像仓库。在魔方云的helm包中添加一个依赖charts,使用刚才制作的docker镜像。在用户添加了告警规则后,钉钉告警转发服务就会自动启动。
钉钉告警使用流程
1.添加钉钉通知
首先在钉钉群中添加一个自定义机器人,并复制该机器人的webhook。
进入集群页面,点击侧边栏的“通知”,然后点击右边的“添加通知”按钮。
选择“dingtalk”,并填写相关信息。可以点击“测试”按钮来测试填写的信息是否正确,如果没有错误,对应的钉钉账号会收到一条测试消息。确认无误后点击下方的”添加“按钮。
2.添加告警规则
点击侧边栏的”告警“进入告警页面,然后点击右边的”添加告警组“按钮,配置告警规则,最好降低告警触发的条件,便于测试,然后在接收者栏中选择钉钉,可以在“Notifier”中填写要at的用户的手机号码,用英文逗号分隔。在这里添加的at用户会覆盖通知中的相应用户。最后点击”创建“按钮。此时一条告警规则已经创建完毕,当告警触发时会向钉钉发送告警信息。
3.等待告警触发
等待告警触发后,相应告警的状态会变成红色字体的“Alerting”。
相应的钉钉账户就会收到一条消息。