告警平臺(tái)2.0——仿出強(qiáng)大
在《告警平臺(tái)1.0》中,我們實(shí)現(xiàn)了告警平臺(tái),可以實(shí)現(xiàn)納管通過AlertManager推送的告警信息,然后進(jìn)行靈活的告警通知發(fā)送。
在這個(gè)基礎(chǔ)上,我們可以實(shí)現(xiàn)對(duì)告警進(jìn)行認(rèn)領(lǐng)、屏蔽、關(guān)閉等操作,也能在移動(dòng)端進(jìn)行操作。
但是,這個(gè)方案現(xiàn)在只能被動(dòng)的接受告警,對(duì)于告警規(guī)則還是需要到Prometheus中去配置,當(dāng)告警規(guī)則較多的情況下,配置分類比較麻煩,所以在想:能不能在現(xiàn)有平臺(tái)上增加規(guī)則配置監(jiān)控功能?
所以,我又到老朋友《快貓F(tuán)lashcat》上進(jìn)行學(xué)習(xí),它們除了有故障管理,也有告警管理,可以實(shí)現(xiàn)監(jiān)控告警一體化。
而且,它支持的數(shù)據(jù)源還比較多,如下:
當(dāng)然,我不需要實(shí)現(xiàn)這么多,只要把常用的Prometheus和Elasticsearch實(shí)現(xiàn)即可。
邏輯梳理
相對(duì)來說,監(jiān)控規(guī)則的實(shí)現(xiàn)邏輯還是比較清晰,如下:
本質(zhì)上就是后臺(tái)系統(tǒng)周期性的在數(shù)據(jù)庫(Prometheus、Elasticsearch)中通過規(guī)則進(jìn)行查詢,當(dāng)異常條件滿足后,就生成對(duì)應(yīng)的告警事件,然后將告警事件分發(fā)到協(xié)作空間
。
另外,為了提升告警事件的管理效率,避免頻繁查詢、更新數(shù)據(jù)庫,當(dāng)產(chǎn)生告警事件后,會(huì)將其存入Redis,告警事件更新會(huì)同步更新Redis中的數(shù)據(jù)。同時(shí),后端會(huì)有一個(gè)常駐的攜程,獲取Redis中的告警事件,評(píng)估是否需要發(fā)送到協(xié)作空間。
實(shí)現(xiàn)效果
為了便于規(guī)則的管理,我們建立了分組,這里的分組沒有和協(xié)作空間建立必然的聯(lián)系,只是為了便于管理告警規(guī)則。
告警規(guī)則就會(huì)按組進(jìn)行分類展示
創(chuàng)建規(guī)則,目前支持創(chuàng)建Prometheus和Elasticsearch規(guī)則。
創(chuàng)建Prometheus規(guī)則。
我們可以:
- 為規(guī)則添加附加標(biāo)簽,比如為了按標(biāo)簽進(jìn)行告警發(fā)送的時(shí)候添加biz=a的標(biāo)簽。
- 定義具體的PromQL。
- 指定告警評(píng)估表達(dá)式,目前支持嚴(yán)重、警告、通知三個(gè)表達(dá)式。
- 可以配置告警持續(xù)時(shí)間,只有當(dāng)告警事件超過持續(xù)時(shí)間,才會(huì)產(chǎn)生真正的告警事件。
- 可以配置通知詳情,便于人員閱讀。
- 可以配置通知渠道,將事件推送到某個(gè)協(xié)作空間。
當(dāng)告警產(chǎn)生后就會(huì)發(fā)送到對(duì)應(yīng)的渠道,比如發(fā)送到企微的消息如下:
創(chuàng)建Elasticsearch規(guī)則。
主要配置的地方:
- 指定數(shù)據(jù)源:需要配置告警的數(shù)據(jù)源地址
- 指定索引:針對(duì)哪個(gè)索引做規(guī)則
- 指定篩選字段:通過這些字段進(jìn)行過濾日志
- 指定標(biāo)注字段:在發(fā)送告警通知的時(shí)候會(huì)將該部分發(fā)送到群里,便于運(yùn)維開發(fā)提取關(guān)鍵信息
其中,添加標(biāo)注是為了在展示需要的告警信息,比如:
另外,分組評(píng)估
用戶將告警信息進(jìn)行分組發(fā)送,觸發(fā)條件
用于判斷是不是發(fā)送告警通知。
當(dāng)告警事件產(chǎn)生,就會(huì)發(fā)送一條告警通知,如下:
日志監(jiān)控的告警邏輯也比較簡單,如下:
代碼實(shí)現(xiàn)
對(duì)于Prometheus監(jiān)控規(guī)則,定時(shí)從Prometheus時(shí)序數(shù)據(jù)庫中查詢值,然后和配置的策略進(jìn)行比較,如果滿足要求則產(chǎn)生告警事件。
// 從時(shí)序數(shù)據(jù)庫中查詢數(shù)據(jù)
resQuery, err = cli.(global.PrometheusProvider).Query(rule.PrometheusConfig.PromQL)
// 然后將值進(jìn)行比較
for _, v: = range resQuery {
for _, ruleExpr: = range rule.PrometheusConfig.Rules {
// 去除空格
cleanedExpr: = strings.ReplaceAll(ruleExpr.Expr, " ", "")
re: = regexp.MustCompile(`([^\d]+)(\d+)`)
matches: = re.FindStringSubmatch(cleanedExpr)
if len(matches) < 2 {
continue
}
t,
_: = strconv.ParseFloat(matches[2], 64)
option: = alarmCenter.EvalCondition {
Operator: matches[1],
QueryValue: v.Value,
ExpectedValue: t,
}
// 進(jìn)行比較,當(dāng)滿足條件后生成事件
if ok := alarmCenter.EvalCondition(option);ok{
event := alarmCenter.BuildAlertEvent(rule)
alarmCenter.SaveAlertEvent(event)
}
}
}
對(duì)于Elasticsearch監(jiān)控規(guī)則,定時(shí)從Elasticsearch數(shù)據(jù)庫中查詢滿足日志條數(shù),當(dāng)滿足告警條件后生成告警事件。
// 從ES中查詢值
curAt: = time.Now()
startsAt: = utils.ParserDuration(curAt, int(rule.ElasticsearchConfig.Scope), "m")
queryOptions: = provider.LogQueryOptions {
ElasticSearch: provider.Elasticsearch {
Index: rule.ElasticsearchConfig.Index,
QueryFilter: rule.ElasticsearchConfig.Filter,
Annotations: rule.ElasticsearchConfig.Annotations,
GroupEval: rule.ElasticsearchConfig.GroupEval,
},
StartAt: utils.FormatTimeToUTC(startsAt.Unix()),
EndAt: utils.FormatTimeToUTC(curAt.Unix()),
}
queryRes, count, err = cli.(global.ElasticsearchProvider).Query(queryOptions)
option := alarmCenter.EvalCondition {
Operator: rule.ElasticsearchConfig.TriggerCondition.Operator,
QueryValue: float64(count),
ExpectedValue: rule.ElasticsearchConfig.TriggerCondition.ExpectedValue,
}
// 進(jìn)行比較,當(dāng)滿足條件后生成事件
if ok := alarmCenter.EvalCondition(option);ok{
event := alarmCenter.BuildAlertEvent(rule)
alarmCenter.SaveAlertEvent(event)
}
條件評(píng)估的代碼如下:
func EvalCondition(ec alarmCenter.EvalCondition) bool {
// 使用 map 存儲(chǔ)操作符對(duì)應(yīng)的比較函數(shù)
operatorMap := map[string]func(float64, float64) bool{
">": func(a, b float64) bool { return a > b },
">=": func(a, b float64) bool { return a >= b },
"<": func(a, b float64) bool { return a < b },
"<=": func(a, b float64) bool { return a <= b },
"==": func(a, b float64) bool { return a == b },
"=": func(a, b float64) bool { return a == b },
"!=": func(a, b float64) bool { return a != b },
}
// 查找并執(zhí)行對(duì)應(yīng)的比較函數(shù)
if compareFunc, exists := operatorMap[ec.Operator]; exists {
return compareFunc(ec.QueryValue, ec.ExpectedValue)
}
global.GVA_LOG.Error(fmt.Sprintf("無效的評(píng)估條件,%s:%s:%f", ec.Type, ec.Operator, ec.ExpectedValue))
return false
}
告警通知的邏輯還是不變,當(dāng)監(jiān)聽到告警事件后,進(jìn)行對(duì)應(yīng)的告警通知。