From 4fe2c4331010fe2f21e866c1ea77ededd9b37288 Mon Sep 17 00:00:00 2001 From: xxm Date: Fri, 21 Aug 2026 11:19:07 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=A4=8D=E5=88=B6alert=5Flogger.py?= =?UTF-8?q?=E5=88=B0deploy/profile-scripts(=E4=BF=AE=E5=A4=8Dprice=5Fmonit?= =?UTF-8?q?or=20ModuleNotFoundError)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/profile-scripts/alert_logger.py | 104 +++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 deploy/profile-scripts/alert_logger.py diff --git a/deploy/profile-scripts/alert_logger.py b/deploy/profile-scripts/alert_logger.py new file mode 100644 index 00000000..8aa796e9 --- /dev/null +++ b/deploy/profile-scripts/alert_logger.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +"""alert_logger.py — MoFin 统一异常告警通道(2026-08-12 落地) + +任何脚本/任务出错时调用 record_alert(): + - 写入 data/alerts.json(active 异常列表,前端浮窗读取 /api/overview 的 alerts) + - 追加 data/alerts_history.log(历史异常实时日志,永久保留) + - 同时通过 xmpp_logger 发送 XMPP 告警(老莫铁律:任何错误/异常 → xmpp告警 + MoFin监控) + +用法: + from alert_logger import record_alert + record_alert(level="error", source="price_monitor", title="重评失败", + detail="688002 LLM 调用超时", code="688002") + +level: info / warning / error / critical +""" +import json +import time +from pathlib import Path + +DATA_DIR = Path("/home/hmo/MoFin/data") +ALERTS_FILE = DATA_DIR / "alerts.json" +HISTORY_FILE = DATA_DIR / "alerts_history.log" + +# 保留的 active 异常数(浮窗只显示 active,最多保留 20 条) +MAX_ACTIVE = 20 + + +def _now_str(): + from datetime import datetime + return datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + +def record_alert(level="error", source="", title="", detail="", code="", clear_after=0): + """记录一条异常。clear_after>0 表示自动清除的秒数(可空)""" + ts = time.time() + ts_str = _now_str() + entry = { + "ts": ts, + "ts_str": ts_str, + "level": level, + "source": source, + "title": title, + "detail": detail, + "code": code, + "clear_after": clear_after, + } + + # 1. active 列表(alerts.json) + try: + alerts = json.loads(ALERTS_FILE.read_text(encoding="utf-8")) if ALERTS_FILE.exists() else [] + if not isinstance(alerts, list): + alerts = [] + # 同 source+code 的旧告警先移除(避免重复堆积),再插到头部 + alerts = [a for a in alerts if not (a.get("source") == source and a.get("code") == code)] + alerts.insert(0, entry) + # 清除超时的 + now = time.time() + alerts = [a for a in alerts if not a.get("clear_after") or now - a.get("ts", 0) < a["clear_after"]] + alerts = alerts[:MAX_ACTIVE] + ALERTS_FILE.write_text(json.dumps(alerts, ensure_ascii=False, indent=1), encoding="utf-8") + except Exception: + pass + + # 2. 历史日志(永久追加) + try: + with HISTORY_FILE.open("a", encoding="utf-8") as f: + f.write(f"[{ts_str}] [{level.upper()}] {source} | {title} | {detail} | {code}\n") + except Exception: + pass + + # 3. XMPP 告警(非 info 级别才发,避免轰炸)→ 本地 XMPP 发送服务 :5805 + if level != "info": + try: + import urllib.request as _ur + body = f"⚠️[{level.upper()}] {source}: {title} {detail} {code}".strip() + req = _ur.Request( + "http://127.0.0.1:5805/", + data=body.encode("utf-8"), + headers={"Content-Type": "text/plain"}, + method="POST", + ) + _ur.urlopen(req, timeout=5).read() + except Exception: + pass + + return entry + + +def clear_alert(source="", code=""): + """清除指定 source+code 的 active 告警(恢复后调用)""" + try: + alerts = json.loads(ALERTS_FILE.read_text(encoding="utf-8")) if ALERTS_FILE.exists() else [] + if not isinstance(alerts, list): + return + alerts = [a for a in alerts if not (a.get("source") == source and (not code or a.get("code") == code))] + ALERTS_FILE.write_text(json.dumps(alerts, ensure_ascii=False, indent=1), encoding="utf-8") + except Exception: + pass + + +if __name__ == "__main__": + # 自测 + record_alert(level="info", source="alert_logger", title="自测", detail="告警通道正常", code="test") + print("alerts.json 写入 OK:", ALERTS_FILE.exists())