夜鶯自定義告警模板

希望在告警通知里有以下數(shù)據(jù):
- 告知當前系統(tǒng)還有多少未處理的告警。
 - 告知當前告警恢復時候的具體值。
 - 告警通知里增加查看未處理告警的頁面鏈接。
 
具體實現(xiàn)
要實現(xiàn)上面的需求很簡單,夜鶯監(jiān)控的數(shù)據(jù)庫表alert_cur_event保存了我們所需要的當前未處理的告警總數(shù),而且夜鶯監(jiān)控也提供了查詢未處理告警的面板,而對于告警恢復時候的值我們只需要根據(jù)自定義的恢復promql即可查詢。
最簡單的方式就是直接通過notify.py腳本進行告警發(fā)送,我們只需要做一丟丟修改即可。
整體腳本如下:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import sys
import json
import requests
import pymysql
# 處理字符問題
reload(sys)
sys.setdefaultencoding('utf-8')
# 通過api查詢指標
def getPrometheus(url, promql):
    response = requests.get(url, params={'query': promql})
    data = json.loads(response.text)
    # 提取指標數(shù)據(jù)
    if response.status_code == 200
        result = data['data']['result']
        if len(result) == 1:
            return result[0]['value'][1]
        else:
            return 0
    else:
        return 0
def count_rows_and_get_rule_names():
    try:
        conn = pymysql.connect(
            host='127.0.0.1',
            port=3306,
            user='n9e',
            passwd='1234',
            db='n9e_v6',
            charset='utf8mb4'
        )
        cursor = conn.cursor()
        # Count the total number of rows in the table
        count_query = "SELECT COUNT(*) FROM alert_cur_event"
        cursor.execute(count_query)
        total_rows = cursor.fetchone()[0]
        return total_rows
    except Exception as e:
        print("Error: ", e)
class Sender(object):
    @classmethod
    def send_qywx(cls, payload):
        users = payload.get('event').get("notify_users_obj")
        is_recovered = payload.get('event').get("is_recovered")
        tokens = {}
        phones = {}
        res = {} 
        history_row = count_rows_and_get_rule_names()
        if is_recovered:
            # 獲取自定義的恢復promql
            promQL = payload.get('event').get("annotations").get("recovery_promql")
            url = "http://127.0.0.1:9090/api/v1/query"
            res = getPrometheus(url, promQL)
        # 查詢活躍告警的面板
        currAlert = "http://127.0.0.1:17000/alert-cur-events"
        for u in users:
            if u.get("phone"):
                phones[u.get("phone")] = 1
            contacts = u.get("contacts")
            if contacts.get("qywx_robot_token", ""):
                tokens[contacts.get("qywx_robot_token", "")] = 1
        headers = {
            "Content-Type": "application/json;charset=utf-8",
            "Host": "qyapi.weixin.qq.com"
        }
        for t in tokens:
            url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={}".format(t)
            content = payload.get('tpls').get("qywx", "qywx not found")
            content = "# **當前環(huán)境的全部告警數(shù)**: %s" % (history_row) + "\n" + content
            if is_recovered:
                content = content + "\n" + "> **恢復時值**: %s" % (res)
            if history_row > 0:
                content = content + "\n" + "[當前活躍告警](%s)" % (currAlert)
            body = {
                "msgtype": "markdown",
                "markdown": {
                    "content": content
                }
            }
            response = requests.post(url, headers=headers, data=json.dumps(body))
  
def main():
    payload = json.load(sys.stdin)
    with open(".payload", 'w') as f:
        f.write(json.dumps(payload, indent=4))
    for ch in payload.get('event').get('notify_channels'):
        send_func_name = "send_{}".format(ch.strip())
        if not hasattr(Sender, send_func_name):
            print("function: {} not found", send_func_name)
            continue
        send_func = getattr(Sender, send_func_name)
        send_func(payload)
  
def hello():
    print("hello nightingale")
if __name__ == "__main__":
    if len(sys.argv) == 1:
        main()
    elif sys.argv[1] == "hello":
        hello()
    else:
        print("I am confused")需要在服務器上安裝pymysql以及requests包
然后將上面的腳本放到夜鶯監(jiān)控面板->系統(tǒng)設置->通知設置->通知腳本中,并將腳本設置為啟用狀態(tài)。

然后新增名叫qywx的通知媒介以及名叫qywx_robot_token的聯(lián)系方式,在發(fā)送告警的時候會通過通知媒介來調(diào)用通知方法,比如你的通知媒介名叫zhangsan,那么你定義的方法名就是send_zhangsan。另外還會從聯(lián)系方式處獲取發(fā)送的token。
然后我們來創(chuàng)建一個通知模板,這個模板是在原生的基礎上進行更改的,如下創(chuàng)建一個名叫qywx的模板。
> **級別狀態(tài)**: {{if .IsRecovered}}<font color="info">告警恢復</font>{{else}}<font color="warning">發(fā)生告警</font>{{end}}   
> **級別級別**: S{{.Severity}}
> **告警類型**: {{.RuleName}}{{if .RuleNote}}   
> **告警詳情**: {{.RuleNote}}{{end}}{{if .TargetIdent}}   
> **監(jiān)控對象**: {{.TargetIdent}}{{end}}   
> **監(jiān)控指標**: {{.TagsJSON}}{{if not .IsRecovered}}   
> **觸發(fā)時值**: {{.TriggerValue}}{{end}}   
{{if .IsRecovered}}> **恢復時間**: {{timeformat .LastEvalTime}}{{else}}> **首次觸發(fā)時間**: {{timeformat .FirstTriggerTime}}{{end}}   
{{$time_duration := sub now.Unix .FirstTriggerTime }}{{if .IsRecovered}}{{$time_duration = sub .LastEvalTime .FirstTriggerTime }}{{end}}> **距離首次告警**: {{humanizeDurationInterface $time_duration}}
> **發(fā)送時間**: {{timestamp}}在實際發(fā)送過程中會對模板進行相應的增加。
最后,再來配置告警,比如我們現(xiàn)在要配置一個K8s中Pod的狀態(tài)異常的告警規(guī)則,如下:

填寫具體的規(guī)則名以及備注,并且填寫具體的promql。
往下繼續(xù)填寫通知媒介以及附加信息。

其中附加信息中就有告警恢復時候的promql,在python腳本中會獲取當前的promql,然后調(diào)用prometheus的接口進行查詢當前值,最后填充到告警模板中去。
以上就是具體的實現(xiàn)思路,希望對你有所啟發(fā)。
加餐
除了這種python腳本的方式,還可以通過自定義webhook的方式實現(xiàn),夜鶯是支持回調(diào)地址的,只需要把回調(diào)地址填寫進去即可。
那這個webhook應該怎么開發(fā)呢?
其實不需要我們做啥大的開發(fā),直接把夜鶯的源碼里告警相關(guān)的CV出來,改吧改吧就能直接用了。
首先,把alert_cur_event的數(shù)據(jù)結(jié)構(gòu)弄過來,查表就查它。
其次,增加一個查詢prometheus的接口,如下:
package prometheus  
  
import (  
   "context"  
   "devops-webhook-service/src/server/config"       "github.com/prometheus/client_golang/api"   "github.com/prometheus/client_golang/api/prometheus/v1"   "github.com/prometheus/common/model"   "github.com/toolkits/pkg/logger"   "time")  
  
func GetMetricsValue(promql string) string {  
   client, err := api.NewClient(api.Config{  
      Address: config.C.Prometheus.Address,  
   })  
   if err != nil {  
      logger.Error("init prometheus client failed. err: ", err)  
   }  
   queryAPI := v1.NewAPI(client)  
   result, warnings, err := queryAPI.Query(context.TODO(), promql, time.Now())  
   if err != nil {  
      // handle error  
      logger.Error("query prometheus metrics failed. err: ", err)  
   }  
   if len(warnings) > 0 {  
      // handle warnings  
   }  
   vector := result.(model.Vector)  
   //for _, sample := range vector {  
   // fmt.Printf("Time: %v, Value: %v\n", sample.Timestamp, sample.Value)   //}  
   return vector[0].Value.String()  
  
}再則,我們就可以把需要的告警字段都動議到告警模板中,通過template自動填充數(shù)據(jù)了。
## 當前環(huán)境的告警總數(shù): {{ .TotalAlert }}  
---  
**級別狀態(tài)**: {{if .IsRecovered}}<font color="info">S{{.Severity}} Recovered</font>{{else}}<font color="warning">S{{.Severity}} Triggered</font>{{end}}  
**規(guī)則標題**: {{.RuleName}}{{if .TargetIdent}}  
**監(jiān)控對象**: {{.TargetIdent}}{{end}}{{ if .IsRecovered }}  
**當前值**: {{ .RecoveryValue }}{{end}}  
**監(jiān)控指標**: {{.TagsJSON}}{{if not .IsRecovered}}  
**觸發(fā)時值**: {{.TriggerValue}}{{end}}  
{{if .IsRecovered}}**恢復時間**: {{timeformat .LastEvalTime}}{{else}}**首次觸發(fā)時間**: {{timeformat .TriggerTime}}{{end}}  
**發(fā)送時間**: {{timestamp}}  
---最后,就是在notify.go中做一丟丟的小修改。
比如event事件增加兩個字段。
type NoticeEvent struct {  
   *models.AlertCurEvent  
   RecoveryValue string // 恢復時候的值  
   TotalAlert    int    // 告警總數(shù)  
}比如在notify.go中的GenNotice方法里,增加查詢prometheus和數(shù)據(jù)庫的代碼。
var recoveryValue string  
if event.IsRecovered {  
   text := event.RuleNote  
   promql := strings.Split(text, "=")[1]  
   recoveryValue = prometheus.GetMetricsValue(promql)  
}  
  
// 獲取當前剩余的總告警數(shù)  
events, err := models.AlertCurEventGetAll(event.Cluster)  
if err != nil {  
   logger.Error("get alert event failed. err: ", err)  
}整體代碼也就只需要一丟丟東西。

最后
以上就是整體的實現(xiàn)了,這只是領導根據(jù)領導的需要做的,每個團隊的需求不一樣,實現(xiàn)方式肯定也不通,這里只是拋磚引玉。
個人建議使用webhook比較好一點,因為可以比較靈活的增加其他的功能,比如告警認領,比如告警抑制,比如告警轉(zhuǎn)發(fā)等。
另外,最近剛換工作沒多久,寫的文章少了,但是對技術(shù)的熱愛并沒有減少。















 
 
 








 
 
 
 