Bosun告警推送到Nagios

Bosun 提供了自己的 Dashboard 页面,但是我们基本上都是用 LOG 模式,即告警不显示在页面上,而是如果该告警没有消除,则一直会操作 action (即会持续发送告警)。当时这么做的原因是,Bosun 的 Dashboard 页面,其实一般的告警接受者很少上去,界面也很简陋。

因此,想了一个办法,将 Bosun 的告警推给 Nagios 来做显示,毕竟有 Nagstamon 这个桌面显示工具还是会方便好多。

Bosun API 接口

我的想法是直接给 Bosun 提交一个检查的规则配置,Bosun 检查完后将有 warning 或者 critial 的项返回来,然后 nagios 那边就好处理了。

官方文档是这么说的:

/api/rule
Test execution for rules. Can execute at various times and intervals, output templates, and send test emails. Example a request for details.  

Example a request for details ...

瞬间泪目。这也够懒的。。。

不过经过简单的打开浏览器调试者工具,在 Rule Editor 页面执行下 Test,就可以知道该 API 的参数了。

uri: /api/rule?alert=${your_alert_rule_name}  
payload: 整个配置文本内容  

用 Python 来举个例子,是这样的:

import requests  
url = 'http://127.0.0.1:8070/api/rule?alert=host_load'  
payload = '''  
    tsdbHost = localhost:4242
    tsdbVersion = 2.2
    smtpHost = smtp.163.com:25
    emailFrom = [email protected]
    template email {
        subject = {{.Last.Status}}: {{.Alert.Name}}
    }
    notification email {
        email = [email protected]
    }
    alert host_load {
        template = email
        warn = avg(q("sum:linux.loadavg_1_min{host=*}", "10m", "")) >= 3
        warnNotification = email
        crit = avg(q("sum:linux.loadavg_1_min{host=*}", "10m", "")) >= 5
        critNotification = email
    }
'''  
print requests.get(url, data=payload)

提交的配置里,template 填个简单的即可,反正我们并不需要用到 Bosun 本身的告警功能。最关键的是查询的语句,还有产生 warning 和 critical 的条件。

之前在写 Bosun 的监控 Dashboard 时候,已经写过这个接口了。这里贴下关键部分:

将不变的文本抽离出来一个常量

var BaseConfig = `  
    tsdbHost = localhost:4242
    tsdbVersion = 2.2
    smtpHost = smtp.163.com:25
    emailFrom = [email protected]
    template email {
        subject = {{.Last.Status}}: {{.Alert.Name}}
    }
    notification email {
        email = [email protected]
    }
`

生成规则配置的函数

func MakeRuleConfig(a string, r Rule) (c string) {  
    c = BaseConfig + fmt.Sprintf(`
            alert %s {
                template = email`, a)
    if r.WarnValue != "" {
        c = c + fmt.Sprintf(`
                warn = %s %s %s
                warnNotification = email
                `, r.Condition, r.Op, r.WarnValue)
    }
    if r.CritValue != "" {
        c = c + fmt.Sprintf(`
                crit = %s %s %s
                critNotification = email
                `, r.Condition, r.Op, r.CritValue)
    }
    c = c + "}"
    return
}

Rule 结构如下:

type Rule struct {  
    Condition string
    Op string
    WarnValue string
    CritValue string
}

Condition 就是查询语句,如上面的

avg(q("sum:linux.loadavg_1_min{host=*}", "10m", ""))  

Op 是 比较符,如 >= <=

WarnValue 和 CritValue 则是告警阀值。

执行 Test 的函数

func RuleTester(alert, glob, config string) (r []RuleRet, err error) {  
    r = make([]RuleRet, 0)
    url := BosunUrl + "/api/rule?alert=" + alert
    data := bytes.NewReader([]byte(config))
    resp, err := http.Post(url, "application/json", data)
    if err != nil {
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return
    }
    js, err := simplejson.NewJson(body)
    if err != nil {
        return
    }
    sets, _ := js.Get("Sets").Array()

    for _, a := range sets {
        b := a.(map[string]interface{})
        c := b["Results"].([]interface{})
        for _, d := range c {
            e := d.(map[string]interface{})
            f := e["Result"].(map[string]interface{})
            s := f["Status"].(string)
            var k string
            k = strings.Replace(e["Group"].(string), alert+"{", "", -1)
            globs := strings.Split(glob, ",")
            for _, g := range globs {
                k = strings.Replace(k, g, "", -1)
            }
            k = strings.Replace(k, "}", "", -1)
            k = strings.Replace(k, ",", "", -1)
            r = append(r, RuleRet{k, s})
        }
    }
    return
}

主要是用 simplejson 将结果解析出来,glob 参数是用来去掉一些不必要的字符,因为我这边只输出像 192.168.10.10:critial 这样的结果,返回的 RuleRet 结构体:

type RuleRet struct {  
    Key string
    Status string
}

执行脚本

package main

import (  
    "fmt"
    "flag"
    "bosunapi/api"
)

var (  
    a = flag.String("a", "", "alert name")
    w = flag.String("w", "", "warning value")
    c = flag.String("c", "", "critial value")
)

func main() {  
    flag.Parse()
    alert, warn, crit := *a, *w, *c
    var r api.Rule
    switch alert {
        case "host_cpu_used_percent":
            r = api.Rule{"host=", `avg(q("sum:rate{counter,,1}:os.cpu{host=*}", "10m", ""))`, `>=`, warn, crit}
        case "host_mem_free_percent":
            r = api.Rule{"host=", `avg(q("sum:os.mem.percent_free{host=*}", "10m", ""))`, `<=`, warn, crit}
        case "root_disk_free_percent":
            r = api.Rule{"disk=/,host=", `avg(q("sum:os.disk.fs.percent_free{disk=/,host=*}", "10m", ""))`, `<=`, warn, crit}
        case "data_disk_free_percent":
            r = api.Rule{"disk=/data,host=", `avg(q("sum:os.disk.fs.percent_free{disk=/data,host=*}", "10m", ""))`, `<=`, warn, crit}
        case "host_load":
            r = api.Rule{"host=", `avg(q("sum:linux.loadavg_1_min{host=*}", "10m", ""))`, `>=`, warn, crit}
    }
    config := api.MakeRuleConfig(alert, r)
        fmt.Println(config)
    Data, _ := api.RuleTester(alert, r.Glob, config)
    output := ""
    for _, v := range Data {
        if v.Status != "normal" {
            if output != "" {
                output = fmt.Sprintf("%s, %s:%s", output, v.Key, v.Status)
            } else {
                output = fmt.Sprintf("%s:%s", v.Key, v.Status)
            }
        }
    }
    fmt.Printf(output)
}

主要是自己定义好需要监控的规则,检查结果向 STDOUT 输出即可。

Nagios脚本

libexec 目录下添加一个简单的脚本

#!/bin/bash

Main() {  
    info=$(/data/sh/monitor/bosun_server_rule -a host_load -w 8 -c 10)
    if /bin/grep -q 'warning' <<< "${info}"; then
        echo "${info}"
        exit 1
    elif /bin/grep -q 'critial' <<< "${info}"; then
        echo "${info}"
        exit 2
    else
        echo "ok"
        exit 0
    fi
}
Main  

这里抽离出来用 Shell 脚本写,主要是有时候需要调整下告警阀值。。

至于 Nagios 服务端和客服端方面的配置,网上都有,就不提了。

其他

  1. Nagios 最好设置下执行的频率,看集群的负载情况而定,我这边是设置了 10 分钟执行一次;
  2. 上面的设置,一个告警项需要加一个 Nagios 脚本,略蛋疼。