fix: 管道审计+全局cron失败监控, 异常时主动推XMPP
This commit is contained in:
@@ -1,118 +1,185 @@
|
||||
#!/usr/bin/env python3
|
||||
"""verify_reassess_pipeline.py — 重评推送管道审计
|
||||
"""verify_reassess_pipeline.py — 重评推送管道审计 + 全局cron失败监控
|
||||
|
||||
检查三个代码级约束是否在正常运转:
|
||||
1. price_monitor 每2分触发 → 突破检测 → reassess调用
|
||||
2. stale_detector/stale_push_wlin 自选买入区检测
|
||||
3. mofin_collect LLM前强制重评
|
||||
检查:
|
||||
1. price_monitor 每2分正常跑
|
||||
2. zone breach检测正常
|
||||
3. holding_strategies有数据
|
||||
4. XMPP bridge在线
|
||||
5. reassess模块可导入
|
||||
6. 【新增】所有关键cron job状态(是否有failed)
|
||||
|
||||
输出:JSON格式管道状态(无异常时静默[SILENT])
|
||||
输出:正常时 [SILENT],有异常时推XMPP
|
||||
"""
|
||||
import json, os, sys, subprocess
|
||||
import json, os, sys, subprocess, sqlite3
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
BASE = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(BASE))
|
||||
sys.path.insert(0, "/home/hmo/MoFin")
|
||||
|
||||
try:
|
||||
from mofin_db import get_conn
|
||||
conn = get_conn()
|
||||
cur = conn.cursor()
|
||||
except Exception:
|
||||
conn = None
|
||||
XMPP_BRIDGE = "http://127.0.0.1:5805/"
|
||||
XMPP_USER = "hmo@yoin.fun"
|
||||
|
||||
now = datetime.now()
|
||||
ok = True
|
||||
checks = []
|
||||
def xmpp_push(text):
|
||||
try:
|
||||
payload = json.dumps({"to": XMPP_USER, "body": text, "type": "chat"}).encode()
|
||||
req = Request(XMPP_BRIDGE, data=payload, headers={"Content-Type": "application/json"})
|
||||
urlopen(req, timeout=5)
|
||||
except Exception as e:
|
||||
print(f"[XMPP推送失败] {e}", file=sys.stderr)
|
||||
|
||||
# 1. price_monitor 最近运行时间
|
||||
try:
|
||||
if conn:
|
||||
lp = cur.execute("SELECT MAX(updated_at) FROM live_prices").fetchone()[0]
|
||||
def scan_cron_failures():
|
||||
"""扫描两个cron jobs.json看是否有failed状态的关键job"""
|
||||
failures = []
|
||||
jobs_files = [
|
||||
"/home/hmo/.hermes/cron/jobs.json",
|
||||
"/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json",
|
||||
]
|
||||
for jf in jobs_files:
|
||||
try:
|
||||
data = json.load(open(jf))
|
||||
for job in data.get("jobs", []):
|
||||
jid = job.get("id", "?")
|
||||
name = job.get("name", "") or jid[:12]
|
||||
status = job.get("last_status", "")
|
||||
enabled = job.get("enabled", True)
|
||||
if not enabled:
|
||||
continue
|
||||
# 关键job:价格监控、重评、盘前中监控
|
||||
key_job = any(kw in name.lower() for kw in [
|
||||
"price_monitor", "monitor", "盘前中", "reassess",
|
||||
"重评", "自选买入", "stale_push", "管道审计",
|
||||
"宏观风险", "策略时效"
|
||||
])
|
||||
if not key_job:
|
||||
continue
|
||||
if status == "failed":
|
||||
last_run = job.get("last_run_at", "?")
|
||||
failures.append(f" ❌ {name} ({jid[:8]}) last_run={last_run}")
|
||||
except Exception:
|
||||
pass
|
||||
return failures
|
||||
|
||||
def check_cron_jobs():
|
||||
"""另法:直接查cron数据库"""
|
||||
issues = []
|
||||
for db_path in [
|
||||
BASE / "cron" / "cron.db",
|
||||
Path("/home/hmo/.hermes/cron/cron.db"),
|
||||
]:
|
||||
if not db_path.exists():
|
||||
continue
|
||||
try:
|
||||
c = sqlite3.connect(str(db_path))
|
||||
for row in c.execute("""
|
||||
SELECT id, name, last_status, last_run_at, enabled
|
||||
FROM cron_jobs WHERE enabled=1
|
||||
ORDER BY last_run_at DESC
|
||||
""").fetchall():
|
||||
jid, name, status, last_run, enabled = row
|
||||
if status == "failed":
|
||||
issues.append(f" ❌ {name}({jid[:8]}) last_run={last_run}")
|
||||
c.close()
|
||||
except Exception:
|
||||
pass
|
||||
return issues
|
||||
|
||||
def run():
|
||||
ok = True
|
||||
alerts = []
|
||||
checks = []
|
||||
|
||||
# 1. price_monitor 最近运行时间
|
||||
try:
|
||||
conn = sqlite3.connect("/home/hmo/MoFin/data/mofin.db")
|
||||
lp = conn.execute("SELECT MAX(updated_at) FROM live_prices").fetchone()[0]
|
||||
if lp:
|
||||
lp_dt = datetime.fromisoformat(lp) if isinstance(lp, str) else lp
|
||||
mins_ago = (now - lp_dt).total_seconds() / 60
|
||||
if hasattr(lp_dt, 'tzinfo') and lp_dt.tzinfo is None:
|
||||
if isinstance(lp, str) and '+' not in lp:
|
||||
lp_dt = lp_dt.replace(tzinfo=None)
|
||||
mins_ago = (datetime.now() - lp_dt).total_seconds() / 60
|
||||
status = "ok" if mins_ago < 10 else "warn"
|
||||
if mins_ago > 15:
|
||||
status = "fail"
|
||||
ok = False
|
||||
checks.append({"check":"price_monitor","status":status,"detail":f"最后更新{mins_ago:.0f}分前 @ {lp}"})
|
||||
alerts.append(f"price_monitor {mins_ago:.0f}分未更新")
|
||||
checks.append({"check":"price_monitor","status":status,"detail":f"最后更新{mins_ago:.0f}分前"})
|
||||
else:
|
||||
checks.append({"check":"price_monitor","status":"warn","detail":"live_prices无数据"})
|
||||
except Exception as e:
|
||||
checks.append({"check":"price_monitor","status":"fail","detail":str(e)})
|
||||
ok = False
|
||||
except Exception as e:
|
||||
checks.append({"check":"price_monitor","status":"fail","detail":str(e)})
|
||||
ok = False
|
||||
alerts.append(f"price_monitor异常: {e}")
|
||||
|
||||
# 2. 今日价格事件(有无zone breach触发)
|
||||
try:
|
||||
if conn:
|
||||
pe = cur.execute("SELECT COUNT(*) FROM price_events WHERE date=date('now')").fetchone()[0]
|
||||
checks.append({"check":"zone_breaches_today","status":"ok","detail":f"{pe}次突破事件"})
|
||||
except Exception as e:
|
||||
checks.append({"check":"zone_breaches","status":"fail","detail":str(e)})
|
||||
|
||||
# 3. holding_strategies 是否有策略数据
|
||||
try:
|
||||
if conn:
|
||||
hs = cur.execute("SELECT COUNT(*) FROM holding_strategies").fetchone()[0]
|
||||
valid = cur.execute("SELECT COUNT(*) FROM holding_strategies WHERE stop_loss IS NOT NULL").fetchone()[0]
|
||||
status = "ok" if hs > 0 else "warn"
|
||||
# 2. holding_strategies 是否有数据
|
||||
try:
|
||||
hs = conn.execute("SELECT COUNT(*) FROM holding_strategies").fetchone()[0]
|
||||
valid = conn.execute("SELECT COUNT(*) FROM holding_strategies WHERE stop_loss IS NOT NULL").fetchone()[0]
|
||||
if hs == 0:
|
||||
ok = False
|
||||
checks.append({"check":"strategies_in_db","status":status,"detail":f"{hs}条策略(含止损{valid}条)"})
|
||||
except Exception as e:
|
||||
checks.append({"check":"strategies","status":"fail","detail":str(e)})
|
||||
|
||||
# 4. mofin_collect 今日是否跑过(检查策略新鲜度日志)
|
||||
try:
|
||||
if conn:
|
||||
fresh = cur.execute("""
|
||||
SELECT COUNT(*) FROM holding_strategies
|
||||
WHERE date(created_at)=date('now')
|
||||
""").fetchone()[0]
|
||||
checks.append({"check":"today_reassessed","status":"ok","detail":f"今日{('reassess刷新'+str(fresh)+'条') if fresh else '暂无新重评记录'}"})
|
||||
except Exception as e:
|
||||
checks.append({"check":"today_reassess","status":"warn","detail":str(e)})
|
||||
|
||||
# 5. XMPP bridge是否在线
|
||||
try:
|
||||
import urllib.request
|
||||
req = urllib.request.Request("http://127.0.0.1:5805/",
|
||||
data=b'{"to":"hmo@yoin.fun","body":"ping","type":"chat"}',
|
||||
headers={"Content-Type":"application/json"})
|
||||
resp = urllib.request.urlopen(req, timeout=3)
|
||||
result = json.loads(resp.read())
|
||||
bridge_ok = result.get("ok") == True
|
||||
checks.append({"check":"xmpp_bridge_5805","status":"ok" if bridge_ok else "fail","detail":"在线" if bridge_ok else "异常"})
|
||||
if not bridge_ok:
|
||||
alerts.append("holding_strategies为0条")
|
||||
checks.append({"check":"strategies_in_db","status":"ok" if hs > 0 else "warn","detail":f"{hs}条策略(含止损{valid}条)"})
|
||||
except Exception as e:
|
||||
checks.append({"check":"strategies","status":"fail","detail":str(e)})
|
||||
ok = False
|
||||
except Exception as e:
|
||||
checks.append({"check":"xmpp_bridge","status":"fail","detail":str(e)})
|
||||
ok = False
|
||||
|
||||
# 6. 验证reassess_with_context可导入
|
||||
try:
|
||||
sys.path.insert(0, "/home/hmo/MoFin")
|
||||
from strategy_lifecycle import reassess_with_context
|
||||
checks.append({"check":"reassess_module","status":"ok","detail":"reassess_with_context可导入"})
|
||||
except Exception as e:
|
||||
checks.append({"check":"reassess_module","status":"fail","detail":str(e)})
|
||||
ok = False
|
||||
# 3. XMPP bridge 是否在线
|
||||
try:
|
||||
req = Request(XMPP_BRIDGE, data=b'{"to":"hmo@yoin.fun","body":"ping","type":"chat"}',
|
||||
headers={"Content-Type":"application/json"})
|
||||
resp = urlopen(req, timeout=3)
|
||||
bridge_ok = json.loads(resp.read()).get("ok") == True
|
||||
if not bridge_ok:
|
||||
ok = False
|
||||
alerts.append("XMPP bridge异常")
|
||||
checks.append({"check":"xmpp_bridge","status":"ok" if bridge_ok else "fail","detail":"在线" if bridge_ok else "异常"})
|
||||
except Exception as e:
|
||||
checks.append({"check":"xmpp_bridge","status":"fail","detail":str(e)})
|
||||
ok = False
|
||||
alerts.append(f"XMPP bridge不可达: {e}")
|
||||
|
||||
# 4. reassess模块可导入
|
||||
try:
|
||||
from strategy_lifecycle import reassess_with_context
|
||||
checks.append({"check":"reassess_module","status":"ok","detail":"可导入"})
|
||||
except Exception as e:
|
||||
checks.append({"check":"reassess_module","status":"fail","detail":str(e)})
|
||||
ok = False
|
||||
alerts.append(f"reassess模块导入失败: {e}")
|
||||
|
||||
# 5. cron job失败检测
|
||||
cron_issues = scan_cron_failures() + check_cron_jobs()
|
||||
if cron_issues:
|
||||
ok = False
|
||||
alerts.append(f"{len(cron_issues)}个cron job失败")
|
||||
for issue in cron_issues[:5]:
|
||||
alerts.append(issue)
|
||||
checks.append({"check":"cron_jobs","status":"fail","detail":"; ".join(cron_issues[:3])})
|
||||
else:
|
||||
checks.append({"check":"cron_jobs","status":"ok","detail":"所有关键job正常"})
|
||||
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
# 输出:无异常静默,有异常报警
|
||||
result = {
|
||||
"pipeline": "ok" if ok else "degraded",
|
||||
"checked_at": now.isoformat(),
|
||||
"checks": checks
|
||||
}
|
||||
# 输出
|
||||
result = {
|
||||
"pipeline": "ok" if ok else "degraded",
|
||||
"checked_at": datetime.now().isoformat(),
|
||||
"checks": checks,
|
||||
"alerts": alerts
|
||||
}
|
||||
|
||||
if ok:
|
||||
# 只有异常时才出声
|
||||
print("[SILENT]")
|
||||
else:
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
if ok:
|
||||
print("[SILENT]")
|
||||
else:
|
||||
msg = "🔴 重评管道异常:\n" + "\n".join(alerts)
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
# 有异常时主动推XMPP(取代静默)
|
||||
xmpp_push(msg)
|
||||
print(f"\n已推送XMPP: {len(alerts)}条告警", file=sys.stderr)
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
|
||||
Reference in New Issue
Block a user