prometheus 咱們都知道它是最近幾年特別火的一個開源的監控工具,原生支持 kubernetes,若是你使用的是 kubernetes 集羣,那麼使用 prometheus 將會是很是方便的,並且 prometheus 也提供了報警工具alertmanager
,實際上在 prometheus 的架構中,告警能力是單獨的一部分,主要是經過自定義一堆的rule
即告警規則,來週期性的對告警規則進行計算,而且會根據設置的報警觸發條件,若是知足,就會進行告警,也就是會向alertmanager
發送告警信息,進而由alertmanager
進行告警。
那麼,alertmanager
告警又是經過何種途徑呢?其實有不少種方式,例如:html
其實還有一些,但這些都不重要,這些只是工具,重要的是如何運用,下面就介紹下使用 webhook 的方式來讓 alertmanager 調用接口,發送POST
請求完成告警消息的推送,而這個推送能夠是郵件,也能夠是微信,釘釘等。前端
大致流程是這樣的,首先在咱們定義好一堆告警規則以後,若是觸發條件,alertmanager 會將報警信息推送給接口,而後咱們的這個接口會作一些相似與聚合、彙總、優化的一些操做,而後將處理過的報警信息再以郵件的形式發送給指定的人或者組。也就是下面這個圖: node
咱們這裏的重點主要是如何寫這個 webhook,以及寫 webhook 的時候須要注意什麼?下面將一一講解python
假設你有一個 prometheus 監控系統,而且告警規則都已配置完成web
首先得先配置 alertmanager,讓其能夠調用接口,配置方式很簡單,只須要指定一下接口地址便可,以下:json
receivers:
- webhook_configs:
url: http://10.127.34.107:5000/webhook
send_resolved: true
複製代碼
這就完了!固然能夠指定多種告警方式 這樣配置完成後,alertmanger 就會把告警信息以 POST 請求方式調用接口flask
既然是用 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()
複製代碼
上面導入的一些模塊,記得要去下載哦markdown
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
}
複製代碼
那既然該注意的都注意了,就開始幹吧,首先說下我要實現的一個最終結果:
先看下時區轉換,這個比較好解決,代碼以下:
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)都是恢復的