使用Golang开发Webhook服务器实现Rancher短信告警

546 阅读4分钟

微信公众号:运维开发故事,作者:刘大仙

开发背景:

单位在内网使用了Rancher作为容器平台,没用办法使用Rancher提供的其他告警方式,但是内网有短信告警平台,所以我准备将Rancher的告警接入内网的短信告警平台,尝试使用Rancher提供的webhook方式。

需求分析:

内网提供的告警平台只需要我们把告警信息写入数据库即可,我们需要开发一个web服务器,接收Rancher发来的Json,然后拼接告警信息,再将信息写入数据库。

代码实现:

Rancher发来的Json示例内容如下

# 此处示例为pod告警json
{
  "receiver": "p-rh2r8:pag-m2xs2",
  "status": "resolved",
  "alerts": [    # 这里面是我们用到的东西, !:平台是在不停的重启pod的,如果勾选了已解决告警,alerts内部会有多个告警信息,包括已解决信息、告警信息,这个信息会不断的更新。
    {
      "status": "resolved",    # 告警时pod的状态
      "labels": {    # pod的信息
        "alert_name": "go-test", # 告警名称
        "alert_type": "podNotRunning", # 告警类型
        "cluster_name": "local (ID: c-qvhh4)",    # 集群名称
        "container_name": "go-test", # 容器名称
        "group_id": "p-rh2r8:pag-m2xs2",
        "logs": "Back-off pulling image \"xxx/go/app:13\"", # 告警日志
        "namespace": "default", # 命名空间
        "pod_name": "go-test-6574b67dd6-7vwzt", # 告警pod
        "project_name": "Default (ID: c-qvhh4:p-rh2r8)", # 项目名称
        "rule_id": "p-rh2r8:pag-m2xs2_par-9h4jr",
        "severity": "critical",
        "workload_name": "go-test" # 工作负载
      },
      "annotations": {},
      "startsAt": "2020-05-21T10:10:00.047662184Z", # 开始时间
      "endsAt": "2020-05-21T10:15:30.046352922Z", # 结束时间, 如果没有恢复,结束时间全为0
      "generatorURL": ""
    },
    {
      "status": "resolved",
      "labels": {
        "alert_name": "gotest",
        "alert_type": "podNotRunning",
        "cluster_name": "local (ID: c-qvhh4)",
        "container_name": "go-test",
        "group_id": "p-rh2r8:pag-m2xs2",
        "logs": "Back-off pulling image \"xxx/go/app:13\"",
        "namespace": "default",
        "pod_name": "go-test-6574b67dd6-7vwzt",
        "project_name": "Default (ID: c-qvhh4:p-rh2r8)",
        "rule_id": "p-rh2r8:pag-m2xs2_par-g4klr",
        "severity": "critical",
        "workload_name": "go-test"
      },
      "annotations": {},
      "startsAt": "2020-05-21T10:05:00.059679579Z",
      "endsAt": "2020-05-21T10:13:30.051427628Z",
      "generatorURL": ""
    }
  ],
  "groupLabels": {
    "group_id": "p-rh2r8:pag-m2xs2"
  },
  "commonLabels": {    # 同样也是告警信息
    "alert_type": "podNotRunning",
    "cluster_name": "local (ID: c-qvhh4)",
    "container_name": "go-test",
    "group_id": "p-rh2r8:pag-m2xs2",
    "logs": "Back-off pulling image \"xxx/go/app:13\"",
    "namespace": "default",
    "pod_name": "go-test-6574b67dd6-7vwzt",
    "project_name": "Default (ID: c-qvhh4:p-rh2r8)",
    "severity": "critical",
    "workload_name": "go-test"
  },
  "commonAnnotations": {},
  "externalURL": "http://alertmanager-cluster-alerting-0:9093",
  "version": "4",
  "groupKey": "{}/{group_id=\"p-rh2r8:pag-m2xs2\"}:{group_id=\"p-rh2r8:pag-m2xs2\"}"
}

Go代码

  • main.go

    package mainimport (    "RancherMSG/MsgStruct"
        "RancherMSG/sql"
        "encoding/json"
        "fmt"
        "github.com/gin-gonic/gin"
        "io/ioutil"
        "log")func msg(c *gin.Context) {
        # 这里的json信息我们使用结构体反序列化
        # 声明一个 RancherMsg 结构体    var m MsgStruct.RancherMsg
        # 从Body里面读取json
        jsonData, _ := ioutil.ReadAll(c.Request.Body)
        # log.Println(string(jsonData)) # 调试使用,打印源信息
        # json反序列化    if err := json.Unmarshal(jsonData, &m); err != nil {
            log.Println(err)        return
        }
        # 获取 被通知人联系电话
        phoneNum := c.Query("phone")
        # 遍历Alerts切片,获取所有的告警信息,在不监控pod的状态下,里面信息一般只有一个。for i := 0; i < len(m.Alerts); i++ {
            alerts := m.Alerts[i]
            # 拼接告警信息
            alertMsg := fmt.Sprintf(`
    告警类型:%s
    告警信息:%s
    当前状态:%s
    告警集群: %s
    告警容器: %s
    告警Pod:%s
    告警负载:%s
    容器命名空间:%s
    开始时间:%s
    结束时间:%s
    `, alerts.Labels.AlertType, alerts.Labels.Logs, alerts.Status, alerts.Labels.ClusterName, alerts.Labels.ContainerName,
                alerts.Labels.PodName, alerts.Labels.WorkloadName, alerts.Labels.Namespace, alerts.StartsAt, alerts.EndsAt)
            # 打印拼接好的告警信息
            log.Println(alertMsg, phoneNum)
            # 将告警信息插入短信数据库
            err := sql.ConnSql(alertMsg, phoneNum)        if err != nil {
                log.Println(err)
            }
        }
    }func main() {
        # 创建gin引擎
        r := gin.Default()
        # 设置url
        r.POST("/msg", msg)
        # 启动gin    if err := r.Run("192.168.111.2:8080"); err != nil {        return
        }
    }
    
  • msgStruct.go

    package MsgStruct
    
    # 
    type RancherMsg struct {
        # 这里我们只保留alerts里面的信息,其他的丢弃,如果有需要可以再加上
        Alerts            []Alerts          `json:"alerts"` }type Alerts struct {
        Status       string      `json:"status"`
        Labels       Labels      `json:"labels"`
        Annotations  Annotations `json:"annotations"`
        StartsAt     string      `json:"startsAt"`
        EndsAt       string      `json:"endsAt"`
        GeneratorURL string      `json:"generatorURL"`}type Labels struct {
        AlertName     string `json:"alert_name"`
        AlertType     string `json:"alert_type"`
        ClusterName   string `json:"cluster_name"`
        ContainerName string `json:"container_name"`
        ComponentName string `json:"component_name"`
        GroupId       string `json:"group_id"`
        Logs          string `json:"logs"`
        RuleId        string `json:"rule_id"`
        Severity      string `json:"severity"`
        Namespace     string `json:"namespace"`
        PodName       string `json:"pod_name"`
        ProjectName   string `json:"project_name"`
        WorkloadName  string `json:"workload_name"`}type Annotations struct{}
    
  • sqlOper.go

    package sqlimport (    "database/sql"
        "log"
        "strings")import (
        _ "github.com/mattn/go-adodb")type Mssql struct {
        *sql.DB
        dataSource string
        database   string
        windows    bool
        sa         SA
    }type SA struct {
        user   string
        passwd string}func (m *Mssql) Open() (err error) {    var conf []string
        conf = append(conf, "Provider=SQLOLEDB")
        conf = append(conf, "Data Source="+m.dataSource)
        conf = append(conf, "Initial Catalog="+m.database)
        conf = append(conf, "user id="+m.sa.user)
        conf = append(conf, "password="+m.sa.passwd)
        log.Println(strings.Join(conf, ";"))
        m.DB, err = sql.Open("adodb", strings.Join(conf, ";"))    if err != nil {        return err
        }    return nil}func ConnSql(msg, mob string) (err error) {
        db := Mssql{
            dataSource: "数据库连接地址",
            database:   "数据库",
            sa: SA{
                user:   "xxx",
                passwd: "xxx",
            },
        }    // 连接数据库
        err = db.Open()    if err != nil {
            log.Println("sql open:", err)        return err
        }    defer db.Close()    // 执行SQL语句
        stmt, err := db.Prepare("insert into USERWakeMessage (Msg, MobileNo) values (?,?)")    if err != nil {
            log.Println("Prepare: ", err)        return err
        }
        rs, err := stmt.Exec(msg, mob)    if err != nil {
            log.Println("Exec :", err)        return err
        }    // id, _ := rs.LastInsertId()
        affect, _ := rs.RowsAffected()
        log.Printf("向%s发送了%d消息", mob, affect)    return nil}
    

配置Rancher的通知:

  • 进入集群通知配置界面
    图片

  • 点击添加通知
    图片

  • 选择webhook通知方式,配置完成
    图片

  • 制作一个坏镜像,使用Rancher启动它,然后配置告警

    图片

图片

  • 配置告警,选择需要监控的pod,选择刚刚配置的通知。

    注意:

    如果要使用Prometheus表达式,需要安装项目级监控。

    图片

  • 测试结果

    图片