前言
- 功能:作为 alertmanager 的 webhook receiver,提取需要的数据转发到钉钉群机器人的webhook
- web框架:gin
- alertmanager版本:0.24
- 系统版本:ubuntu 20.04
- 功能比较简单,所以就随便写了,全部属于
main
包
原始json数据示例
{ "receiver": "web\\.hook", "status": "firing", "alerts": [{ "status": "firing", "labels": { "alertname": "node", "instance": "192.168.0.10", "job": "rh7", "severity": "critical" }, "annotations": { "description": "rh7 192.168.0.10 节点断联已超过1分钟!", "summary": "192.168.0.10 down " }, "startsAt": "2022-04-28T08:44:23.05Z", "endsAt": "0001-01-01T00:00:00Z", "generatorURL": "http://localhost.localdomain:19090/graph?g0.expr=up+%3D%3D+0\u0026g0.tab=1", "fingerprint": "726681bf4674e8a5" }], "groupLabels": { "alertname": "node" }, "commonLabels": { "alertname": "node", "instance": "192.168.0.10", "job": "rh7", "severity": "critical" }, "commonAnnotations": { "description": "rh7 192.168.0.10 节点断联已超过1分钟!", "summary": "192.168.0.10 down " }, "externalURL": "http://192.168.0.10:19092", "version": "4", "groupKey": "{}:{alertname=\"node\"}", "truncatedAlerts": 0 }
示例代码
- model.go(定义结构体)
package main // 定义接收JSON数据的结构体 type ReqAlert struct { Status string `json:"status"` StartsAt string `json:"startsAt"` EndsAt string `json:"endsAt"` GeneratorURL string `json:"generatorURL"` Fingerprint string `json:"fingerprint"` Labels ReqAlertLabel `json:"labels"` Annotations ReqAlertAnnotations `json:"annotations"` } type ReqGroupLabels struct { Alertname string `json:"alertname"` } type ReqCommonLabels struct { Alertname string `json:"alertname"` Instance string `json:"instance"` Job string `json:"job"` Severity string `json:"severity"` } type ReqCommonAnnotations struct { Description string `json:"description"` Summary string `json:"summary"` } type ReqAlertLabel struct { Alertname string `json:"alertname"` Instance string `json:"instance"` Job string `json:"job"` Severity string `json:"severity"` } type ReqAlertAnnotations struct { Description string `json:"description"` Summary string `json:"summary"` } type RequestBody struct { // alertmanager传来的请求体 Receiver string `json:"receiver"` Status string `json:"status"` ExternalURL string `json:"externalURL"` Version string `json:"version"` GroupKey string `json:"groupkey"` TruncatedAlerts float64 `json:"truncatedAlerts"` Alert []ReqAlert `json:"alerts"` GroupLabels ReqGroupLabels `json:"groupLabels"` CommonLabels ReqCommonLabels `json:"commonLabels"` CommonAnnotations ReqCommonAnnotations `json:"commonAnnotations"` }
- dingmsg.go(对接钉钉webhook,注意替换webhook的url)
package main import ( "bytes" "encoding/json" "fmt" "io/ioutil" "log" "net/http" "strings" ) type Md struct { Title string `json:"title"` Text string `json:"text"` } type Ding struct { Msgtype string `json:"msgtype"` Markdown Md `json:"markdown"` } func DingMarshal(text string) string { // struct序列化为json var myjson Ding = Ding{ Msgtype: "markdown", Markdown: Md{ Title: "锄田测试钉钉机器人", Text: text, }, } va, err := json.Marshal(myjson) if err != nil { panic(err) } return string(va) } func PostUrl(jsondata string) { // 发起post请求 // 替换钉钉群机器人的webhook url := "https://oapi.dingtalk.com/robot/send?access_token=123456" var jsonstr = []byte(jsondata) req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonstr)) if err != nil { log.Fatal(err) } req.Header.Set("Content-Type", "application/json;charset=utf-8") client := &http.Client{} resp, err := client.Do(req) if err != nil { log.Fatal(err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Fatal(err) } fmt.Printf("钉钉webhook响应: %v\n", string(body)) } func SpliceString(status, description, summary string) string { // 拼接字符串 var rst strings.Builder rst.WriteString("# 锄田测试钉钉机器人 \n\n") rst.WriteString("- status: " + status + " \n\n") rst.WriteString("- description: " + description + " \n\n") rst.WriteString("- summary: " + summary + " \n\n") return rst.String() }
- main.go
package main import ( "context" "errors" "fmt" "io" "log" "net/http" "os" "os/signal" "syscall" "time" "github.com/gin-gonic/gin" ) func ParseJson(c *gin.Context) { // 解析AlertManager 传递的 json 数据 var json RequestBody // 将数据解析到结构体中 if err := c.ShouldBindJSON(&json); err != nil { // 返回错误信息 c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error(), }) return } // 遍历json中alerts数据 for k, v := range json.Alert { fmt.Printf("k: %d, status: %s, description: %s, summary: %s \n", k, v.Status, v.Annotations.Description, v.Annotations.Summary) text := SpliceString(v.Status, v.Annotations.Description, v.Annotations.Summary) va := DingMarshal(text) PostUrl(va) } // 返回给客户端的消息 c.JSON(http.StatusOK, gin.H{ "msg": "success", }) } func main() { // 设置gin运行模式,可选:DebugMode、ReleaseMode、TestMode gin.SetMode(gin.ReleaseMode) // 关闭控制台日志颜色 gin.DisableConsoleColor() // 记录到文件 f, _ := os.OpenFile("gin.log", os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) gin.DefaultWriter = io.MultiWriter(f) r := gin.Default() // 只接受来自127.0.0.1的请求 r.SetTrustedProxies([]string{"127.0.0.1"}) v1 := r.Group("/v1") { v1.POST("/alert", ParseJson) } // 设置程序优雅退出 srv := &http.Server{ Addr: "127.0.0.1:19100", Handler: r, } go func() { if err := srv.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) { log.Printf("listen: %s\n", err) } }() quit := make(chan os.Signal) signal.Notify(quit, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("Shutting down server...") ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { log.Fatal("Server forced to shutdown:", err) } log.Println("Server exiting") }
使用示例
- 导入依赖
go mod tidy
- 编译
go build -o goalert.bin ./main.go ./model.go ./dingmsg.go
- 运行
./goalert.bin
- 测试。使用python + requests 测试。python代码如下
import requests # 注意json字符串的代码转为utf-8,否则会报Unicode的错误 # 使用json原字符串,而不是字典 json_firing = r""" {"receiver":"web\\.hook","status":"firing","alerts":[{"status":"firing","labels":{"alertname":"node","instance":"192.168.0.10","job":"rh7","severity":"critical"},"annotations":{"description":"rh7 192.168.0.10 节点断联已超过1分钟!","summary":"192.168.0.10 down "},"startsAt":"2022-04-28T08:44:23.05Z","endsAt":"0001-01-01T00:00:00Z","generatorURL":"http://localhost.localdomain:19090/graph?g0.expr=up+%3D%3D+0\u0026g0.tab=1","fingerprint":"726681bf4674e8a5"}],"groupLabels":{"alertname":"node"},"commonLabels":{"alertname":"node","instance":"192.168.0.10","job":"rh7","severity":"critical"},"commonAnnotations":{"description":"rh7 192.168.0.10 节点断联已超过1分钟!","summary":"192.168.0.10 down "},"externalURL":"http://192.168.0.10:19092","version":"4","groupKey":"{}:{alertname=\"node\"}","truncatedAlerts":0} """.encode("utf-8").decode("latin1") url = "http://127.0.0.1:19100/v1/alert" resp = requests.post(url=url, data = json_firing, headers={"Content-Type": "application/json"}) print(resp.text)
- alertmanager配置略过
补充
- 钉钉可以通过面对面建群的方式建一个单人群,单人群也能添加机器人。
- 如果不知道alertmanager发送的json数据是什么样,可以写个服务端,直接接收数据不解析,在控制台原样输出字符串。示例代码如下:
package main import ( "fmt" "io/ioutil" "net/http" ) func f1(w http.ResponseWriter, r *http.Request) { // 向客户端响应ok defer fmt.Fprintf(w, "ok\n") // 获取客户端的请求方式 fmt.Println("method:", r.Method) // 获取客户端请求的body body, err := ioutil.ReadAll(r.Body) if err != nil { fmt.Printf("read body err, %v\n", err) return } fmt.Println("json: ", string(body)) } func main() { http.HandleFunc("/", f1) http.ListenAndServe("127.0.0.1:8888", nil) }