prometheus 的幾種告警方式
prometheus 我們都知道它是最近幾年特別火的一個開源的監控工具,原生支持 kubernetes,如果你使用的是 kubernetes 集群,那么使用 prometheus 將會是非常方便的,而且 prometheus 也提供了報警工具alertmanager
,實際上在 prometheus 的架構中,告警能力是單獨的一部分,主要是通過自定義一堆的rule
即告警規則,來周期性的對告警規則進行計算,並且會根據設置的報警觸發條件,如果滿足,就會進行告警,也就是會向alertmanager
發送告警信息,進而由alertmanager
進行告警。
那么,alertmanager
告警又是通過何種途徑呢?其實有很多種方式,例如:
- 郵件告警
- 企業微信告警
- 釘釘告警
- slack 告警
- webhook 接口方式告警
其實還有一些,但這些都不重要,這些只是工具,重要的是如何運用,下面就介紹下使用 webhook 的方式來讓 alertmanager 調用接口,發送POST
請求完成告警消息的推送,而這個推送可以是郵件,也可以是微信,釘釘等。
調用接口以郵件形式告警
大體流程是這樣的,首先在我們定義好一堆告警規則之后,如果觸發條件,alertmanager 會將報警信息推送給接口,然后我們的這個接口會做一些類似與聚合、匯總、優化的一些操作,然后將處理過的報警信息再以郵件的形式發送給指定的人或者組。也就是下面這個圖:
我們這里的重點主要是如何寫這個 webhook,以及寫 webhook 的時候需要注意什么?下面將一一講解
假設你有一個 prometheus 監控系統,並且告警規則都已配置完成
配置 alertmanager
首先得先配置 alertmanager,讓其可以調用接口,配置方式很簡單,只需要指定一下接口地址即可,如下:
receivers:
- webhook_configs:
url: http://10.127.34.107:5000/webhook
send_resolved: true
這就完了!當然可以指定多種告警方式
這樣配置完成后,alertmanger 就會把告警信息以 POST 請求方式調用接口
編寫一個最簡單的接口
既然是用 python 來編寫一個接口,那么肯定是用 flask 的,代碼也非常簡單,如下:
import json
from flask import Flask, request
from gevent.pywsgi import WSGIServer
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
prometheus_data = json.loads(request.data)
print(prometheus_data)
return "test"
if __name__ == '__main__':
WSGIServer(('0.0.0.0', 5000), app).serve_forever()
上面導入的一些模塊,記得要去下載哦
pip install flask
pip install gevent
這樣的話,我們直接運行此段代碼,此時機器上會監聽 5000 端口,如果此時 prometheus 有告警,那么我們就會看到 prometheus 傳過來的數據格式是什么樣的了,這里我貼一個示例:
{
'receiver': 'webhook',
'status': 'firing',
'alerts': [{
'status': 'firing',
'labels': {
'alertname': '內存使用率',
'instance': '10.127.92.100',
'job': 'sentry',
'severity': 'warning',
'team': 'ops'
},
'annotations': {
'description': '內存使用率已超過55%,內存使用率:58%',
'summary': '內存使用率'
},
'startsAt': '2020-12-30T07:20:08.775177336Z',
'endsAt': '0001-01-01T00:00:00Z',
'generatorURL': 'http://prometheus-server:9090/graph?g0.expr=round%28%281+-+%28node_memory_MemAvailable_bytes%7Bjob%3D%22sentry%22%7D+%2F+%28node_memory_MemTotal_bytes%7Bjob%3D%22sentry%22%7D%29%29%29+%2A+100%29+%3E+55&g0.tab=1',
'fingerprint': '09f94bd1aa7da54f'
}, {
'status': 'firing',
'labels': {
'alertname': '內存使用率',
'instance': '10.127.92.101',
'job': 'sentry',
'severity': 'warning',
'team': 'ops'
},
'annotations': {
'description': '內存使用率已超過55%,內存使用率:58%',
'summary': '內存使用率'
},
'startsAt': '2020-12-30T07:20:08.775177336Z',
'endsAt': '0001-01-01T00:00:00Z',
'generatorURL': 'http://prometheus-server:9090/graph?g0.expr=round%28%281+-+%28node_memory_MemAvailable_bytes%7Bjob%3D%22sentry%22%7D+%2F+%28node_memory_MemTotal_bytes%7Bjob%3D%22sentry%22%7D%29%29%29+%2A+100%29+%3E+55&g0.tab=1',
'fingerprint': '8a972e4907cf2c60'
}],
'groupLabels': {
'alertname': '內存使用率'
},
'commonLabels': {
'alertname': '內存使用率',
'job': 'sentry',
'severity': 'warning',
'team': 'ops'
},
'commonAnnotations': {
'summary': '內存使用率'
},
'externalURL': 'http://alertmanager-server:9093',
'version': '4',
'groupKey': '{}:{alertname="內存使用率"}',
'truncatedAlerts': 0
}
通過 prometheus 傳過來的告警信息,可以看到是一個標准的json
,我們在使用python
在做處理時,需要先將json
字符串轉換成python
的字典,可以用json
這個模塊來實現,通過這個json
我們可以得到以下信息(非常重要):
- 每次發出的
json
數據流中的報警信息是同一個類型的報警,比如這里都是關於內存的 status
:表示告警的狀態,兩種:firing
和resolved
alerts
:是一個列表,里面的元素是由字典組成,每一個元素都是一條具體的告警信息commonLabels
:這里面就是一些公共的信息
剩下的幾個 key 都比較好理解,就不一一說了,下面結合 prometheus 的一些 rule 來看下這個告警是憑什么這樣發的。
# cat system-rule.yaml #文件名隨意設置,因為prometheus的配置里配置的是: *.yaml
groups:
- name: sentry
rules:
- alert: "Memory Usage"
expr: round((1-(node_memory_MemAvailable_bytes{job='sentry'} / (node_memory_MemTotal_bytes{job='sentry'})))* 100) > 85
for: 5m
labels:
team: ops
severity: warning
cloud: yizhuang
annotations:
summary: "Memory usage is too high and over 85% for 5min"
description: "The current host {{$labels.instance}}' memory usage is {{ $value }}%"
這里就是配置的告警規則,告訴 prometheus 應該按照什么方式進行告警,配置完成后,要在 prometheus 的配置里引用下,如下所示:
# cat prometheus.yml
global:
scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
# scrape_timeout is set to the global default (10s).
# Alertmanager configuration
alerting:
alertmanagers:
- static_configs:
- targets: ['10.10.10.111:9093']
# 就是這里,看這里
rule_files:
- "/alertmanager/rule/*.yaml" #文件目錄隨意設置
...
...
...
此處省略一堆配置
到這里應該就知道告警規則是什么發出來的了吧,然后也應該知道告警內容為什么是這樣的了吧,嗯,下面看下最關鍵的地方
處理原始告警信息並進行郵件告警
原始的告警信息看起來還挺規則的,只需要拼接下就可以了,但是有一個問題就是alerts
里面的startsAt
和endsAt
這倆時間格式有些問題,是 UTC 時區的時間,需要轉換下。還有一個地方需要注意的,最外層的status
如果是firing
狀態,就不代表alerts
中的status
就一定都是firing
,還有可能是resolved
,如下json
所示:
{
'receiver': 'webhook',
'status': 'firing',
'alerts': [{
'status': 'resolved', # 這里就是resolved狀態,所以處理時需要注意下
'labels': {
'alertname': 'CPU使用率',
'instance': '10.127.91.26',
'severity': 'warning',
'team': 'ops'
},
'annotations': {
'description': 'CPU使用率已超過35%,CPU使用率:38%',
'summary': 'CPU使用率'
},
'startsAt': '2020-12-30T07:38:38.775177336Z',
'endsAt': '2020-12-30T07:38:53.775177336Z',
'generatorURL': 'http://prometheus-server:9090/graph?g0.expr=round%28100+-+%28avg+by%28instance%29+%28irate%28node_cpu_seconds_total%7Bjob%3D%22sentry%22%2Cmode%3D%22idle%22%7D%5B5m%5D%29%29+%2A+100%29%29+%3E+35&g0.tab=1',
'fingerprint': '58393b2abd2c6987'
}, {
'status': 'resolved',
'labels': {
'alertname': 'CPU使用率',
'instance': '10.127.92.101',
'severity': 'warning',
'team': 'ops'
},
'annotations': {
'description': 'CPU使用率已超過35%,CPU使用率:38%',
'summary': 'CPU使用率'
},
'startsAt': '2020-12-30T07:42:08.775177336Z',
'endsAt': '2020-12-30T07:42:38.775177336Z',
'generatorURL': 'http://prometheus-server:9090/graph?g0.expr=round%28100+-+%28avg+by%28instance%29+%28irate%28node_cpu_seconds_total%7Bjob%3D%22sentry%22%2Cmode%3D%22idle%22%7D%5B5m%5D%29%29+%2A+100%29%29+%3E+35&g0.tab=1',
'fingerprint': 'eaca600142f9716c'
}],
'groupLabels': {
'alertname': 'CPU使用率'
},
'commonLabels': {
'alertname': 'CPU使用率',
'severity': 'warning',
'team': 'ops'
},
'commonAnnotations': {
'summary': 'CPU使用率'
},
'externalURL': 'http://alertmanager-server:9093',
'version': '4',
'groupKey': '{}:{alertname="CPU使用率"}',
'truncatedAlerts': 0
}
那既然該注意的都注意了,就開始干吧,首先說下我要實現的一個最終結果:
- 時區轉換
- 不同類型的告警信息推送給不同的人
- 告警內容以表格的形式展示,通過 html 實現
時區轉換
先看下時區轉換,這個比較好解決,代碼如下:
import datetime
from dateutil import parser
def time_zone_conversion(utctime):
format_time = parser.parse(utctime).strftime('%Y-%m-%dT%H:%M:%SZ')
time_format = datetime.datetime.strptime(format_time, "%Y-%m-%dT%H:%M:%SZ")
return str(time_format + datetime.timedelta(hours=8))
發送郵件
再來看下郵件發送,也很簡單,代碼如下:
import smtplib
from email.mime.text import MIMEText
def sendEmail(title, content, receivers=None):
if receivers is None:
receivers = ['chenf-o@glodon.com']
mail_host = "xxx"
mail_user = "xxx"
mail_pass = "xxx"
sender = "xxx"
msg = MIMEText(content, 'html', 'utf-8')
msg['From'] = "{}".format(sender)
msg['To'] = ",".join(receivers)
msg['Subject'] = title
try:
smtpObj = smtplib.SMTP_SSL(mail_host, 465)
smtpObj.login(mail_user, mail_pass)
smtpObj.sendmail(sender, receivers, msg.as_string())
print('mail send successful.')
except smtplib.SMTPException as e:
print(e)
告警模板生成
下面就是告警推送的形式了,上面說了,使用表格的形式,如果用 html 來生成表格,還是比較簡單的,但是這個表格是不停的變化的,所以為了支持這個動態變化,肯定是得用到模板語言:jinja
了,如果是搞運維的肯定知道ansible
,ansible 里的 template 用的也是jinja
模板語言,所以比較好理解,這里就不再單獨說了,后面會詳細說一下 python 中如何使用這個jinja
模板語言,不明白的可以先看下官方文檔,比較簡單: http://docs.jinkan.org/docs/jinja2/
那么我這個 html 就長成了這個樣子,由於本人對前端一點都不懂,所以能實現我的需求就行了。
<meta http-equiv="Content-Type"content="text/html;charset=utf-8">
<html align='left'>
<body>
<h2 style="font-size: x-large;">{{ prometheus_monitor_info['commonLabels']['cloud'] }}--監控告警通知</h2><br/>
<br>
<table border="1" width = "70%" cellspacing='0' cellpadding='0' align='left'>
<tr>
<!--監控類型:系統層級,業務層級,服務層級等等-->
<th style="font-size: 20px; padding: 5px; background-color: #F3AE60">監控類別</th>
<!--狀態:報警通知還是恢復通知-->
<th style="font-size: 20px; padding: 5px; background-color: #F3AE60">狀態</th>
<!--狀態:級別:報警級別-->
<th style="font-size: 20px; padding: 5px; background-color: #F3AE60">級別</th>
<!--狀態:實例:機器地址-->
<th style="font-size: 20px; padding: 5px; background-color: #F3AE60">實例</th>
<!--狀態:描述:報警描述-->
<th style="font-size: 20px; padding: 5px; background-color: #F3AE60">描述</th>
<!--狀態:詳細描述:報警詳細描述-->
<th style="font-size: 20px; padding: 5px; background-color: #F3AE60">詳細描述</th>
<!--狀態:開始時間:報警開始時間-->
<th style="font-size: 20px; padding: 5px; background-color: #F3AE60">開始時間</th>
<!--狀態:開始時間:報警結束時間-->
<th style="font-size: 20px; padding: 5px; background-color: #F3AE60">結束時間</th>
</tr>
{% for items in prometheus_monitor_info['alerts'] %}
<tr align='center'>
{% if loop.first %}
<td style="font-size: 16px; padding: 3px; word-wrap: break-word; background-color: #F3AE60" rowspan="{{ loop.length }}">{{ prometheus_monitor_info['commonLabels']['alertname'] }}</td>
{% endif %}
{% if items['status'] == 'firing' %}
<td style="font-size: 16px; padding: 3px; background-color: red; word-wrap: break-word">告警</td>
{% else %}
<td style="font-size: 16px; padding: 3px; background-color: green; word-wrap: break-word">恢復</td>
{% endif %}
<td style="font-size: 16px; padding: 3px; word-wrap: break-word; background-color: #EBE4D3">{{ items['labels']['severity'] }}</td>
<td style="font-size: 16px; padding: 3px; word-wrap: break-word; background-color: #EBE4D3">{{ items['labels']['instance'] }}</td>
<td style="font-size: 16px; padding: 3px; word-wrap: break-word; background-color: #EBE4D3">{{ items['annotations']['summary'] }}</td>
<td style="font-size: 16px; padding: 3px; word-wrap: break-word; background-color: #EBE4D3">{{ items['annotations']['description'] }}</td>
<td style="font-size: 16px; padding: 3px; word-wrap: break-word; background-color: #EBE4D3">{{ items['startsAt'] }}</td>
{% if items['endsAt'] == '0001-01-01T00:00:00Z' %}
<td style="font-size: 16px; padding: 3px; word-wrap: break-word; background-color: #EBE4D3">00:00:00:00</td>
{% else %}
<td style="font-size: 16px; padding: 3px; word-wrap: break-word; background-color: #3DE869">{{ items['endsAt'] }}</td>
{% endif %}
</tr>
{% endfor %}
</table>
</body>
</html>
en。。。。仔細一看好像也挺簡單的,就是一堆 for 循環,if 判斷啥的,比較不好弄的就是這個表格的合並單元格,對我來說有點費勁,我就簡單把監控類別給合並成一個單元格了,其他的就沒再歸類了
<tr>...</tr>
這里設置的是表格的表頭信息,我這里都有詳細的注釋,就不介紹了。
<td>...</td>
里是一行一行的告警信息,里面有一個判斷,是判斷這一條告警信息里到底是報警還是已恢復,然后根據不同來設置一個不同的顏色展示,這樣的話領導看了肯定會覺着真貼心。
然后我就說一個比較重要的地方
{% for items in prometheus_monitor_info['alerts'] %}
這里面是最關鍵的告警信息,其中prometheus_monitor_info這個是一個變量吧,代表的是把prometheus推過來的json字符串轉換成python的一個字典,注意這是一個字典,然后這個字典做了一個時區轉換的操作。
嗯,那prometheus_monitor_info['alerts']這里就是取得alerts這個列表了,然后用for循環迭代這個列表,items這里就是每一條具體的告警信息,它是一個字典,嗯,然后就是把字典里的value取出來了,嗯。仔細想想也很簡單。
{% endfor %}
這樣的話,我這個 html 的模板就寫好了,然后我怎么使用這個模板呢?這里我又寫了一個方法來解析這個模板,並傳入對應的參數
from jinja2 import Environment, FileSystemLoader
class ParseingTemplate:
def __init__(self, templatefile):
self.templatefile = templatefile
def template(self, **kwargs):
try:
env = Environment(loader=FileSystemLoader('templates'))
template = env.get_template(self.templatefile)
template_content = template.render(kwargs)
return template_content
except Exception as error:
raise error
簡單說下這個類的作用,就是為了傳入告警信息,然后再讀取 html 模板,最后把解析好的 html 內容返回出來,最后通過郵件,把這個內容發出去,就完事了。
精准告警,對應到具體的人
這里其實比較簡單,只需要解析原始 json 里的commonLabels
下的team
,如果你仔細看我上面貼的那個 rule 報警規則的話,你肯定注意到里面有一個自定義的 key-value:
groups:
- name: sentry # 這個名字可以理解為一個分類,做一個區分
rules:
- alert: "Memory Usage"
expr: round((1-(node_memory_MemAvailable_bytes{job='sentry'} / (node_memory_MemTotal_bytes{job='sentry'})))* 100) > 85
for: 5m
labels:
team: ops # 就是這里,我定義了一個組,用來給這個組發消息
severity: warning
cloud: yizhuang
......
......
然后我再解析原始 json 的時候,我把這個team
的值獲取出來,根據這個值,去取這個組里的具體郵件地址,最后發給這些人就好了。
具體的郵件地址,我是取出來了,但是我怎么知道區分這些人應該對應哪個環境或者哪個應用呢,那就是下面這個:
groups:
- name: sentry
......
......
這里的 name 肯定和 prometheus 中指定的 job_name 對應,那么 prometheus 中相應的配置就是:
# cat prometheus.yml
global:
scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
# scrape_timeout is set to the global default (10s).
# Alertmanager configuration
alerting:
alertmanagers:
- static_configs:
- targets: ['10.127.92.105:9093']
# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
- "/alertmanager/rule/*.yaml"
# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
- job_name: 'prometheus'
static_configs:
- targets: ['10.127.92.105:9090']
- job_name: 'cadvisor-app'
file_sd_configs:
- refresh_interval: 1m
files:
- /etc/prometheus/file-sd-configs/cadvisor-metrics.json
- job_name: 'sentry'
file_sd_configs:
- refresh_interval: 1m
files:
- /etc/prometheus/file-sd-configs/system-metrics.json
- job_name: 'kafka-monitor'
file_sd_configs:
- refresh_interval: 1m
files:
- /etc/prometheus/file-sd-configs/kafka-metrics.json
是不是串起來了呢?可以回想下,然后再參考我最終完整的代碼
完整代碼參考
代碼參考
from flask import Flask, request
from dateutil import parser
import json
import yaml
import datetime
import smtplib
from email.mime.text import MIMEText
from jinja2 import Environment, FileSystemLoader
from gevent.pywsgi import WSGIServer
def time_zone_conversion(utctime):
format_time = parser.parse(utctime).strftime('%Y-%m-%dT%H:%M:%SZ')
time_format = datetime.datetime.strptime(format_time, "%Y-%m-%dT%H:%M:%SZ")
return str(time_format + datetime.timedelta(hours=8))
def get_email_conf(file, email_name=None, action=0):
"""
:param file: yaml格式的文件類型
:param email_name: 發送的郵件列表名
:param action: 操作類型,0: 查詢收件人的郵件地址列表, 1: 查詢收件人的列表名稱, 2: 獲取郵件賬號信息
:return: 根據action的值,返回不通的數據結構
"""
try:
with open(file, 'r', encoding='utf-8') as fr:
read_conf = yaml.safe_load(fr)
if action == 0:
for email in read_conf['email']:
if email['name'] == email_name:
return email['receive_addr']
else:
print("%s does not match for %s" % (email_name, file))
else:
print("No recipient address configured")
elif action == 1:
return [items['name'] for items in read_conf['email']]
elif action == 2:
return read_conf['send']
except KeyError:
print("%s not exist" % email_name)
exit(-1)
except FileNotFoundError:
print("%s file not found" % file)
exit(-2)
except Exception as e:
raise e
def sendEmail(title, content, receivers=None):
if receivers is None:
receivers = ['chenf-o@glodon.com']
send_dict = get_email_conf('email.yaml', action=2)
mail_host = send_dict['smtp_host']
mail_user = send_dict['send_user']
mail_pass = send_dict['send_pass']
sender = send_dict['send_addr']
msg = MIMEText(content, 'html', 'utf-8')
msg['From'] = "{}".format(sender)
msg['To'] = ",".join(receivers)
msg['Subject'] = title
try:
smtpObj = smtplib.SMTP_SSL(mail_host, 465)
smtpObj.login(mail_user, mail_pass)
smtpObj.sendmail(sender, receivers, msg.as_string())
print('mail send successful.')
except smtplib.SMTPException as e:
print(e)
class ParseingTemplate:
def __init__(self, templatefile):
self.templatefile = templatefile
def template(self, **kwargs):
try:
env = Environment(loader=FileSystemLoader('templates'))
template = env.get_template(self.templatefile)
template_content = template.render(kwargs)
return template_content
except Exception as error:
raise error
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def webhook():
try:
prometheus_data = json.loads(request.data)
# 時間轉換,轉換成東八區時間
for k, v in prometheus_data.items():
if k == 'alerts':
for items in v:
if items['status'] == 'firing':
items['startsAt'] = time_zone_conversion(items['startsAt'])
else:
items['startsAt'] = time_zone_conversion(items['startsAt'])
items['endsAt'] = time_zone_conversion(items['endsAt'])
team_name = prometheus_data["commonLabels"]["team"]
generate_html_template_subj = ParseingTemplate('email_template_firing.html')
html_template_content = generate_html_template_subj.template(
prometheus_monitor_info=prometheus_data
)
# 獲取收件人郵件列表
email_list = get_email_conf('email.yaml', email_name=team_name, action=0)
sendEmail(
'Prometheus Monitor',
html_template_content,
receivers=email_list
)
return "prometheus monitor"
except Exception as e:
raise e
if __name__ == '__main__':
WSGIServer(('0.0.0.0', 5000), app).serve_forever()
配置文件參考
send:
smtp_host: smtp.163.com
send_user: warxxxxgs@163.com
send_addr: warxxxs@163.com
send_pass: BRxxxxxxxZPUZEK
email:
- name: kafka-monitor # 要和team對應
receive_addr:
- 郵件地址1
- 郵件地址2
- 郵件地址3
- name: ops
receive_addr:
- 郵件地址1
- 郵件地址2
最終效果圖
1)全是告警的
2)既有告警又有恢復的
3)都是恢復的
歡迎各位朋友關注我的公眾號,來一起學習進步哦