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)都是恢复的