fix: 复制alert_logger.py到deploy/profile-scripts(修复price_monitor ModuleNotFoundError)

This commit is contained in:
xxm
2026-08-21 11:19:07 +08:00
parent 37867f378c
commit 4fe2c43310
+104
View File
@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
"""alert_logger.py — MoFin 统一异常告警通道(2026-08-12 落地)
任何脚本/任务出错时调用 record_alert()
- 写入 data/alerts.jsonactive 异常列表,前端浮窗读取 /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())