chore: deployed L0-L4 self-check system

This commit is contained in:
知微
2026-07-20 19:40:23 +08:00
parent c545a82023
commit 135bfced5a
6 changed files with 2530 additions and 1758 deletions
@@ -0,0 +1,216 @@
#!/usr/bin/env python3
"""functional_health_check.py — L1 功能健康检查
判据不是"进程活着",而是"功能是否达成":每个核心模块检查其
**输出物的新鲜度和有效性**。输出物不新鲜 = 功能未达成 = 异常。
频率:交易时段每 15 分钟(cron),输出 gateway/logs/functional_health.json。
责任矩阵 L1。修复由 L3 self_repair 读取本报告执行。
"""
import os, sys, json, sqlite3, subprocess
from datetime import datetime, timedelta
sys.path.insert(0, '/home/hmo/MoFin')
DB = '/home/hmo/MoFin/data/mofin.db'
OUT = '/home/hmo/MoFin/gateway/logs/functional_health.json'
# ── 功能注册表:模块 → 功能判据 ──────────────────────────────
# type:
# db_freshness: DB 表 MAX(col) 距今不超过 max_agetrading=交易时段才检查, daily=每日, always=总是)
# file_freshness: 文件 mtime 距今不超过 max_age
# bot_activity: XMPP bot 活动检查(journal
# agent_log: gateway agent.log 最近有成功 LLM 调用
# soft=True: 无输出可能属正常(如区间未触发),降级为 warn 而非 fail
REGISTRY = [
{"module": "price_monitor", "function": "实时价格写入DB(live_prices)",
"check": {"type": "db_freshness", "table": "live_prices", "col": "updated_at",
"max_age_min": 6, "when": "trading"},
"repair": {"action": "rerun_script", "script": "price_monitor.py"}},
{"module": "market_watch", "function": "市场快照采集(market_snapshots)",
"check": {"type": "db_freshness", "table": "market_snapshots", "col": "created_at",
"max_age_min": 40, "when": "trading"},
"repair": {"action": "rerun_script", "script": "market_watch.py"}},
{"module": "mtf_cache", "function": "多周期均线缓存刷新(mtf_cache)",
"check": {"type": "db_freshness", "table": "mtf_cache", "col": "updated_at",
"max_age_min": 75, "when": "trading"},
"repair": {"action": "rerun_script", "script": "refresh_mtf_cache.py"}},
{"module": "macro_context", "function": "宏观上下文刷新(macro_context_log)",
"check": {"type": "db_freshness", "table": "macro_context_log", "col": "created_at",
"max_age_min": 45, "when": "trading"},
"repair": {"action": "rerun_script", "script": "refresh_macro_context.py"}},
{"module": "health_collector", "function": "健康数据采集(mofin_health.json)",
"check": {"type": "file_freshness", "path": "/home/hmo/web-dashboard/static/mofin_health.json",
"max_age_min": 20, "when": "trading"},
"repair": {"action": "rerun_script", "script": "mofin_health.py"}},
{"module": "premarket", "function": "盘前全量重评(premarket summary)",
"check": {"type": "file_freshness", "path": "/tmp/mofin_premarket/summary.json",
"max_age_h": 26, "when": "daily"},
"repair": {"action": "rerun_script", "script": "premarket_full_review.py"}},
{"module": "gateway_llm", "function": "LLM调用链可用(gateway agent.log)",
"check": {"type": "agent_log", "max_age_min": 30, "when": "always"},
"repair": {"action": "llm_diagnose"}},
{"module": "xmpp_bot", "function": "XMPP消息收发(bot journal)",
"check": {"type": "bot_activity", "when": "always"},
"repair": {"action": "llm_diagnose"}},
{"module": "cron_engine", "function": "cron调度引擎本身(有job在最近10min运行)",
"check": {"type": "cron_engine", "max_age_min": 12, "when": "trading"},
"repair": {"action": "llm_diagnose"}},
]
def is_trading_now(now):
return now.weekday() < 5 and 9 <= now.hour <= 16
def check_db_freshness(conn, chk, now):
try:
row = conn.execute(f"SELECT MAX({chk['col']}) FROM {chk['table']}").fetchone()
if not row or not row[0]:
return "fail", f"{chk['table']} 无数据"
last = datetime.fromisoformat(str(row[0]).replace("Z", ""))
age_min = (now - last).total_seconds() / 60
limit = chk.get("max_age_min", chk.get("max_age_h", 24) * 60)
if age_min > limit:
return ("warn" if chk.get("soft") else "fail"), \
f"{chk['table']} 最新记录 {last.strftime('%m-%d %H:%M')}{age_min/60:.1f}h前,阈值{limit}min"
return "ok", f"{age_min:.0f}min前"
except Exception as e:
return "fail", f"查询失败: {str(e)[:80]}"
def check_file_freshness(chk, now):
p = chk["path"]
if not os.path.exists(p):
return "fail", f"文件不存在: {p}"
age_min = (now.timestamp() - os.path.getmtime(p)) / 60
limit = chk.get("max_age_min", chk.get("max_age_h", 24) * 60)
if age_min > limit:
return ("warn" if chk.get("soft") else "fail"), \
f"{os.path.basename(p)} {age_min/60:.1f}h 未更新(阈值{limit}min"
return "ok", f"{age_min:.0f}min前"
def check_agent_log(chk, now):
try:
sys.path.insert(0, '/home/hmo/MoFin')
from xmpp_logger import _scan_agent_log
r = _scan_agent_log(now.timestamp(), "zhiwei")
if r["status"] == "ok":
age = r.get("age_sec", -1)
if age > chk["max_age_min"] * 60:
return "warn", f"最近成功LLM调用在 {age//60}min 前(无新流量,可能正常)"
return "ok", f"latency={r.get('latency')}"
return "fail", f"agent.log 最近调用失败: {r.get('error','?')[:100]}"
except Exception as e:
return "fail", f"检查失败: {e}"
def check_bot_activity(chk, now):
try:
# 第一判据:服务当前是否 active(权威"现在在不在跑"
r2 = subprocess.run(["systemctl", "is-active", "xmpp-zhiwei"],
capture_output=True, text=True, timeout=5)
if r2.stdout.strip() != "active":
return "fail", "bot 服务非 active(已停止)"
# 第二判据:最近 journal 是否处于断线循环
r = subprocess.run(
["journalctl", "-u", "xmpp-zhiwei", "--no-pager", "--since", "30 min ago", "-o", "cat"],
capture_output=True, timeout=8, text=True)
lines = r.stdout
if "连接超时" in lines and "就绪" not in lines:
return "fail", "bot 处于断线重连循环"
if "XMPP 就绪" in lines or "已发送" in lines or "收到" in lines:
return "ok", "bot 活动正常"
return "ok", "bot 在线空闲(无新消息)"
except Exception as e:
return "fail", f"检查失败: {e}"
def check_cron_engine(chk, now):
"""cron 引擎:最近 max_age_min 内是否有任何 job 运行过"""
try:
d = json.load(open('/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json'))
jobs = d if isinstance(d, list) else d.get('jobs', [])
latest = None
for j in jobs:
lr = j.get('last_run_at')
if lr:
try:
t = datetime.fromisoformat(lr.replace('Z', '+00:00')).replace(tzinfo=None)
if latest is None or t > latest:
latest = t
except Exception:
pass
if not latest:
return "fail", "所有 job 均无运行记录"
age_min = (now - latest).total_seconds() / 60
if age_min > chk["max_age_min"]:
return "fail", f"最近 job 运行在 {age_min:.0f}min 前,调度引擎疑似停摆"
return "ok", f"最近 job 运行于 {age_min:.0f}min 前"
except Exception as e:
return "fail", f"检查失败: {e}"
def main():
now = datetime.now()
trading = is_trading_now(now)
results = []
conn = sqlite3.connect(DB, timeout=10)
for item in REGISTRY:
chk = item["check"]
when = chk.get("when", "always")
if when == "trading" and not trading:
results.append({"module": item["module"], "function": item["function"],
"status": "skip", "reason": "非交易时段"})
continue
if when == "daily":
# 每日类:只 fail 不 warn,且非交易日放宽到 72h
if now.weekday() >= 5:
chk = dict(chk)
chk["max_age_h"] = 72
t = chk["type"]
if t == "db_freshness":
status, reason = check_db_freshness(conn, chk, now)
elif t == "file_freshness":
status, reason = check_file_freshness(chk, now)
elif t == "agent_log":
status, reason = check_agent_log(chk, now)
elif t == "bot_activity":
status, reason = check_bot_activity(chk, now)
elif t == "cron_engine":
status, reason = check_cron_engine(chk, now)
else:
status, reason = "fail", f"未知检查类型 {t}"
results.append({"module": item["module"], "function": item["function"],
"status": status, "reason": reason,
"repair": item.get("repair")})
conn.close()
fails = [r for r in results if r["status"] == "fail"]
warns = [r for r in results if r["status"] == "warn"]
report = {
"generated_at": now.strftime("%Y-%m-%d %H:%M:%S"),
"trading_hours": trading,
"summary": {"total": len(results), "ok": sum(1 for r in results if r["status"] == "ok"),
"warn": len(warns), "fail": len(fails),
"skip": sum(1 for r in results if r["status"] == "skip")},
"status": "fail" if fails else ("warn" if warns else "ok"),
"checks": results,
}
os.makedirs(os.path.dirname(OUT), exist_ok=True)
with open(OUT, "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
print(f"功能健康: {report['summary']} status={report['status']}")
for r in results:
if r["status"] in ("fail", "warn"):
print(f" {r['status'].upper()} {r['module']}: {r['reason']}")
if __name__ == "__main__":
main()