乐趣区

魔方云钉钉告警服务

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

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

退出移动版