Prometheus钉钉或微信报警

用来说明Prometheus使用Alertmanager报警到钉钉或微信的实现过程,相关export的安装不再进行说明。

这是一个我根据自己的理解画的流程图,后面我会根据这些来说明:

Prometheus报警流程

Prometheus

prometheus.yml

这是一个示例文件,基于此说明Prometheus的配置:

# my global config
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: ['localhost:9093']
       #- "localhost:9093"

# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
   - "rules/node.yml"
   #- "second_rules.yml"

# 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: ['localhost:9090']

  - job_name: "ENV_TEST_HOST"
    file_sd_configs:
      - files:
        - "/opt/prometheus/target/host_status.json"
        refresh_interval: 1m
    relabel_configs:
    - source_labels:
      - "__address__"
      regex: "(.*):9119"
      target_label: "instance"
      action: replace
      replacement: "$1"

配置主要分为下面四个部分:

  1. global:
    全局配置文件,默认配置了两项

    • scrape_interval:Prometheus从目标抓取指标的频率
    • evaluation_interval:评估规则的频率
  2. alerting:
    配置报警器,对于alertmanager,指定地址即可

  3. rule_files:
    Prometheus 基于此文件进行判断是否报警,可以指定多个,可以使用通配符

  4. scrape_configs:
    对Prometheus抓取数据进行配置,基本分为两类

    • file_sd_configs:定义发现规则,可以是静态定义或者基于文件或者服务或者DNS,不同的发现方式对应不同的关键字,比如静态定义的static_configs

    • relabel_configs:将抓取到的label进行重新配置,label是Prometheus非常重要的特性,这个label来自exporter和target中的配置。可以在Discovered Labels处查看发现到的lables,relabels之后在Target Labels处,此处的label即可被其他程序使用

就我目前所知,采集的指标也就是metric中的信息无法提取添加到labels

target

下面是一个示例配置:

[
    {
        "targets": [
            "192.168.4.247:9119",
            "192.168.5.176:9119"
            ],
        "labels": {
        "job": "env_test_host",
        "app": "a",
        "service": "test_service"
        }
    },
    {
        "targets": [
            "192.168.3.21:9119",
            "192.168.4.172:9119"
            ],
        "labels": {
        "job": "env_test_host",
        "app": "cjdropship",
        "service": "test_service"
        }
    }
]

配置是一个JSON格式的文件,主要分为三部分:

  1. targets:
    Prometheus会通过这里配置的地址发现所要监控的对象,对象可以是exports暴露的端口,也可以是集成Prometheus的程序提供的地址

  2. labels:
    这里是自定义labels的地方,一般用来标识targets的属性。targets中定义的target越少,labels的定义粒度越细。这里此处的定义会在Prometheus抓取的时候添加到Discovered Labels

  3. metrics_path:
    通常是不需要定义的,默认会去/metrics这个URI去查找,如果有不同URI,可以在此处定义

rules

此处是配置报警规则的地方,Prometheus根据这里的定义判断是否需要发送报警信息。下面是一个示例:

groups:
- name: Node_alert
  rules:
  - alert: "实例丢失"
    expr: up == 0
    for: 60s
    labels:
      severity: page
    annotations:
      summary: "Prometheus中断了与 {{ $labels.instance }} 的连接"
      description: "{{ $labels.instance }} 已经失联至少 1 分钟了"

  - alert: "可用内存不足10%"
    expr: ((node_memory_MemTotal_bytes - node_memory_MemFree_bytes - node_memory_Buffers_bytes - node_memory_Cached_bytes) / (node_memory_MemTotal_bytes )) * 100 > 90
    for: 300s
    labels:
      severity: warning
    annotations:
      summary: "节点 {{ $labels.instance }} 可用内存不足"
      description: "{{ $labels.instance }} 内存资源已不足10%"
  1. groups:
    给报警信息分组,根据此信息可以在Alertmanager中定义不同的路由发送给不同的接收者
  2. rules: 具体的规则信息主要有下面几项
    • alert:报警名称,这个信息会随警告发出
    • expr:表达式,通过此语句判断是否需要发出告警
    • for:持续此时间之后发出告警,在此之前告警信息会处于pending状态
    • labels:
      • severity:一般使用此项表示告警的紧急程度,在需要配置告警抑制的时候,此项至关重要,此项也会随报警发送
    • annotations:这是一个自定义选项,甚至可以没有。这里配置的信息会随着告警信息发送,至于summary和description,自定义的,想写点啥就写点啥。

关于变量,在annotations中使用了{{ $labels.instance }}之类的变量,常用的还有{{ $value }},这个是模版语言的引用方式,其中labels中可用的变量来自于target中配置和自动抓取到的label,即在Prometheus页面中通过Target labels查看到的那些。至于value这个来自expr表达式查询出来的结果。

Alertmanager

Prometheus自身并没有报警这个功能,它只能把信息发出来,至于怎么发,这个就需要通过告警组件来实现,对于Prometheus来说,最常用的就是Alertmanager。

alertmanager.yml

这里有一个比较完善的Alertmanager的配置:

global:
  resolve_timeout: 5m

templates: 
- '/etc/alertmanager/template/*.tmpl'

route:
  group_by: ['alertname', 'cluster', 'service']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 3h 
  receiver: web.hook
  
  routes:
  - match_re:
      service: ^(foo1|foo2|baz)$
    receiver: web.hook
    routes:
    - match:
        severity: critical
      receiver: wechat

#  - match:
#      service: files
#    receiver: team-Y-mails
#    routes:
#    - match:
#        severity: critical
#      receiver: team-Y-pager
#
#  - match:
#      service: database
#    receiver: team-DB-pager
#    group_by: [alertname, cluster, database]
#    routes:
#    - match:
#        owner: team-X
#      receiver: team-X-pager
#      continue: true
#    - match:
#        owner: team-Y
#      receiver: team-Y-pager

inhibit_rules:
- source_match:
    severity: 'critical'
  target_match:
    severity: 'warning'
  equal: ['alertname', 'cluster', 'service'] 

receivers:
- name: 'web.hook'
  webhook_configs:
#  - url: 'http://127.0.0.1:8060/dingtalk/ops_dingding/send'
  - url: 'http://192.168.3.53:5000/'
    send_resolved: true

- name: 'wechat'
  wechat_configs:
  - send_resolved: false
    to_user: ''
    to_party: '1'
    agent_id: '1000002'
    corp_id: ''
    api_secret: ''
    message: '{{ template "wechat.default.message" . }}'

这个配置主要包含了下面的五个配置(这个配置文件不可以直接使用):

  1. global:
    全局配置,通常只有一项配置

    • resolve_timeout:经过此时间后,如果尚未更新告警,则将告警声明为已恢复
  2. templates:
    指定发送信息的模版,模版使用的是模版语言编写的tmpl格式的文件,可以使用通配符匹配文件,也可以在一个文件中编写多个模版,在配置receivers的时候,可以指定使用哪个模版

  3. route:
    这里注释了多条路由,定义了多条路由,通常不会又这么麻烦的路由信息。多数情况,发到一两个群。

    • group_by:分组,同一个分组下的告警将聚合为一条信息发送以避免频繁的消息提醒,如果不想使用分组,可以这样写group_by: [...]
    • group_wait:第一组告警发送通知需要等待的时间,这种方式可以确保有足够的时间为同一分组获取多个告警,然后一起触发这个告警信息。
    • group_interval:发送第一个告警后,等待”group_interval”发送一组新告警。
    • repeat_interval:相同的消息在此时间间隔之后再次发送(使用分组这条大概率没啥卵用)
    • receiver:默认接收者
    • routes:子路由配置
      • match_re:匹配出标签含有service=foo1或service=foo2或service=baz的告警
      • receiver:指定接收器
      • routes:二级子路由配置,字段相同
  4. inhibit_rules:
    告警抑制规则,主要用来减少关联报警消息,比如主机A挂了,则只发送A挂了的报警,运行在其上的程序及服务的报警不再发送

    • source_match:源告警,通常配置告警级别
    • target_match:被抑制的告警,通常也是配置告警级别
    • equal:必须在源告警和目标告警中具有相同的等值标签才会生效

      以配置中的示例,其含义是,告警中alertname、cluster、service相同的告警,忽略登记为warning级别的告警,只发送critical级别的。

  5. receivers:
    不同的receivers配置方式略有不同,对于微信来说,这里配置的两个都可以,其中一个是通过Webhook发送到群机器人,里一个直接发送到一个或者多个联系人。对于钉钉来说,它只支持Webhook的方式发送。方式是一样的。其中对于wechat定义中的message即使用的模版。关于模版的定义在下边。

    template

    模版语言这个东西,我也不太会,好像在用Python或者go语言用web框架开发的时候填充前段页面会用这玩意儿,简单解释下:

第一行,定义了一个模版,在alertmanager.yaml中关于wechat的receiver中可以看到对其的引用

第二行是一个循环,将Prometheus发来的消息循环出来

其他的部分就很好理解了,就是将循环出来的变量展示出来,不是变量的,就是纯文本。不过有几个变量我觉得有必要说一下怎么来的,知道之后能更好的自定义:

  • .Status:这个是Prometheus发过来的状态信息,分别是firing和resolved,分别代表正在报警和报警已解除
  • Labels:这个就是target文件和自动发现经过relabel后出现在Prometheus Target labels位置的信息,你定义了什么就可以在这里使用什么
  • Annotations:还记得告警规则里关于Annotaions的定义吗,这就是从那儿来的,同样取决于你怎么定义
  • 时间:故障和恢复时间是报警时Prometheus附加上去的,这里进行了一些转换,就是那个Format,因为它默认使用的不是北京时间
  • end:表示模版结束,同一个文件中似乎可以定义多个这东西,只要在receiver那里正确引用就好了
{{ define "wechat.default.message" }}
{{ range .Alerts }}
========Start=========
告警环境: {{ .Labels.job }}
告警类别: {{ .Labels.app }}
告警状态:{{ .Status }}
告警名称: {{ .Labels.alertname }}
故障主机: {{ .Labels.instance }}
告警详情: {{ .Annotations.description }}
故障时间: {{ (.StartsAt.Add 28800e9).Format "2006-01-02 15:04:05" }}
恢复时间: {{ (.EndsAt.Add 28800e9).Format "2006-01-02 15:04:05" }}
=========End===========
{{ end }}
{{ end }}

发送脚本

发送脚本针对的是Webhook的告警方式,网上这种脚本还挺多的,你也可以自己写,原理就是启动一个web接口,接收来自Alertmanager发送来的报警数据,然后将数据格式化为钉钉或微信要求的数据格式之后发送即可。下面是脚本内容:

wechat

import os
import json
import requests
import arrow

from flask import Flask
from flask import request

app = Flask(__name__)

@app.route('/', methods=['POST', 'GET'])
def send():
    if request.method == 'POST':
        post_data = request.get_data()
        send_alert(bytes2json(post_data))
        return 'success'
    else:
        return 'weclome to use prometheus alertmanager wechat webhook server!'

def bytes2json(data_bytes):
    data = data_bytes.decode('utf8').replace("'", '"')
    return json.loads(data)

def send_alert(data):
    headers = {"Content-Type": "application/json; charset=utf-8"}
    token = ""
    if not token:
        print('you must set ROBOT_TOKEN env')
        return
    url = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=%s' % token
    for output in data['alerts'][:]:
        
        try:
            title = output['labels']['alertname']
        except KeyError:
            title = 'Prometheus Alert'
        
        try:
            message = output['annotations']['message']
        except KeyError:
            try:
                message = output['annotations']['description']
            except KeyError:
                message = 'null'

        send_data = {
            "msgtype": "markdown",
            "markdown": {
                "content": "# <font color=\"comment\">通知类型:</font> <font color=\"warning\">**%s** </font>" % output['status'] + "\n\n" +
                        "> **告警名称**: %s \n" % output['labels']['alertname'] +
#                        "**告警级别**: %s \n\n" % output['labels']['severity'] +
                        "> **告警状态**: %s \n" % output['status'] +
                        "> **告警类别**: %s \n" % output['labels']['app'] +
                        "> **告警主机**: %s \n" % output['labels']['instance'] +
                        "> **告警详情**: %s \n" % message +
                        "> **触发时间**: %s \n" % arrow.get(output['startsAt']).to('Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss ZZ') +
                        "> **触发结束时间**: %s \n" % arrow.get(output['endsAt']).to('Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss ZZ')
            }
        }
        req = requests.post(url, json=send_data, headers = headers)
        result = req.json()
        if result['errcode'] != 0:
            print('notify dingtalk error: %s' % result['errcode'])

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

脚本很简单,使用Flask启动了一个web程序,程序接收到来自Alertmanager的数据之后转换为Json,然后遍历该Json数据,将其构造为想要的格式,然后发送。需要注意下面的东西:

  • 需要将token添加上去,其实也可以不适用token这个变量,直接给完整的URL

  • 和刚才的模版语言一样,这里的JSON能遍历出来什么取决于你在之前定义了什么,根据自己定义的内容调整

  • 找一个不冲突的端口启动

  • Python3写的

    dingtalk

  • 修改token和URL

  • 看官方文档对于发送数据的格式要求,比如Markdown格式钉钉就不支持颜色

  • 钉钉在创建机器人的时候又三个安全选项,其中加签需要使用官方提供的代码计算一个签名,然后将计算出来的结果拼接到URL中,否则发送消息失败。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!