Files
AgentsMeeting/gateway/scripts/agents_daily_health.py
T
hmo 69fa67e24f F可视化: dashboard 完整实现 A-F 六原则呈现 + 弹窗修复
- A: git log + dirty status
- B: 服务架构矩阵 (/health + watchdog覆盖 + 拓扑依赖)
- C: Tier1/Tier2 监控结果 + 定时任务状态
- D: TODO执行记录 + escalation日志
- F: 期望状态 vs 实际状态合规矩阵
- Kanban: dashboard内嵌版块
- 弹窗修复: 计划任务改用pythonw.exe + subprocess加CREATE_NO_WINDOW
- 修复ast_grep_replace导致的
2026-07-14 02:49:07 +08:00

258 lines
9.1 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.
"""
agents_daily_health.py — Tier 2 全面健康检查(每日 08:00 触发)
在 Tier 1(进程/端口/HTTP)基础上增加:
- 磁盘空间检查
- 定时任务存活检查
- 数据新鲜度(各服务最后响应时间)
- 跨服务依赖链检查
- XMPP 异常报告
基于 MoFin morning_health_check.py 的模式。
"""
import json, os, sys, time, subprocess, urllib.request, urllib.error
from datetime import datetime
# 强制 stdout/stderr 为 UTF-8,避免 Windows GBK 控制台对中文/emoji 编码失败
try:
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
except Exception:
# Python <3.7 不支持 reconfigure,忽略
pass
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
GATEWAY_DIR = os.path.dirname(SCRIPT_DIR)
BASE = os.path.dirname(GATEWAY_DIR)
TEMP = os.path.join(BASE, "gateway", "temp")
LOGS = os.path.join(BASE, "gateway", "logs")
PYTHON = r"C:\Users\hmo\AppData\Local\Programs\Python\Python310\python.exe"
os.makedirs(LOGS, exist_ok=True)
os.makedirs(TEMP, exist_ok=True)
REPORT_LOG = os.path.join(LOGS, "daily_health_report.log")
REPORT_FILE = os.path.join(TEMP, "last_daily_health.json")
# ── 磁盘阈值 ─────────────────────────────────
DISK_WARN_GB = 10 # 剩余 <10G → 警告
DISK_CRIT_GB = 2 # 剩余 <2G → 严重
# ── 服务注册表(从共享 registry 导入)─────────────
sys.path.insert(0, SCRIPT_DIR)
from service_registry import SERVICES as _SR, TEMP as _TEMP, GATEWAY_DIR as _GD
TEMP = _TEMP
LOGS = os.path.join(_GD, "logs")
os.makedirs(LOGS, exist_ok=True)
os.makedirs(TEMP, exist_ok=True)
SERVICES = []
for svc in _SR:
SERVICES.append({
"name": svc["name"],
"pid_file": svc["pid_file"],
"port": svc["port"],
"health_url": svc["health_url"],
"accept_401": svc.get("accept_401", False),
"depends_on": svc.get("depends_on", []),
})
def log(msg):
ts = datetime.now().strftime("%H:%M:%S")
line = f"[{ts}] {msg}"
print(line)
with open(REPORT_LOG, "a", encoding="utf-8") as f:
f.write(line + "\n")
def sizeof_fmt(n):
for unit in ("B", "K", "M", "G"):
if n < 1024:
return f"{n:.1f}{unit}"
n /= 1024
return f"{n:.1f}T"
# ── 检查基元 ──────────────────────────────────
def pid_alive(pid):
try:
r = subprocess.run(["tasklist", "/FI", f"PID eq {pid}", "/NH"],
capture_output=True, text=True, timeout=5,
creationflags=subprocess.CREATE_NO_WINDOW)
return str(pid) in r.stdout
except:
return False
def port_open(port):
try:
r = subprocess.run(["netstat", "-ano"], capture_output=True,
text=True, timeout=5,
creationflags=subprocess.CREATE_NO_WINDOW)
return any(f":{port} " in line and "LISTENING" in line
for line in r.stdout.splitlines())
except:
return False
def http_check(url, timeout=5, accept_401=False):
try:
r = urllib.request.urlopen(url, timeout=timeout)
return (r.status == 200, f"HTTP {r.status}")
except urllib.error.HTTPError as e:
if accept_401 and e.code in (401, 403):
return (True, "auth")
return (False, f"HTTP {e.code}")
except urllib.error.URLError as e:
return (False, str(e.reason)[:60])
except Exception as e:
return (False, str(e)[:60])
def check_disk(path="C:"):
"""Windows 磁盘剩余空间检查。"""
try:
r = subprocess.run(
["wmic", "LogicalDisk", "where", f"DeviceID='{path}'",
"get", "FreeSpace,Size", "/format:csv"],
capture_output=True, text=True, timeout=10
)
for line in r.stdout.splitlines():
parts = [p.strip() for p in line.split(",")]
if len(parts) >= 3 and parts[1] == path:
free = int(parts[2])
free_gb = free / (1024**3)
if free_gb < DISK_CRIT_GB:
return ("critical", f"{free_gb:.1f}GB free (threshold: {DISK_CRIT_GB}GB)")
elif free_gb < DISK_WARN_GB:
return ("warn", f"{free_gb:.1f}GB free (threshold: {DISK_WARN_GB}GB)")
else:
return ("ok", f"{free_gb:.1f}GB free")
except:
pass
return ("unknown", "disk check failed")
def check_cron_tasks():
"""检查关键定时任务是否启用。"""
expected = ["agents-health-check"]
results = []
try:
for name in expected:
r = subprocess.run(
["schtasks", "/Query", "/TN", name, "/FO", "CSV", "/NH"],
capture_output=True, text=True, timeout=10
)
if name in r.stdout:
status = "ok" if "Ready" in r.stdout or "Running" in r.stdout else "warn"
results.append({"name": name, "status": status})
else:
results.append({"name": name, "status": "missing"})
except:
results.append({"name": "check_failed", "status": "error"})
return results
# ── 主检查 ────────────────────────────────────
def main():
report = {
"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"services": [],
"disk": {},
"cron_tasks": [],
"summary": {"total": 0, "ok": 0, "warn": 0, "fail": 0},
"recommendations": [],
}
# 0. 磁盘
disk_level, disk_msg = check_disk("C:")
report["disk"] = {"level": disk_level, "message": disk_msg}
if disk_level in ("warn", "critical"):
report["summary"][disk_level] += 1
report["recommendations"].append(f"磁盘空间不足: {disk_msg}")
# 1. 服务检查
for svc in SERVICES:
name = svc["name"]
entry = {"name": name}
report["services"].append(entry)
report["summary"]["total"] += 1
pid = 0
pid_file = svc.get("pid_file")
if pid_file:
try:
with open(pid_file) as f:
pid = int(f.read().strip())
except:
pid = 0
pid_ok = (pid > 0 and pid_alive(pid)) if pid_file else True # 无 PID 文件不视为异常
port_ok = port_open(svc["port"])
http_ok = False
http_msg = ""
if port_ok:
http_ok, http_msg = http_check(svc["health_url"], accept_401=svc.get("accept_401", False))
entry["pid"] = pid if pid_ok else 0
entry["pid_ok"] = pid_ok
entry["port_ok"] = port_ok
entry["http_ok"] = http_ok
entry["http_detail"] = http_msg
if pid_ok and port_ok and http_ok:
entry["status"] = "ok"
report["summary"]["ok"] += 1
elif pid_ok and port_ok and not http_ok:
entry["status"] = "degraded"
report["summary"]["warn"] += 1
report["recommendations"].append(f"{name}: PID/Port 正常但 /health 异常({http_msg}")
else:
entry["status"] = "fail"
report["summary"]["fail"] += 1
report["recommendations"].append(f"{name}: 离线(PID={pid_ok}, Port={port_ok}, HTTP={http_msg}")
# 2. 定时任务检查
report["cron_tasks"] = check_cron_tasks()
for t in report["cron_tasks"]:
if t["status"] == "missing":
report["recommendations"].append(f"定时任务 {t['name']} 缺失")
# 3. 看门狗日志新鲜度(最后修改时间)
watchdog_log = os.path.join(LOGS, "watchdog.log")
if os.path.exists(watchdog_log):
mtime = os.path.getmtime(watchdog_log)
age_hours = (time.time() - mtime) / 3600
report["watchdog_log_age_hours"] = round(age_hours, 1)
if age_hours > 1:
report["recommendations"].append(f"看门狗日志 {age_hours:.1f} 小时未更新,可能看门狗已死")
else:
report["watchdog_log_age_hours"] = None
report["recommendations"].append("看门狗日志不存在")
# 4. 输出报告
summary = report["summary"]
log(f"=== 每日健康检查: {summary['ok']}/{summary['total']} 正常, {summary['warn']} 降级, {summary['fail']} 离线 ===")
for svc in report["services"]:
emoji = {"ok": "[OK]", "degraded": "[WARN]", "fail": "[DOWN]"}.get(svc["status"], "[?]")
detail = svc.get("http_detail", "")
log(f" {emoji} {svc['name']} | PID={svc['pid_ok']} Port={svc['port_ok']} HTTP={detail}")
log(f" [DISK] {report['disk']['message']}")
for t in report["cron_tasks"]:
log(f" [CRON] {t['name']}: {t['status']}")
log(f" [WATCHDOG] 日志: {report.get('watchdog_log_age_hours', 'N/A')}h old")
if report["recommendations"]:
log("=== 建议动作 ===")
for r in report["recommendations"]:
log(f" - {r}")
# 保存 JSON 报告
with open(REPORT_FILE, "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
if __name__ == "__main__":
main()