From ea77bd5afddc61366ca708a1efb9b20420fe2c29 Mon Sep 17 00:00:00 2001 From: xxm Date: Thu, 13 Aug 2026 00:35:53 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BC=82=E5=B8=B8=E6=B5=AE=E7=AA=97+=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E5=91=8A=E8=AD=A6=E9=80=9A=E9=81=93+=E5=86=B7?= =?UTF-8?q?=E5=8D=B41=E5=B0=8F=E6=97=B6:=20alert=5Flogger=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E5=BC=82=E5=B8=B8=E5=86=99=E5=85=A5(alerts.json+histo?= =?UTF-8?q?ry.log+XMPP),=20=E5=89=8D=E7=AB=AF=E5=8F=B3=E4=B8=8A=E8=A7=92?= =?UTF-8?q?=E6=B5=AE=E5=8A=A8=E5=BC=82=E5=B8=B8=E5=8C=BA(=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=B1=95=E5=BC=805=E7=A7=92=E6=8A=98=E5=8F=A0,?= =?UTF-8?q?=E5=8F=AA=E6=98=BE=E7=A4=BAactive),=20price=5Fmonitor=E6=8E=A5?= =?UTF-8?q?=E5=85=A5=E5=BC=82=E5=B8=B8=E9=92=A9=E5=AD=90(=E9=87=8D?= =?UTF-8?q?=E8=AF=84=E5=A4=B1=E8=B4=A5/=E6=8B=89=E5=8F=96=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5/DB=E5=90=8C=E6=AD=A5),=20=E5=86=B7=E5=8D=B430?= =?UTF-8?q?=E5=88=86->1=E5=B0=8F=E6=97=B6+15=E5=88=865%=E7=AA=81=E5=8F=91?= =?UTF-8?q?=E4=BE=8B=E5=A4=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- alert_logger.py | 104 ++++++++++++++++++++++++ deploy/profile-scripts/price_monitor.py | 26 +++++- 2 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 alert_logger.py diff --git a/alert_logger.py b/alert_logger.py new file mode 100644 index 00000000..8aa796e9 --- /dev/null +++ b/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()) diff --git a/deploy/profile-scripts/price_monitor.py b/deploy/profile-scripts/price_monitor.py index 6c896d7b..a7ef07de 100644 --- a/deploy/profile-scripts/price_monitor.py +++ b/deploy/profile-scripts/price_monitor.py @@ -9,6 +9,7 @@ import sqlite3 from datetime import datetime from mo_data import read_decisions +from alert_logger import record_alert, clear_alert BREACH_PATH = "/home/hmo/.hermes/zone_breach.json" STATE_PATH = "/home/hmo/.hermes/price_trigger_state.json" @@ -105,6 +106,10 @@ def fetch_all_prices(codes): text = r.read().decode("gbk") except Exception as e: print(f"⚠️ 批量拉取失败: {e}", file=sys.stderr) + try: + record_alert(level="warning", source="price_monitor", title="批量拉取失败", detail=str(e)[:200]) + except Exception: + pass return {} results = {} @@ -149,6 +154,10 @@ def refresh_data_prices(): conn.close() except Exception as e: print(f"⚠️ 从DB读代码失败: {e}", file=sys.stderr) + try: + record_alert(level="warning", source="price_monitor", title="读取持仓失败", detail=str(e)[:200]) + except Exception: + pass return 0 if not all_codes: @@ -295,6 +304,10 @@ def refresh_data_prices(): except Exception: pass conn = None print(f"⚠️ DB同步异常: {e}", file=sys.stderr) + try: + record_alert(level="error", source="price_monitor", title="DB同步异常", detail=str(e)[:200]) + except Exception: + pass break else: # for-else: loop exhausted without break @@ -537,7 +550,10 @@ def run_once(round_label=""): record_event(code, name, "swing_re", price, str(_ma10)) _push_action("波段再进", f"📈 {name}({code}) {price} 收回MA10({_ma10:.2f})且突破前一日高点 → 波段再进,可接回") except Exception as _e: - pass + try: + record_alert(level="warning", source="price_monitor", title="波段检测异常", detail=str(_e)[:200], code=code) + except Exception: + pass # 时间预算检查:如果超时,跳过重评只做状态记录 _budget_low = (time.time() - start) > TIME_BUDGET @@ -578,6 +594,10 @@ def run_once(round_label=""): outputs.append(f" 📨 止损重评→已推送Dad: {action}") except Exception as e: outputs.append(f" ⚠️ 止损重评失败: {e}") + try: + record_alert(level="error", source="price_monitor", title="止损重评失败", detail=str(e)[:200], code=code) + except Exception: + pass else: extra = "" if "_price" in key: @@ -626,6 +646,10 @@ def run_once(round_label=""): outputs.append(f" 📋 本地日志(不推): {reason}") except Exception as e: outputs.append(f" ⚠️ 区间重评失败: {e}") + try: + record_alert(level="error", source="price_monitor", title="区间重评失败", detail=str(e)[:200], code=code) + except Exception: + pass state[code][key] = True state_updated = True