用户可以在魔方云中定义告警,当告警被触发时,魔方云。这样的告警流程是基于 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”。
相应的钉钉账户就会收到一条消息。