魔方云钉钉告警服务

用户可以在魔方云中定义告警,当告警被触发时,魔方云。这样的告警流程是基于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”。

相应的钉钉账户就会收到一条消息。