alerter.py 2.3 KB

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