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&timestamp=%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&timestamp=%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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM