BearyChat 监控机器人

本来想试下 slack,不过经 寂寞哥 介绍,测试了 slack 的中国“克隆版” BearyChat

本文主要测试了将 bosun 和 BearyChat 机器人结合起来,可以让 bosun 发送告警信息 到 BearyChat,或者在 BearyChat 中输入简单指令让 bosun 返回指标信息或指标图等。

如何在 BearyChat 注册团队就不提了,提下所用到的机器人,使用 BearyChat 提供的两种自定义机器人:incoming 和 outgoing。

Incoming

incoming 机器人是外部程序发送数据到 BearyChat 的接口,我这里命名为 BosunSender。

就是往这个 Hook 地址发 json 数据,很简单吧?

bosun 本身就支持 post json,所以就很方便了。

以下是 bosun 的 notification 配置:

notification BearyChat {  
    post = https://hook.bearychat.com/yyyyyy/incoming/xxxxxxxxxxxxxxxxxxxxx
    body = {"text": {{.|json}}}
    contentType = application/json
}

直接在 alert 配置里面调用 (critNotification = BearyChat ) 即可。这样在 BearyChat 上就能收到告警了:

如果讲究即时性和便携性,可以在手机上装个 BearyChat App,这样才能满足7*24的苦逼运维的需求。

Outgoing

outgoing 的意思是从 BearyChat 下发指令给外部服务,然后外服服务可以调用 incoming 将执行结果反馈到 BearyChat。

这里有三个需要注意的项,其中 token 是系统自动生成的;触发词是自己定义的指令开始词语,最后的 POST 地址 是外服服务的地址,具体来说就是往你设置的 POST 地址 POST一个特定格式的内容,如下:

{
  "token" : "aa3aa2916bb94f1fd48d185845b7f0f0",
  "ts" : 1355517523,
  "text" : "!baike 中国",
  "trigger_word" : "!baike",
  "subdomain" : "your_domain",
  "channel_name" : "your_channel",
  "user_name" : "your_name"
}

trigger_word 就是触发词,而 text 则是整个用户输入。

OK,了解了上面的信息后,可以开始写外部服务了。

一个例子

这里举个例子,比如说我设置了触发词 @Bosun,然后我在 BearyChat 中输入:

@Bosun 指标 图 负载 10.10.1.2 1 星期

我希望可以给我返回 10.10.1.2 这个主机的一星期负载指标图,显示在 BearyChat 中。

首先第一点要先解决 bosun 如何返回指标图的问题。

bosun 提供了 API,只需要将 表达式 base64 编码后之后 GET /api/egraph/{base64-encode-expression}.svg 即可。

  • 然而,bosun 的接口,为了安全起见,只对部分 ip 开放的;而我们又不清楚 BearyChat 会从哪个 ip 发请求过来。看来只能用 nginx 来做转发了,专门设置个 BearyChat 要访问的域名然后将特定 uri 请求转发给 bosun。

  • 这个问题解决之后,又发现 BearyChat 并不支持 svg 格式的图片显示 (或者是直接给它塞上面的 bosun api 地址是不行的?)。我后来想到的办法,是先将 svg 下载到本地,然后用 ImageMagick 转成 jpg 。。。 囧rz

相关功能函数:

func B64Encode(s string) (string) {  
    return base64.StdEncoding.EncodeToString([]byte(s))
}

func Download(url, localpath string) (bool) {  
    out, err := os.Create(localpath)
    if err != nil {
        return false
    }
    defer out.Close()

    resp, err := http.Get(url)
    if err != nil {
        return false
    }
    defer resp.Body.Close()

    _, err = io.Copy(out, resp.Body)
    if err != nil {
        return false
    } else {
        return true
    }
}

func Convert(src, dest string) (bool) {  
    cmd := fmt.Sprintf("/usr/bin/convert %s %s", src, dest)
    _, err := exec.Command("/bin/bash", "-c", cmd).Output()
        if err != nil {
        return false
    } else {
        return true
    }
}

func PicSender(s string) (bool) {  
    b := B64Encode(s)
    fn := b + ".svg"
    url := fmt.Sprintf("http://yourdomain.com/api/egraph/%s", fn)
    src := fmt.Sprintf("/data/web/pic/%s", fn)
        dest := fmt.Sprintf("/data/web/pic/%s.jpg", b)
    if ! Download(url, src) {
        return false
    }
    if ! Convert(src, dest) {
        return false
    }
    js := fmt.Sprintf(`{"text":"Bosun Message", "attachments":[{"title":"指标图","text":"指标图如下", "images":[{"url":"http://yourdomain.com/%s.jpg"}]}]}`, b)
    err := PostJson(BotUrl, js)
    if err != nil {
        return false
    } else {
        return true
    }
}

post json 和 解析 json (处理 outgoing 提交过来的 json ) 的处理:

func PostJson(url, s string) (error) {  
        jsonStr := []byte(s)
        req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr))
        req.Header.Set("Content-Type", "application/json")
        client := &http.Client{}
        _, err := client.Do(req)
        if err != nil {
                return err
        } else {
                return nil
        }
}

func ParseJson(s string) (map[string]interface{}, error) {  
        var dat map[string]interface{}
        if err := json.Unmarshal([]byte(s), &dat); err == nil {
                return dat, nil
        } else {
                return nil, err
        }
}

主函数里对参数的检查:

if request.Method == "POST" {  
    body, err := ioutil.ReadAll(request.Body)
    if err != nil {
        response.Write([]byte("read body err!"))
        return
    }
    dat, err := ParseJson(string(body))
    if err != nil {
        response.Write([]byte("parse json err!"))
        return
    }
    key, ok := dat["token"]
    if ! ok || key != "上面所说的系统自动生成的token" {
        response.Write([]byte("token err!"))
        return
    }
    text, ok := dat["text"]
    if ! ok {
        response.Write([]byte("text parameter err!"))
        return
    }
    word, ok := dat["trigger_word"]
    if ! ok {
        response.Write([]byte("trigger_word parameter err!"))
        return
    }
    ......
    // 之后就可以用 regexp 或者 strings 之类的文本处理拿到指令处理了...

nginx 转发:

server {  
    listen      80;
    server_name yourdomain.com;
    location / {
            proxy_redirect off;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            if ( $request_uri ~* "^/api/egraph/" ){
                proxy_pass http://localhost:8070;
            }
            root /data/web/pic;
    }
}

proxy_pass 的地址是 bosun 的地址。

效果如下:

其他

可以直接输出数值:

还可以结合 Zabbix 接口:

TODO

  1. 根据指令返回 top10 之类的指标项;
  2. 根据指令跑 bosun alert 检查;
  3. 列出 scollector agent;
  4. 其他好玩的功能。