prometheus中使用python手寫(xiě)webhook完成告警

prometheus 的幾種告警方式

prometheus 我們都知道它是最近幾年特別火的一個(gè)開(kāi)源的監(jiān)控工具良价,原生支持 kubernetes辣吃,如果你使用的是 kubernetes 集群,那么使用 prometheus 將會(huì)是非常方便的仰猖,而且 prometheus 也提供了報(bào)警工具alertmanager捏肢,實(shí)際上在 prometheus 的架構(gòu)中,告警能力是單獨(dú)的一部分饥侵,主要是通過(guò)自定義一堆的rule即告警規(guī)則鸵赫,來(lái)周期性的對(duì)告警規(guī)則進(jìn)行計(jì)算,并且會(huì)根據(jù)設(shè)置的報(bào)警觸發(fā)條件躏升,如果滿足辩棒,就會(huì)進(jìn)行告警,也就是會(huì)向alertmanager發(fā)送告警信息膨疏,進(jìn)而由alertmanager進(jìn)行告警一睁。
那么,alertmanager告警又是通過(guò)何種途徑呢佃却?其實(shí)有很多種方式者吁,例如:

  • 郵件告警
  • 企業(yè)微信告警
  • 釘釘告警
  • slack 告警
  • webhook 接口方式告警

其實(shí)還有一些,但這些都不重要饲帅,這些只是工具复凳,重要的是如何運(yùn)用,下面就介紹下使用 webhook 的方式來(lái)讓 alertmanager 調(diào)用接口洒闸,發(fā)送POST請(qǐng)求完成告警消息的推送染坯,而這個(gè)推送可以是郵件,也可以是微信丘逸,釘釘?shù)取?/p>

調(diào)用接口以郵件形式告警

大體流程是這樣的单鹿,首先在我們定義好一堆告警規(guī)則之后,如果觸發(fā)條件深纲,alertmanager 會(huì)將報(bào)警信息推送給接口仲锄,然后我們的這個(gè)接口會(huì)做一些類似與聚合劲妙、匯總、優(yōu)化的一些操作儒喊,然后將處理過(guò)的報(bào)警信息再以郵件的形式發(fā)送給指定的人或者組镣奋。也就是下面這個(gè)圖:


image

我們這里的重點(diǎn)主要是如何寫(xiě)這個(gè) webhook,以及寫(xiě) webhook 的時(shí)候需要注意什么怀愧?下面將一一講解

假設(shè)你有一個(gè) prometheus 監(jiān)控系統(tǒng)侨颈,并且告警規(guī)則都已配置完成

配置 alertmanager

首先得先配置 alertmanager,讓其可以調(diào)用接口芯义,配置方式很簡(jiǎn)單哈垢,只需要指定一下接口地址即可,如下:

receivers:
- webhook_configs:
  url: http://10.127.34.107:5000/webhook
  send_resolved: true

這就完了扛拨!當(dāng)然可以指定多種告警方式
這樣配置完成后耘分,alertmanger 就會(huì)把告警信息以 POST 請(qǐng)求方式調(diào)用接口

編寫(xiě)一個(gè)最簡(jiǎn)單的接口

既然是用 python 來(lái)編寫(xiě)一個(gè)接口,那么肯定是用 flask 的绑警,代碼也非常簡(jiǎn)單求泰,如下:

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()

上面導(dǎo)入的一些模塊,記得要去下載哦

pip install flask
pip install gevent

這樣的話计盒,我們直接運(yùn)行此段代碼渴频,此時(shí)機(jī)器上會(huì)監(jiān)聽(tīng) 5000 端口,如果此時(shí) prometheus 有告警北启,那么我們就會(huì)看到 prometheus 傳過(guò)來(lái)的數(shù)據(jù)格式是什么樣的了枉氮,這里我貼一個(gè)示例:

{
    'receiver': 'webhook',
    'status': 'firing',
    'alerts': [{
        'status': 'firing',
        'labels': {
            'alertname': '內(nèi)存使用率',
            'instance': '10.127.92.100',
            'job': 'sentry',
            'severity': 'warning',
            'team': 'ops'
        },
        'annotations': {
            'description': '內(nèi)存使用率已超過(guò)55%,內(nèi)存使用率:58%',
            'summary': '內(nèi)存使用率'
        },
        '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': '內(nèi)存使用率',
            'instance': '10.127.92.101',
            'job': 'sentry',
            'severity': 'warning',
            'team': 'ops'
        },
        'annotations': {
            'description': '內(nèi)存使用率已超過(guò)55%暖庄,內(nèi)存使用率:58%',
            'summary': '內(nèi)存使用率'
        },
        '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': '內(nèi)存使用率'
    },
    'commonLabels': {
        'alertname': '內(nèi)存使用率',
        'job': 'sentry',
        'severity': 'warning',
        'team': 'ops'
    },
    'commonAnnotations': {
        'summary': '內(nèi)存使用率'
    },
    'externalURL': 'http://alertmanager-server:9093',
    'version': '4',
    'groupKey': '{}:{alertname="內(nèi)存使用率"}',
    'truncatedAlerts': 0
}

通過(guò) prometheus 傳過(guò)來(lái)的告警信息,可以看到是一個(gè)標(biāo)準(zhǔn)的json楼肪,我們?cè)谑褂?code>python在做處理時(shí)培廓,需要先將json字符串轉(zhuǎn)換成python的字典,可以用json這個(gè)模塊來(lái)實(shí)現(xiàn)春叫,通過(guò)這個(gè)json我們可以得到以下信息(非常重要):

  • 每次發(fā)出的json數(shù)據(jù)流中的報(bào)警信息是同一個(gè)類型的報(bào)警肩钠,比如這里都是關(guān)于內(nèi)存的
  • status:表示告警的狀態(tài),兩種:firingresolved
  • alerts:是一個(gè)列表暂殖,里面的元素是由字典組成价匠,每一個(gè)元素都是一條具體的告警信息
  • commonLabels:這里面就是一些公共的信息

剩下的幾個(gè) key 都比較好理解,就不一一說(shuō)了呛每,下面結(jié)合 prometheus 的一些 rule 來(lái)看下這個(gè)告警是憑什么這樣發(fā)的踩窖。

# cat system-rule.yaml  #文件名隨意設(shè)置,因?yàn)閜rometheus的配置里配置的是: *.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 }}%"

這里就是配置的告警規(guī)則晨横,告訴 prometheus 應(yīng)該按照什么方式進(jìn)行告警洋腮,配置完成后箫柳,要在 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"  #文件目錄隨意設(shè)置
...
...
...
此處省略一堆配置

到這里應(yīng)該就知道告警規(guī)則是什么發(fā)出來(lái)的了吧悯恍,然后也應(yīng)該知道告警內(nèi)容為什么是這樣的了吧,嗯伙狐,下面看下最關(guān)鍵的地方

處理原始告警信息并進(jìn)行郵件告警

原始的告警信息看起來(lái)還挺規(guī)則的涮毫,只需要拼接下就可以了,但是有一個(gè)問(wèn)題就是alerts里面的startsAtendsAt這倆時(shí)間格式有些問(wèn)題贷屎,是 UTC 時(shí)區(qū)的時(shí)間罢防,需要轉(zhuǎn)換下。還有一個(gè)地方需要注意的豫尽,最外層的status如果是firing狀態(tài)篙梢,就不代表alerts中的status就一定都是firing,還有可能是resolved,如下json所示:

{
    'receiver': 'webhook',
    'status': 'firing',
    'alerts': [{
        'status': 'resolved',  # 這里就是resolved狀態(tài)美旧,所以處理時(shí)需要注意下
        'labels': {
            'alertname': 'CPU使用率',
            'instance': '10.127.91.26',
            'severity': 'warning',
            'team': 'ops'
        },
        'annotations': {
            'description': 'CPU使用率已超過(guò)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使用率已超過(guò)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
}

那既然該注意的都注意了榴嗅,就開(kāi)始干吧妄呕,首先說(shuō)下我要實(shí)現(xiàn)的一個(gè)最終結(jié)果:

  • 時(shí)區(qū)轉(zhuǎn)換
  • 不同類型的告警信息推送給不同的人
  • 告警內(nèi)容以表格的形式展示,通過(guò) html 實(shí)現(xiàn)

時(shí)區(qū)轉(zhuǎn)換

先看下時(shí)區(qū)轉(zhuǎn)換嗽测,這個(gè)比較好解決绪励,代碼如下:

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))

發(fā)送郵件

再來(lái)看下郵件發(fā)送,也很簡(jiǎn)單唠粥,代碼如下:

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)

告警模板生成

下面就是告警推送的形式了疏魏,上面說(shuō)了,使用表格的形式晤愧,如果用 html 來(lái)生成表格大莫,還是比較簡(jiǎn)單的,但是這個(gè)表格是不停的變化的官份,所以為了支持這個(gè)動(dòng)態(tài)變化只厘,肯定是得用到模板語(yǔ)言:jinja了,如果是搞運(yùn)維的肯定知道ansible舅巷,ansible 里的 template 用的也是jinja模板語(yǔ)言羔味,所以比較好理解,這里就不再單獨(dú)說(shuō)了钠右,后面會(huì)詳細(xì)說(shuō)一下 python 中如何使用這個(gè)jinja模板語(yǔ)言赋元,不明白的可以先看下官方文檔,比較簡(jiǎn)單: http://docs.jinkan.org/docs/jinja2/
那么我這個(gè) html 就長(zhǎng)成了這個(gè)樣子,由于本人對(duì)前端一點(diǎn)都不懂们陆,所以能實(shí)現(xiàn)我的需求就行了寒瓦。

<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'] }}--監(jiān)控告警通知</h2><br/>
        <br>
    <table border="1" width = "70%" cellspacing='0' cellpadding='0' align='left'>
    <tr>
        <!--監(jiān)控類型:系統(tǒng)層級(jí),業(yè)務(wù)層級(jí)坪仇,服務(wù)層級(jí)等等-->
        <th style="font-size: 20px; padding: 5px; background-color: #F3AE60">監(jiān)控類別</th>
        <!--狀態(tài):報(bào)警通知還是恢復(fù)通知-->
        <th style="font-size: 20px; padding: 5px; background-color: #F3AE60">狀態(tài)</th>
        <!--狀態(tài):級(jí)別:報(bào)警級(jí)別-->
        <th style="font-size: 20px; padding: 5px; background-color: #F3AE60">級(jí)別</th>
        <!--狀態(tài):實(shí)例:機(jī)器地址-->
        <th style="font-size: 20px; padding: 5px; background-color: #F3AE60">實(shí)例</th>
        <!--狀態(tài):描述:報(bào)警描述-->
        <th style="font-size: 20px; padding: 5px; background-color: #F3AE60">描述</th>
        <!--狀態(tài):詳細(xì)描述:報(bào)警詳細(xì)描述-->
        <th style="font-size: 20px; padding: 5px; background-color: #F3AE60">詳細(xì)描述</th>
        <!--狀態(tài):開(kāi)始時(shí)間:報(bào)警開(kāi)始時(shí)間-->
        <th style="font-size: 20px; padding: 5px; background-color: #F3AE60">開(kāi)始時(shí)間</th>
        <!--狀態(tài):開(kāi)始時(shí)間:報(bào)警結(jié)束時(shí)間-->
        <th style="font-size: 20px; padding: 5px; background-color: #F3AE60">結(jié)束時(shí)間</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">恢復(fù)</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杂腰。。椅文。喂很。仔細(xì)一看好像也挺簡(jiǎn)單的,就是一堆 for 循環(huán)皆刺,if 判斷啥的少辣,比較不好弄的就是這個(gè)表格的合并單元格,對(duì)我來(lái)說(shuō)有點(diǎn)費(fèi)勁羡蛾,我就簡(jiǎn)單把監(jiān)控類別給合并成一個(gè)單元格了漓帅,其他的就沒(méi)再歸類了

<tr>...</tr>這里設(shè)置的是表格的表頭信息,我這里都有詳細(xì)的注釋痴怨,就不介紹了忙干。

<td>...</td>里是一行一行的告警信息,里面有一個(gè)判斷浪藻,是判斷這一條告警信息里到底是報(bào)警還是已恢復(fù)捐迫,然后根據(jù)不同來(lái)設(shè)置一個(gè)不同的顏色展示,這樣的話領(lǐng)導(dǎo)看了肯定會(huì)覺(jué)著真貼心爱葵。

然后我就說(shuō)一個(gè)比較重要的地方

{% for items in prometheus_monitor_info['alerts'] %}
這里面是最關(guān)鍵的告警信息施戴,其中prometheus_monitor_info這個(gè)是一個(gè)變量吧,代表的是把prometheus推過(guò)來(lái)的json字符串轉(zhuǎn)換成python的一個(gè)字典萌丈,注意這是一個(gè)字典赞哗,然后這個(gè)字典做了一個(gè)時(shí)區(qū)轉(zhuǎn)換的操作。

嗯辆雾,那prometheus_monitor_info['alerts']這里就是取得alerts這個(gè)列表了懈玻,然后用for循環(huán)迭代這個(gè)列表,items這里就是每一條具體的告警信息乾颁,它是一個(gè)字典,嗯艺栈,然后就是把字典里的value取出來(lái)了英岭,嗯。仔細(xì)想想也很簡(jiǎn)單湿右。
{% endfor %}

這樣的話诅妹,我這個(gè) html 的模板就寫(xiě)好了,然后我怎么使用這個(gè)模板呢?這里我又寫(xiě)了一個(gè)方法來(lái)解析這個(gè)模板吭狡,并傳入對(duì)應(yīng)的參數(shù)

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

簡(jiǎn)單說(shuō)下這個(gè)類的作用尖殃,就是為了傳入告警信息,然后再讀取 html 模板划煮,最后把解析好的 html 內(nèi)容返回出來(lái)送丰,最后通過(guò)郵件,把這個(gè)內(nèi)容發(fā)出去弛秋,就完事了器躏。

精準(zhǔn)告警,對(duì)應(yīng)到具體的人

這里其實(shí)比較簡(jiǎn)單蟹略,只需要解析原始 json 里的commonLabels下的team登失,如果你仔細(xì)看我上面貼的那個(gè) rule 報(bào)警規(guī)則的話,你肯定注意到里面有一個(gè)自定義的 key-value:

groups:
    - name: sentry  # 這個(gè)名字可以理解為一個(gè)分類挖炬,做一個(gè)區(qū)分
      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   # 就是這里揽浙,我定義了一個(gè)組,用來(lái)給這個(gè)組發(fā)消息
          severity: warning
          cloud: yizhuang
......
......

然后我再解析原始 json 的時(shí)候意敛,我把這個(gè)team的值獲取出來(lái)馅巷,根據(jù)這個(gè)值,去取這個(gè)組里的具體郵件地址空闲,最后發(fā)給這些人就好了令杈。
具體的郵件地址,我是取出來(lái)了碴倾,但是我怎么知道區(qū)分這些人應(yīng)該對(duì)應(yīng)哪個(gè)環(huán)境或者哪個(gè)應(yīng)用呢逗噩,那就是下面這個(gè):

groups:
    - name: sentry
......
......

這里的 name 肯定和 prometheus 中指定的 job_name 對(duì)應(yīng),那么 prometheus 中相應(yīng)的配置就是:

# 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

是不是串起來(lái)了呢跌榔?可以回想下异雁,然后再參考我最終完整的代碼

完整代碼參考

代碼參考

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: 發(fā)送的郵件列表名
    :param action: 操作類型,0: 查詢收件人的郵件地址列表, 1: 查詢收件人的列表名稱, 2: 獲取郵件賬號(hào)信息
    :return: 根據(jù)action的值僧须,返回不通的數(shù)據(jù)結(jié)構(gòu)
    """
    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)
        # 時(shí)間轉(zhuǎn)換纲刀,轉(zhuǎn)換成東八區(qū)時(shí)間
        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對(duì)應(yīng)
    receive_addr:
      - 郵件地址1
      - 郵件地址2
      - 郵件地址3
  - name: ops
    receive_addr:
      - 郵件地址1
      - 郵件地址2

最終效果圖

1)全是告警的

image-20210130220201242
image-20210130220627475

2)既有告警又有恢復(fù)的

image-20210130220350608

3)都是恢復(fù)的

image-20210130220508751
image-20210130220546362
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市担平,隨后出現(xiàn)的幾起案子示绊,更是在濱河造成了極大的恐慌,老刑警劉巖暂论,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件面褐,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡取胎,警方通過(guò)查閱死者的電腦和手機(jī)展哭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門湃窍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人匪傍,你說(shuō)我怎么就攤上這事您市。” “怎么了役衡?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵茵休,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我映挂,道長(zhǎng)泽篮,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任柑船,我火速辦了婚禮帽撑,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘鞍时。我一直安慰自己亏拉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布逆巍。 她就那樣靜靜地躺著及塘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪锐极。 梳的紋絲不亂的頭發(fā)上笙僚,一...
    開(kāi)封第一講書(shū)人閱讀 49,185評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音灵再,去河邊找鬼肋层。 笑死,一個(gè)胖子當(dāng)著我的面吹牛翎迁,可吹牛的內(nèi)容都是我干的栋猖。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼汪榔,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼蒲拉!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起痴腌,我...
    開(kāi)封第一講書(shū)人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤雌团,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后士聪,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體辱姨,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年戚嗅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡懦胞,死狀恐怖替久,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情躏尉,我是刑警寧澤蚯根,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站胀糜,受9級(jí)特大地震影響颅拦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜教藻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一距帅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧括堤,春花似錦碌秸、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至轧抗,卻和暖如春恩敌,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背横媚。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工纠炮, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人分唾。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓抗碰,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親绽乔。 傳聞我的和親對(duì)象是個(gè)殘疾皇子弧蝇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容