# -*- coding:utf-8 -*- """ 企业微信告警发送。 职责: - 读 conf/alerter.ini 拿 base_url + 按通道名取机器人 key - send_markdown 发企微 markdown 消息 失败一律抛异常,不静默:告警发不出去必须让调度看见,否则问题被双重掩盖。 覆盖四种情况——配置文件缺失、通道未注册、key 还是占位符、企微返回 errcode != 0。 (ConfigParser.read 对缺失文件静默返回,故先显式校验存在性。) 消息拼装由调用方负责:不同 monitor 的标题和字段各不相同,塞进来只会变成参数堆。 """ import json import os import urllib.request from configparser import ConfigParser _PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) _DEFAULT_CONF_PATH = os.path.join(_PROJECT_ROOT, 'conf', 'alerter.ini') _TIMEOUT_SEC = 20 class Alerter: def __init__(self, channel='default', conf_path=None): # type: (str, str) -> None path = conf_path or _DEFAULT_CONF_PATH if not os.path.isfile(path): raise RuntimeError('告警配置文件不存在:' + path) cp = ConfigParser() cp.read(path, encoding='utf-8') if not cp.has_option('channels', channel): raise RuntimeError( "告警通道未注册:'{0}';{1} [channels] 段加 '{0} = '".format(channel, path)) key = cp.get('channels', channel).strip() if key.startswith('<'): raise RuntimeError( "告警通道 '{0}' 的 key 仍是占位符,填真实机器人 key:{1}".format(channel, path)) self.webhook = cp.get('wechat_work', 'base_url').strip() + key def send_markdown(self, content): # type: (str) -> None """发 markdown 消息。企微 markdown 单条上限 4096 字节,超长由调用方自行截断。""" payload = json.dumps({'msgtype': 'markdown', 'markdown': {'content': content}}, ensure_ascii=False).encode('utf-8') req = urllib.request.Request( self.webhook, data=payload, headers={'Content-Type': 'application/json'}) resp = urllib.request.urlopen(req, timeout=_TIMEOUT_SEC).read().decode('utf-8') if json.loads(resp).get('errcode') != 0: raise RuntimeError('企微发送失败:' + resp)