夜鶯自定義告警模板
希望在告警通知里有以下數(shù)據(jù):
- 告知當(dāng)前系統(tǒng)還有多少未處理的告警。
- 告知當(dāng)前告警恢復(fù)時(shí)候的具體值。
- 告警通知里增加查看未處理告警的頁(yè)面鏈接。
具體實(shí)現(xiàn)
要實(shí)現(xiàn)上面的需求很簡(jiǎn)單,夜鶯監(jiān)控的數(shù)據(jù)庫(kù)表alert_cur_event保存了我們所需要的當(dāng)前未處理的告警總數(shù),而且夜鶯監(jiān)控也提供了查詢未處理告警的面板,而對(duì)于告警恢復(fù)時(shí)候的值我們只需要根據(jù)自定義的恢復(fù)promql即可查詢。
最簡(jiǎn)單的方式就是直接通過(guò)notify.py腳本進(jìn)行告警發(fā)送,我們只需要做一丟丟修改即可。
整體腳本如下:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import sys
import json
import requests
import pymysql
# 處理字符問(wèn)題
reload(sys)
sys.setdefaultencoding('utf-8')
# 通過(guò)api查詢指標(biāo)
def getPrometheus(url, promql):
response = requests.get(url, params={'query': promql})
data = json.loads(response.text)
# 提取指標(biāo)數(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:
# 獲取自定義的恢復(fù)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 = "# **當(dāng)前環(huán)境的全部告警數(shù)**: %s" % (history_row) + "\n" + content
if is_recovered:
content = content + "\n" + "> **恢復(fù)時(shí)值**: %s" % (res)
if history_row > 0:
content = content + "\n" + "[當(dāng)前活躍告警](%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")
需要在服務(wù)器上安裝pymysql以及requests包
然后將上面的腳本放到夜鶯監(jiān)控面板->系統(tǒng)設(shè)置->通知設(shè)置->通知腳本中,并將腳本設(shè)置為啟用狀態(tài)。
然后新增名叫qywx的通知媒介以及名叫qywx_robot_token的聯(lián)系方式,在發(fā)送告警的時(shí)候會(huì)通過(guò)通知媒介來(lái)調(diào)用通知方法,比如你的通知媒介名叫zhangsan,那么你定義的方法名就是send_zhangsan。另外還會(huì)從聯(lián)系方式處獲取發(fā)送的token。
然后我們來(lái)創(chuàng)建一個(gè)通知模板,這個(gè)模板是在原生的基礎(chǔ)上進(jìn)行更改的,如下創(chuàng)建一個(gè)名叫qywx的模板。
> **級(jí)別狀態(tài)**: {{if .IsRecovered}}<font color="info">告警恢復(fù)</font>{{else}}<font color="warning">發(fā)生告警</font>{{end}}
> **級(jí)別級(jí)別**: S{{.Severity}}
> **告警類型**: {{.RuleName}}{{if .RuleNote}}
> **告警詳情**: {{.RuleNote}}{{end}}{{if .TargetIdent}}
> **監(jiān)控對(duì)象**: {{.TargetIdent}}{{end}}
> **監(jiān)控指標(biāo)**: {{.TagsJSON}}{{if not .IsRecovered}}
> **觸發(fā)時(shí)值**: {{.TriggerValue}}{{end}}
{{if .IsRecovered}}> **恢復(fù)時(shí)間**: {{timeformat .LastEvalTime}}{{else}}> **首次觸發(fā)時(shí)間**: {{timeformat .FirstTriggerTime}}{{end}}
{{$time_duration := sub now.Unix .FirstTriggerTime }}{{if .IsRecovered}}{{$time_duration = sub .LastEvalTime .FirstTriggerTime }}{{end}}> **距離首次告警**: {{humanizeDurationInterface $time_duration}}
> **發(fā)送時(shí)間**: {{timestamp}}
在實(shí)際發(fā)送過(guò)程中會(huì)對(duì)模板進(jìn)行相應(yīng)的增加。
最后,再來(lái)配置告警,比如我們現(xiàn)在要配置一個(gè)K8s中Pod的狀態(tài)異常的告警規(guī)則,如下:
填寫具體的規(guī)則名以及備注,并且填寫具體的promql。
往下繼續(xù)填寫通知媒介以及附加信息。
其中附加信息中就有告警恢復(fù)時(shí)候的promql,在python腳本中會(huì)獲取當(dāng)前的promql,然后調(diào)用prometheus的接口進(jìn)行查詢當(dāng)前值,最后填充到告警模板中去。
以上就是具體的實(shí)現(xiàn)思路,希望對(duì)你有所啟發(fā)。
加餐
除了這種python腳本的方式,還可以通過(guò)自定義webhook的方式實(shí)現(xiàn),夜鶯是支持回調(diào)地址的,只需要把回調(diào)地址填寫進(jìn)去即可。
那這個(gè)webhook應(yīng)該怎么開(kāi)發(fā)呢?
其實(shí)不需要我們做啥大的開(kāi)發(fā),直接把夜鶯的源碼里告警相關(guān)的CV出來(lái),改吧改吧就能直接用了。
首先,把a(bǔ)lert_cur_event的數(shù)據(jù)結(jié)構(gòu)弄過(guò)來(lái),查表就查它。
其次,增加一個(gè)查詢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()
}
再則,我們就可以把需要的告警字段都動(dòng)議到告警模板中,通過(guò)template自動(dòng)填充數(shù)據(jù)了。
## 當(dāng)前環(huán)境的告警總數(shù): {{ .TotalAlert }}
---
**級(jí)別狀態(tài)**: {{if .IsRecovered}}<font color="info">S{{.Severity}} Recovered</font>{{else}}<font color="warning">S{{.Severity}} Triggered</font>{{end}}
**規(guī)則標(biāo)題**: {{.RuleName}}{{if .TargetIdent}}
**監(jiān)控對(duì)象**: {{.TargetIdent}}{{end}}{{ if .IsRecovered }}
**當(dāng)前值**: {{ .RecoveryValue }}{{end}}
**監(jiān)控指標(biāo)**: {{.TagsJSON}}{{if not .IsRecovered}}
**觸發(fā)時(shí)值**: {{.TriggerValue}}{{end}}
{{if .IsRecovered}}**恢復(fù)時(shí)間**: {{timeformat .LastEvalTime}}{{else}}**首次觸發(fā)時(shí)間**: {{timeformat .TriggerTime}}{{end}}
**發(fā)送時(shí)間**: {{timestamp}}
---
最后,就是在notify.go中做一丟丟的小修改。
比如event事件增加兩個(gè)字段。
type NoticeEvent struct {
*models.AlertCurEvent
RecoveryValue string // 恢復(fù)時(shí)候的值
TotalAlert int // 告警總數(shù)
}
比如在notify.go中的GenNotice方法里,增加查詢prometheus和數(shù)據(jù)庫(kù)的代碼。
var recoveryValue string
if event.IsRecovered {
text := event.RuleNote
promql := strings.Split(text, "=")[1]
recoveryValue = prometheus.GetMetricsValue(promql)
}
// 獲取當(dāng)前剩余的總告警數(shù)
events, err := models.AlertCurEventGetAll(event.Cluster)
if err != nil {
logger.Error("get alert event failed. err: ", err)
}
整體代碼也就只需要一丟丟東西。
最后
以上就是整體的實(shí)現(xiàn)了,這只是領(lǐng)導(dǎo)根據(jù)領(lǐng)導(dǎo)的需要做的,每個(gè)團(tuán)隊(duì)的需求不一樣,實(shí)現(xiàn)方式肯定也不通,這里只是拋磚引玉。
個(gè)人建議使用webhook比較好一點(diǎn),因?yàn)榭梢员容^靈活的增加其他的功能,比如告警認(rèn)領(lǐng),比如告警抑制,比如告警轉(zhuǎn)發(fā)等。
另外,最近剛換工作沒(méi)多久,寫的文章少了,但是對(duì)技術(shù)的熱愛(ài)并沒(méi)有減少。