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
- 根据指令返回 top10 之类的指标项;
- 根据指令跑 bosun alert 检查;
- 列出 scollector agent;
- 其他好玩的功能。