Prometheus使用钉钉Webhook推送告警通知
构建钉钉Webhook镜像
代码依赖文件:requirements.txt
certifi==2018.10.15
chardet==3.0.4
Click==7.0
Flask==1.0.2
idna==2.7
itsdangerous==1.1.0
Jinja2==2.10
MarkupSafe==1.1.0
requests==2.20.1
urllib3==1.24.1
Werkzeug==0.15.3
钉钉Webhook代码示例文件:app.py
import os
import json
import logging
import requests
import time
import hmac
import hashlib
import base64
import urllib.parse
from urllib.parse import urlparse
from flask import Flask
from flask import request
app = Flask(__name__)
logging.basicConfig(
level=logging.DEBUG if os.getenv('LOG_LEVEL') == 'debug' else logging.INFO,
format='%(asctime)s %(levelname)s %(message)s')
@app.route('/', methods=['POST', 'GET'])
def send():
if request.method == 'POST':
post_data = request.get_data()
app.logger.debug(post_data)
send_alert(json.loads(post_data))
return 'Success!'
else:
return 'Weclome to use dingtalk webhook server!'
def send_alert(data):
token = os.getenv('ROBOT_TOKEN')
secret = os.getenv('ROBOT_SECRET')
if not token:
app.logger.error('You must set ROBOT_TOKEN env!')
return
if not secret:
app.logger.error('You must set ROBOT_SECRET env!')
return
timestamp = int(round(time.time() * 1000))
url = 'https://oapi.dingtalk.com/robot/send?access_token=%s×tamp=%d&sign=%s' % (token, timestamp, make_sign(timestamp, secret))
alerts = data['alerts']
alert_name = alerts[0]['labels']['alertname']
def _mark_item(alert):
labels = alert['labels']
annotations = "> "
for k, v in alert['annotations'].items():
annotations += "{0}: {1}\n".format(k, v)
if 'job' in labels:
mark_item = "\n> job: " + labels['job'] + '\n\n' + annotations + '\n'
else:
mark_item = "\n> " + annotations + '\n'
return mark_item
title = '[云监控] %s 有 %d 条新的报警' % (alert_name, len(alerts))
external_url = alerts[0]['generatorURL']
prometheus_url = os.getenv('PROME_URL')
if prometheus_url:
res = urlparse(external_url)
external_url = external_url.replace(res.netloc, prometheus_url)
send_data = {
"msgtype": "markdown",
"markdown": {
"title": title,
"text": title + "\n" + _mark_item(alerts[0]) + "\n" + "[>>点击查看完整信息](" + external_url + ")\n"
}
}
req = requests.post(url, json=send_data)
result = req.json()
if result['errcode'] != 0:
app.logger.error('notify dingtalk error: %s' % result['errcode'])
def make_sign(timestamp, secret):
"""新版钉钉更新了安全策略,这里我们采用签名的方式进行安全认证。钉钉开发文档地址如下:
https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq"""
secret_enc = bytes(secret, 'utf-8')
string_to_sign = '{}\n{}'.format(timestamp, secret)
string_to_sign_enc = bytes(string_to_sign, 'utf-8')
hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
return sign
if __name__ == '__main__':
app.debug=False
app.run(host='0.0.0.0', port=5000)
脚本详细解释:
import os
import json
import logging
import requests
import time
import hmac
import hashlib
import base64
import urllib.parse
from urllib.parse import urlparse
# 导入Flask模块
from flask import Flask
from flask import request
"""第一部分,初始化:
所有的Flask都必须创建程序实例,web服务器使用wsgi协议,把客户端所有的请求都转发给这个程序实例。
程序实例是Flask的对象,一般情况下用如下方法实例化。
Flask类只有一个必须指定的参数,即程序主模块或者包的名字,__name__是系统变量,该变量指的是本py文件的文件名。"""
app = Flask(__name__)
"""日志配置部分:
这里判断系统环境变量LOG_LEVEL的值,如果为debug,则日志输出级别为DEBUG(即输出所有级别日志),其他值为INFO级别。
通过自定义日志格式,使日志输出更美观。"""
logging.basicConfig(
level=logging.DEBUG if os.getenv('LOG_LEVEL') == 'debug' else logging.INFO,
format='%(asctime)s %(levelname)s %(message)s')
"""第二部分,路由和视图函数:
客户端发送url给web服务器,web服务器将url转发给flask程序实例,
程序实例需要知道对于每一个url请求启动哪一部分代码,所以保存了一个url和python函数的映射关系。
处理url和函数之间关系的程序,称为路由。
在flask中,定义路由最简便的方式,是使用程序实例的app.route装饰器,把装饰的函数注册为路由。
这里请求的url地址为"/",允许的请求方式为POST和GET,类型为可迭代对象,请求方式多达八种。"""
@app.route('/', methods=['POST', 'GET'])
def send():
if request.method == 'POST':
# 如果请求方式为POST,则获取未经处理过的原始数据而不管内容类型。如果数据格式是json的,则取得的是json字符串,排序和请求参数一致。
post_data = request.get_data()
# 将获取的原始数据输出到DEBUG级别日志。
app.logger.debug(post_data)
# 将获取的原始数据类型由str转换为dict,并以此为参数,调用send_alert函数发送告警信息。
send_alert(json.loads(post_data))
return 'Success!'
else:
return 'Weclome to use dingtalk webhook server!'
"""发送告警信息函数。"""
def send_alert(data):
# 获取系统环境变量的值。
token = os.getenv('ROBOT_TOKEN')
secret = os.getenv('ROBOT_SECRET')
if not token:
# 如果值为空,则输出ERROR级别日志。
app.logger.error('You must set ROBOT_TOKEN env!')
return
if not secret:
app.logger.error('You must set ROBOT_SECRET env!')
return
# 默认情况下python的时间戳是以秒为单位输出的float,通过把秒转换毫秒的方法获得13位的时间戳,round()是四舍五入。
timestamp = int(round(time.time() * 1000))
# 把token、timestamp和签名值拼接到URL中。签名值由make_sign函数计算得到。
url = 'https://oapi.dingtalk.com/robot/send?access_token=%s×tamp=%d&sign=%s' % (token, timestamp, make_sign(timestamp, secret))
# 获取告警列表。
alerts = data['alerts']
# 从第一条记录获取告警名称。
alert_name = alerts[0]['labels']['alertname']
"""获取告警摘要信息函数。"""
def _mark_item(alert):
# 获取告警记录的label列表。
labels = alert['labels']
# 初始化变量的值。
annotations = "> "
# Python字典items()方法以列表返回可遍历的(键, 值)元组数组。
for k, v in alert['annotations'].items():
# 格式化并拼接获取到的键值对。
annotations += "{0}: {1}\n".format(k, v)
if 'job' in labels:
# 如果存在job的label,则将其拼接到摘要信息。
mark_item = "\n> job: " + labels['job'] + '\n\n' + annotations + '\n'
else:
mark_item = "\n> " + annotations + '\n'
return mark_item
# 拼接告警标题,len函数获取告警总数。
title = '[云监控] %s 有 %d 条新的告警' % (alert_name, len(alerts))
# 获取告警记录的generatorURL。
external_url = alerts[0]['generatorURL']
# 获取外部访问prometheus的URL地址。
prometheus_url = os.getenv('PROME_URL')
if prometheus_url:
# urlparse解析URL,返回元组 (scheme, netloc, path, parameters, query, fragment)。
res = urlparse(external_url)
# 替换netloc部分的值,即主机地址。
external_url = external_url.replace(res.netloc, prometheus_url)
"""定义要发送的消息,数据格式为markdown类型。"""
send_data = {
"msgtype": "markdown",
"markdown": {
"title": title,
"text": title + "\n" + _mark_item(alerts[0]) + "\n" + "[>>点击查看完整信息](" + external_url + ")\n"
}
}
# 使用requests的post方法发送消息数据,json参数会自动将字典类型的对象转换为json格式。
req = requests.post(url, json=send_data)
# 将发送出的消息转换为json格式。
result = req.json()
if result['errcode'] != 0:
# 如果errcode的值为非0,则输出相关ERROR级别日志。
app.logger.error('notify dingtalk error: %s' % result['errcode'])
"""签名计算函数。"""
def make_sign(timestamp, secret):
"""新版钉钉更新了安全策略,这里采用签名的方式进行安全认证。钉钉开发文档地址如下:
https://ding-doc.dingtalk.com/doc#/serverapi2/qf2nxq"""
# 将secret进行utf-8编码。
secret_enc = bytes(secret, 'utf-8')
# 格式化timestamp和secret。
string_to_sign = '{}\n{}'.format(timestamp, secret)
# 对格式化后的string进行utf-8编码。
string_to_sign_enc = bytes(string_to_sign, 'utf-8')
# 采用SHA256进行哈希计算,digest()返回摘要,作为二进制数据字符串值。
hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
# urllib.parse.quote_plus()编码了斜线,b64encode()对hmac_code进行Base64编码。
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
return sign
"""第三部分:程序实例用run方法启动flask集成的【开发web服务器】。
__name__ == '__main__'是python常用的方法,表示只有直接启动本脚本时,才用app.run方法。
如果是其他脚本调用本脚本,程序假定父级脚本会启用不同的服务器,因此不用执行app.run()。
服务器启动后,会启动轮询,等待并处理请求。轮询会一直请求,直到程序停止。"""
if __name__ == '__main__':
app.debug=False
app.run(host='0.0.0.0', port=5000)
"""如上述代码所示,app是flask的实例,功能就是接受来自web服务器的请求,整个流程如下:
1. 浏览器将请求给web服务器,web服务器将请求给app;
2. app收到请求,通过路由找到对应的视图函数,然后将请求处理,得到一个响应response;
3. 然后app将响应返回给web服务器;
4. web服务器返回给浏览器;
5. 浏览器展示给用户观看。"""
镜像构建模板文件:Dockerfile
FROM python:3.6.4-alpine3.4
MAINTAINER varden
RUN echo "https://mirrors.aliyun.com/alpine/v3.4/main/" > /etc/apk/repositories
RUN echo "https://mirrors.aliyun.com/alpine/v3.4/community/" >> /etc/apk/repositories
RUN apk update
RUN apk upgrade
RUN apk add --no-cache ca-certificates tzdata curl bash && rm -rf /var/cache/apk/*
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo "Asia/Shanghai" > /etc/timezone
WORKDIR /app
COPY requirements.txt /app/requirements.txt
RUN pip install -r requirements.txt -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com
COPY app.py /app/app.py
CMD python app.py
构建命令
docker build -t dingtalk-hook:1.0.1 .
docker-compose部署
version: "3.3"
networks:
bridge_net:
external:
name: bridge
services:
dingtalk_hook:
image: dingtalk-hook:1.0.1
ports:
- "15000:5000"
networks:
- bridge_net
environment:
LOG_LEVEL: debug
PROME_URL: 10.10.10.200:32101
env_file: robot.env
deploy:
mode: replicated
replicas: 1
resources:
limits:
cpus: '0.2'
memory: 128M
reservations:
cpus: '0.1'
memory: 64M
healthcheck:
test: curl -f http://localhost:5000 || exit 1
interval: 30s
timeout: 30s
retries: 5
环境变量文件:robot.env
ROBOT_TOKEN=248fc95536c33cbba9c6d2418d651766a7e8060078a0cffxxxxxxxxxxxxxxxxx
ROBOT_SECRET=SECe76d2fedf79602b265dc103494e1d8e87e7999cbe73xxxxxxxxxxxxxxxxxxxxxxx
K8s部署
创建Secret资源对象
kubectl create secret generic dingtalk-secret --from-literal=token=248fc95536c33cbba9c6d2418d651766a7e8060078a0cff833409xxxxxxxxxxxx --from-literal=secret=SECe76d2fedf79602b265dc103494e1d8e87e7999cbe73badbaa8c3xxxxxxxxxxxx -n monitoring
squid代理访问外网:
kubectl create secret generic proxy-secret \
--from-literal=http_proxy=http://<username>:<password>@10.10.10.15:3128 \
--from-literal=https_proxy=http://<username>:<password>@10.10.10.15:3128 \
-n monitoring
K8s部署清单
apiVersion: apps/v1
kind: Deployment
metadata:
name: dingtalk-hook
namespace: monitoring
spec:
replicas: 1
selector:
matchLabels:
app: dingtalk-hook
template:
metadata:
labels:
app: dingtalk-hook
spec:
containers:
- name: dingtalk-hook
image: dingtalk-hook:1.0.1
imagePullPolicy: IfNotPresent
ports:
- containerPort: 5000
name: http
env:
- name: PROME_URL
value: 10.10.10.200:32101
- name: LOG_LEVEL
value: debug
- name: ROBOT_TOKEN
valueFrom:
secretKeyRef:
name: dingtalk-secret
key: token
- name: ROBOT_SECRET
valueFrom:
secretKeyRef:
name: dingtalk-secret
key: secret
- name: http_proxy
valueFrom:
secretKeyRef:
name: proxy-secret
key: http_proxy
- name: https_proxy
valueFrom:
secretKeyRef:
name: proxy-secret
key: https_proxy
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 100m
memory: 200Mi
livenessProbe:
httpGet:
scheme: HTTP
path: /
port: 5000
initialDelaySeconds: 30
timeoutSeconds: 30
readinessProbe:
httpGet:
scheme: HTTP
path: /
port: 5000
initialDelaySeconds: 30
timeoutSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: dingtalk-hook
namespace: monitoring
spec:
selector:
app: dingtalk-hook
ports:
- name: hook
port: 5000
targetPort: http
在AlertManager中webhook地址直接通过DNS形式访问即可:
receivers:
- name: 'webhook'
webhook_configs:
- url: 'http://dingtalk-hook:5000'
send_resolved: true