Files
MoFin/alert_logger.py
T

105 lines
3.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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())