飞书告警实践

在上一篇中我们完整实践了整个 Prometheus 监控体系是怎么玩儿的,关于告警信息的发送我们只是简单的将告警信息打印在屏幕中,有了一个直观的了解,但是在真正的业务场景中,是需要将告警信息发送给具体的告警接收人的,所以这一篇,我们开发个简单的 webhook 服务,来将告警信息通过飞书的方式发送。

新建飞书 Robot

需要新建一个飞书群组

在飞书群组中新建一个飞书 Robot

拿到飞书 Robot 的 Webhook 地址,接下来的开发中会用到这个 Webhook 地址

开发一个 Webhook 服务

基于飞书卡片模版open in new window,来开发 Webhook 服务

启动服务 go run main.go

点击查看代码
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"time"
)

type AlertMsg struct {
	Receiver          string
	Status            string
	GroupLabels       map[string]string
	CommonLabels      map[string]string
	CommonAnnotations map[string]string
	ExternalURL       string
	Alerts            []Alert
}

type Alert struct {
	Status       string
	Labels       map[string]string
	Annotations  map[string]string
	StartsAt     time.Time
	EndsAt       time.Time
	GeneratorURL string
	Fingerprint  string
}

type FeishuMsg struct {
	Card    MsgCard `json:"card"`
	MsgType string  `json:"msg_type"`
}

type MsgCard struct {
	Header   CardHeader    `json:"header"`
	Elements []CardElement `json:"elements"`
}

type CardHeader struct {
	Template string `json:"template"`
	Title    ContentTag
}
type ContentTag struct {
	Content string `json:"content"`
	Tag     string `json:"tag"`
}

type CardElement struct {
	Tag    string         `json:"tag"`
	Fields []FieldContent `json:"fields"`
}

type FieldContent struct {
	IsShort bool       `json:"is_short"`
	Text    ContentTag `json:"text"`
}

const Webhook = "https://open.feishu.cn/open-apis/bot/v2/hook/e6e55b00-8782-43a2-9cf4-a6fcf1b6aef4"

func main() {
	http.HandleFunc("/alerts", alertHandler)
	log.Fatal(http.ListenAndServe("0.0.0.0:5001", nil))
}

func alertHandler(w http.ResponseWriter, r *http.Request) {
	dec := json.NewDecoder(r.Body)
	defer r.Body.Close()

	var m AlertMsg
	if err := dec.Decode(&m); err != nil {
		log.Printf("decoding alert message error: %v", err)
		return
	}
	for _, alert := range m.Alerts {
		var feishu_msg FeishuMsg
		feishu_msg.MsgType = "interactive"
		// 告警消息
		if alert.Status == "firing" {
			feishu_msg.Card.Header.Template = "red"
			feishu_msg.Card.Header.Title.Content = fmt.Sprintf("崩盘 - %s", alert.Labels["_product_name"])
			feishu_msg.Card.Header.Title.Tag = "plain_text"
			content := fmt.Sprintf(
				"**告警名称**: %s\n**告警时间**: %s\n**告警概述**: %s\n**告警产品**: %s\n**告警实例**: %s\n",
				alert.Labels["alertname"],
				alert.StartsAt.UTC().Format("2006-01-02 15:04:05"),
				alert.Annotations["summary"],
				alert.Labels["_product_name"],
				alert.Labels["_name"],
			)
			field := FieldContent{
				IsShort: true,
				Text:    ContentTag{Tag: "lark_md", Content: content},
			}

			fields := make([]FieldContent, 0)
			fields = append(fields, field)
			element := CardElement{
				Tag:    "div",
				Fields: fields,
			}
			elements := make([]CardElement, 0)
			elements = append(elements, element)
			feishu_msg.Card.Elements = elements
		} else {
			// 恢复消息
			feishu_msg.Card.Header.Template = "green"
			feishu_msg.Card.Header.Title.Content = fmt.Sprintf("恢复 - %s", alert.Labels["_product_name"])
			feishu_msg.Card.Header.Title.Tag = "plain_text"
			content := fmt.Sprintf(
				"**告警名称**: %s\n**恢复时间**: %s\n**告警概述**: %s\n**告警产品**: %s\n**告警实例**: %s\n",
				alert.Labels["alertname"],
				alert.EndsAt.UTC().Format("2006-01-02 15:04:05"),
				alert.Annotations["summary"],
				alert.Labels["_product_name"],
				alert.Labels["_name"],
			)
			field := FieldContent{
				IsShort: true,
				Text:    ContentTag{Tag: "lark_md", Content: content},
			}

			fields := make([]FieldContent, 0)
			fields = append(fields, field)
			element := CardElement{
				Tag:    "div",
				Fields: fields,
			}
			elements := make([]CardElement, 0)
			elements = append(elements, element)
			feishu_msg.Card.Elements = elements
		}
		msg_body, err := json.Marshal(feishu_msg)
		if err != nil {
			log.Fatalf("json marshal feishu_msg error: %v", err)
		}
		req, err := http.NewRequest("POST", Webhook, bytes.NewBuffer(msg_body))
		req.Header.Set("Content-Type", "application/json")
		if err != nil {
			log.Fatalf("http new request error: %v", err)
		}
		client := &http.Client{}
		resp, err := client.Do(req)
		if err != nil {
			log.Fatalf("post feishu_msg error: %v", err)
		}
		defer resp.Body.Close()
	}
}

查看飞书告警信息

飞书群组接收到的告警信息如图

下面我们测试下告警恢复的消息,先把 node exporte 关闭再开启,这样就模拟了监控对象的离线和在线

结语

这里我们只是简单的实践了通过飞书的机器人来发送告警消息,还没有深入的开发。如:

  • 告警消息很多的情况下,怎么发送告警信息,因为如果还是一条条发送的话,会面临告警风暴
  • 不同的人需要接收到不同的告警消息,那么我们的后端服务就需要一个接收者列表,当发送消息的时候去请求指定的 Webhook 地址
  • 飞书的消息卡片支持回调,当我们收到告警卡片信息的时候,可以有按钮点击设置当前告警的维护
上次更新:
贡献者: kongzZ