统一通知渠道(notify.py)¶
仓库路径:
deploy/nfos/monitoring/notify.py| 线上路径:/opt/Cy/services/system.d/nfos/monitoring/notify.py语言:python3 | 定时器:无(被调用,不挂 timer)
用途¶
全平台唯一的通知出口模块:把统一告警分发到
- 飞书机器人(告警机器人 / 安全信息机器人 / 汇总通报机器人);
- 邮件(SMTP
smtp.feishu.cn,发件人按通知类别选择统一邮箱之一)。
它不是可独立执行的任务脚本,而是被同目录的三个调用方 import:
| 调用方 | 调用方式 |
|---|---|
monitoring/sec-notify.py |
notify.NotifyConfig.load() + notify.Notifier(cfg),再 dispatch() / render() |
monitoring/ticket-notify.py |
from notify import Notifier(导入失败即非 0 退出) |
monitoring/digest.py |
发送时函数内 import notify |
monitoring/alert-adapter.py |
顶部 from notify import Notifier(失败则「通知不可用」降级,不影响告警主链路) |
两条关键设计(写在源码注释里):
- 「类别 → 通道」是显式路由表
CATEGORY_ROUTING,digest类别单独一条通道(汇总通报专用机器人),与实时安全告警分群,避免值班被汇总刷屏、也避免实时告警被汇总淹没;邮件侧digest沿用安全邮箱。 - 脱敏在
render()出口一处收口:飞书正文、邮件主题、邮件正文全部经sanitize.py。任何异常都原样返回——脱敏是展示层加固,绝不能反过来阻断告警。
何时用¶
- 正常运行时不需要有人碰它:它只被别的脚本调用。
- 想在不改配置的前提下确认「通道到底配了哪些、开关开没开」:直接执行脚本本体(它打印
describe()快照,不含口令,也不发任何消息)。 - 想主动确认通道真的通:
--test(发一条自检消息)。 - 想临时静音某个通道:用环境变量开关(
NOTIFY_FEISHU_ENABLED/NOTIFY_MAIL_ENABLED),不需要改代码。 - 想降低邮件频率:调
NFOS_MAIL_MIN_INTERVAL_<CATEGORY>(值为秒,0= 不限速)。
命令与参数¶
本脚本没有 argparse。 __main__ 里只识别唯一一个字面量 --test:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
--test |
位置参数(os.sys.argv[1] == "--test" 精确比较) |
无(不传则只打印配置) | 发一条自检消息,用于确认飞书与邮件通道可用,无需处理 |
除
--test外没有任何其它 argv 入口(源码未给出);其余行为全部通过环境变量 / 环境文件控制。
不带参数时的默认行为¶
实例化 Notifier() → 打印 cfg.describe() 的 JSON(只输出字段清单与「已配置 / 未配置」标记,不含任何口令)→ 结束。不发任何消息、不写任何状态文件。
可用的环境变量(全部取自 NotifyConfig.load())¶
| 变量 | 作用 | 默认 |
|---|---|---|
NOTIFICATION_ENV |
覆盖凭据文件路径 | /opt/Cy/services/system.d/nfos/secrets/notification.env |
SMTP_HOST / SMTP_PORT / SMTP_SECURITY / SMTP_TIMEOUT_SECS |
SMTP 目标与安全模式 | smtp.feishu.cn / 465 / ssl / 20.0 |
ALERT_TO / ALERT_CC |
收件人 / 抄送 | 空 |
MAIL_TRAP_ADDRESS、MAIL_SECURITY_ADDRESS、MAIL_ONLINE_ADDRESS、MAIL_SERVICE_ADDRESS |
四个类别的发件人 | 空 |
MAIL_TRAP_PASSWORD、MAIL_SECURITY_PASSWORD、MAIL_ONLINE_PASSWORD、MAIL_SERVICE_PASSWORD |
对应发件人口令 | 空(取值见 secrets/notification.env,不得转录) |
FEISHU_ALERT_WEBHOOK / FEISHU_SECURITY_WEBHOOK / FEISHU_DIGEST_WEBHOOK |
三个飞书机器人 webhook | 空(取值见 secrets/notification.env,不得转录) |
NOTIFY_FEISHU_ENABLED / NOTIFY_MAIL_ENABLED |
通道总开关(0 / false / False 视为关) |
1(开) |
NFOS_MAIL_MIN_INTERVAL_<CATEGORY> |
按类别覆盖邮件最小间隔(秒) | 见下表 |
NFOS_MAIL_RATE_LIMIT_COOLDOWN |
被服务商限流后的冷却秒数 | 1800 |
NFOS_MAIL_THROTTLE_STATE |
邮件限速状态文件 | /opt/Cy/services/system.d/nfos/state/notify-mail-throttle.json |
环境变量优先于环境文件;此外任何以 SMTP_ / MAIL_ / ALERT_ / FEISHU_ / NOTIFY_ 开头的进程环境变量都会被吸收进来。
内置路由与限速表(源码常量,非参数)¶
| 类别 | 发件邮箱变量前缀 | 飞书机器人变量 |
|---|---|---|
alert |
MAIL_TRAP |
FEISHU_ALERT_WEBHOOK |
security |
MAIL_SECURITY |
FEISHU_SECURITY_WEBHOOK |
ticket |
MAIL_ONLINE |
FEISHU_ALERT_WEBHOOK |
service |
MAIL_SERVICE |
FEISHU_ALERT_WEBHOOK |
digest |
MAIL_SECURITY |
FEISHU_DIGEST_WEBHOOK |
| 级别 | 飞书 | 邮件 |
|---|---|---|
critical / high / error / warning |
✅ | ✅ |
info / debug |
❌ | ❌(仅日志) |
dispatch(channels=(True, True))可显式覆盖级别默认渠道——ticket-notify.py用它保证「工单状态转变皆有邮件通知」,不因工单级别是info被静默跳过。
| 邮件类别 | 最小间隔(秒) |
|---|---|
default |
600 |
security |
900 |
ticket |
900 |
alert |
600 |
service |
1800 |
digest |
0(不限速) |
示例¶
D=/opt/Cy/services/system.d/nfos
# 1) 只打印配置快照(不发消息、不写状态、不含口令)
sudo python3 "$D/monitoring/notify.py"
# 2) 发一条自检消息,确认飞书与邮件通道可用
sudo python3 "$D/monitoring/notify.py" --test
# 3) 用 API 方式拿同一份快照(供脚本消费)
sudo python3 -c 'import sys; sys.path.insert(0, "/opt/Cy/services/system.d/nfos/monitoring"); \
import notify; print(notify.NotifyConfig.load().describe())'
# 4) 只做「渲染」不发消息:拿到脱敏后的飞书正文与邮件主题/正文
sudo python3 -c 'import sys; sys.path.insert(0, "/opt/Cy/services/system.d/nfos/monitoring"); \
import notify; n = notify.Notifier(); \
print(n.render("security","warning","通道自检","Cy.AliCOS-BJ","SELFTEST-1","正文",{"来源":"手册"}))'
# 5) 临时静音「邮件」通道(飞书保持实时),验证渠道隔离
sudo env NOTIFY_MAIL_ENABLED=0 python3 "$D/monitoring/notify.py" --test
# 6) 应急:把某类别邮件间隔调到 0(不限速)——仅排障期使用
sudo env NFOS_MAIL_MIN_INTERVAL_SECURITY=0 python3 "$D/monitoring/notify.py" --test
更贴近线上链路的通道自检(走真实告警字段、输出更完整)优先用
sudo python3 "$D/monitoring/sec-notify.py" --selftest,见 sec-notify.md。
依赖与权限¶
| 项 | 内容 |
|---|---|
| 解释器 | 任意 python3(零第三方依赖:smtplib + ssl + urllib + json + threading,全部标准库) |
| 同目录模块 | sanitize.py(_sanitize_out() 内函数级自取) |
| 外部命令 | 无 |
| root | 取决于调用场景:要读 secrets/notification.env(mode 600)就必须是 root 或 root 组;被 timer 拉起的调用方(ticket-notify、digest)单元里都是 User=root |
sudo -n |
不需要(本模块自身不调用 sudo) |
| 读取 | secrets/notification.env(600)。凭据取值的唯一来源就是这个文件;本页与该文件之外一律不记录 |
| 写入 | state/notify-mail-throttle.json(原子写:*.tmp → os.replace)。写失败静默忽略(except: pass),不影响发送 |
| 网络 | 出站 HTTPS(飞书 webhook)+ SMTP smtp.feishu.cn:465(SSL,默认超时 20s)。四类发件邮箱都走同一 SMTP 主机 |
| 并发 | send_mail() 用实例级 threading.Lock 串行化 SMTP 会话(alert-adapter.py 是 ThreadingHTTPServer,会并发调它) |
幂等与失败语义¶
- 不做业务去重:
.py层不判断「这条告警是不是发过」。去重由调用方负责(digest.py的last_sent_bucket、ticket-notify.py的状态快照、守卫的dedup_key)。 - 邮件限速是「跳过」而不是「失败」:被限速时
send_mail()返回字符串"skipped"(注意:函数签名标注的是-> bool,实际可能返回"skipped"),并置last_error为原因;调用方按「渠道未启用」处理,不重试、不丢状态。 - 服务商限流有独立更长的冷却:
send_mail()捕获到错误串里含exceeds limit/exceed时,把该类别标记rate_limited_until = now + NFOS_MAIL_RATE_LIMIT_COOLDOWN(默认 1800s),短期不再硬撞。 - 缺配置 = 记
skipped并返回 False:send_mail()在「发件人 / 口令 / 收件人」任一缺失时skipped+last_error,不抛异常。 - 发送异常不向外抛:
send_mail()/send_feishu()内部try/except,失败返回False并递增统计;因此调用方不会被异常打断(这是「缺省行为:未配置渠道时静默降级为仅日志,不影响告警主链路」的落地方式)。 - 状态/统计:模块内存里维护
stats(feishu_ok/feishu_failed/mail_ok/mail_failed/skipped/mail_throttled),并提供metrics_lines()输出 Prometheus 文本(nfos_notify_feishu_total等)。这些计数只活在进程内存里,不落盘。 - 退出码:
__main__不调用sys.exit→ 正常路径恒为 0;只有未捕获异常才会非 0。
常见错误与排查¶
| 现象 | 代码里的对应分支 | 排查步骤 |
|---|---|---|
| 日志只写「渠道未配置」但没人收到消息 | send_mail() 的 if not sender or not pw or not to: → skipped;send_feishu() 的同类分支 |
跑 python3 notify.py 看 describe() 的 senders / feishu_channels / passwords_set / alert_to;全为「未配置」说明 secrets/notification.env 没被读到或权限不对 |
describe() 报 "source": "环境变量" 而不是文件路径 |
cfg.source = SECRETS_FILE if os.path.exists(SECRETS_FILE) else "环境变量" |
说明 NOTIFICATION_ENV 指向的(或默认的)文件不存在;检查路径与 NOTIFICATION_ENV 拼写 |
邮件全丢、飞书正常、日志里有 501 mail count exceeds limit |
send_mail() 的 rate_limited 分支 → 冷却 1800s |
看 state/notify-mail-throttle.json 里该类别的 rate_limited_until;等冷却过期,或临时 NFOS_MAIL_RATE_LIMIT_COOLDOWN 调整;治本靠 MAIL_MIN_INTERVAL 与「汇总由 digest 承担」 |
| 邮件「有时发有时不发」且间隔看起来随机 | _mail_throttle_check() 的常规限速分支 |
按类别核对 MAIL_MIN_INTERVAL(security/ticket 900s、service 1800s、digest 0);需要临时放开用 NFOS_MAIL_MIN_INTERVAL_<CATEGORY> |
| 出站正文里出现内部路径 / 私网 / webhook | _sanitize_out() 的 except 分支(静默失效) |
用上面示例 4 的 render() 直接看渲染结果;若仍含明文,先 python3 "$D/monitoring/sanitize.py" --selftest 确认 sanitize.py 在同目录且可导入(import sanitize 走的是 notify.py 所在目录) |
json.log 里同一件事没有正文行 |
调用方的问题,不在本模块 | 本模块只负责渲染与投递;「字段传了但正文里没有」由 TICKET_FIELD_ALIASES 别名归一覆盖,核对调用方传的键名 |
多线程(alert-adapter.py)下 SMTP 会话互相干扰 |
send_mail() 的 with self._lock: |
已知并已用实例锁串行化;若仍见会话异常,检查是否新建了多个 Notifier 实例(每个实例一把锁) |
回滚方式¶
本模块不修改系统状态、不写配置,唯一的落盘是邮件限速状态文件。回滚路径:
D=/opt/Cy/services/system.d/nfos
BK=$D/state/unit-backups
# 1) 只回滚「限速状态」(例如误把某类别拉黑太久)
TS=$(date +%Y%m%d-%H%M%S)
sudo install -d -m 755 "$BK"
sudo cp -a "$D/state/notify-mail-throttle.json" "$BK/notify-mail-throttle.json.bak-$TS" 2>/dev/null
# 确认后清空(下次发送会重新标记;清空只是「忘记上次发送时间」)
sudo rm -f "$D/state/notify-mail-throttle.json"
# 2) 临时静音通道(不改文件,重启即恢复)
sudo systemctl set-environment NOTIFY_MAIL_ENABLED=0 # 或直接在调用方的 unit 里加 Environment=
sudo systemctl restart nfos-ticket-notify.timer
# 3) 回滚脚本本体
TS=$(date +%Y%m%d-%H%M%S)
sudo cp -a "$D/monitoring/notify.py" "$BK/notify.py.bak-$TS" # 改之前先留一份
sudo cp -a "$BK/notify.py.bak-<时间戳>" "$D/monitoring/notify.py"
sudo chown root:root "$D/monitoring/notify.py"
sudo chmod 644 "$D/monitoring/notify.py"
备份落点铁律:备份放
/opt/Cy/services/system.d/nfos/state/unit-backups/(命名<文件名>.bak-YYYYMMDD-HHMMSS)或仓库侧/opt/Cy/nfarms/.backup/。 绝不能放在/etc/systemd/system/—— 会触发 PS-002 完整性告警。见 timers.md 第 11 节。另注:
docs/CHANGELOG-2026-09-26.md的「§8.2 告警治理:备节点回滚」一节里, 对备节点用的是sudo rm -f "$D/monitoring/sanitize.py"。那只适用于「本轮新增」的场景——notify.py现在依赖sanitize.py,删掉它会让脱敏静默失效,请勿照抄。
相关 CHANGELOG¶
| CHANGELOG | 章节标题 |
|---|---|
docs/CHANGELOG-2026-09-18.md |
§36. 新增 sec-notify.py(安全告警 → 飞书安全群/安全邮箱) |
docs/CHANGELOG-2026-09-23.md |
§2. 主节点补齐 notify.py,修复 sec-notify.py 加载器 |
docs/CHANGELOG-2026-09-26.md |
§1.7 脱敏:出口统一收口在 notify.py 的 render() |
docs/CHANGELOG-2026-09-26.md |
§9. 通知腿 NameError 缺陷(P0,已修复) |
docs/CHANGELOG-2026-09-26.md |
§10.1 汇总通报统一走新机器人(用户要求) |
docs/CHANGELOG-2026-09-26.md |
§12. 通知风暴与邮件限流(2026-09-26 16:4x,用户投诉「告警过于繁多」) |
定时器全景(含各调用方的
ExecStart逐字抄录)见 timers.md。