Files
AgentsMeeting/gateway/scripts/agents_daily_health.py
T

196 lines
7.4 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.
#!/usr/bin/env python3
"""
agents_daily_health.py — Tier 2 每日全面健康检查(Linux/246 部署版)
==================================================================
每天 08:00 触发。检查范围:服务状态、磁盘空间、crontab 存活、看门狗日志新鲜度。
部署:Linux crontab: 0 8 * * * cd /home/hmo/AgentsMeeting/gateway/scripts && /usr/bin/python3 agents_daily_health.py >> ../logs/daily_health.log 2>&1
"""
import json, os, sys, time, subprocess, shutil
from datetime import datetime
from urllib.request import urlopen, Request
from urllib.error import HTTPError, URLError
try:
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
except Exception:
pass
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
GATEWAY_DIR = os.path.dirname(SCRIPT_DIR)
TEMP = os.path.join(GATEWAY_DIR, "temp")
LOGS = os.path.join(GATEWAY_DIR, "logs")
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
DISK_CRIT_GB = 2
# ── 服务列表(同 Tier1,但增加 depends_on / description)──
SERVICES = [
{"name": "dashboard", "host": "127.0.0.1", "port": 5803, "health_url": "http://127.0.0.1:5803/api/health", "depends_on": [], "desc": "管理门户"},
{"name": "hermes_gateway_mohe", "host": "127.0.0.1", "port": 8642, "health_url": "http://127.0.0.1:8642/v1/health", "depends_on": [], "desc": "莫荷 AI Gateway"},
{"name": "hermes_gateway_zhiwei","host": "127.0.0.1", "port": 8643, "health_url": "http://127.0.0.1:8643/v1/health", "depends_on": [], "desc": "知微 AI Gateway"},
{"name": "wechat_bridge", "host": "127.0.0.1", "port": 3001, "health_url": None, "depends_on": [], "desc": "微信桥接 Docker"},
{"name": "xmpp_bot_xxm", "host": "192.168.1.248", "port": 5807, "health_url": "http://192.168.1.248:5807/health", "depends_on": ["ejabberd"],"desc": "小小莫 XMPP Bot"},
{"name": "article_processor", "host": "192.168.1.248", "port": 5810, "health_url": "http://192.168.1.248:5810/health", "depends_on": [], "desc": "文章抓取服务"},
]
# ── 期望的定时任务(crontab 条目关键字)──────────────────
EXPECTED_CRON = [
"auto_heal.py",
"agents_health_check.py",
]
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 port_open(host, port, timeout=3):
import socket
try:
s = socket.create_connection((host, port), timeout=timeout)
s.close()
return True
except:
return False
def http_check(url, timeout=8):
if not url:
return (True, "no_health_url")
try:
req = Request(url)
with urlopen(req, timeout=timeout) as r:
return (r.status == 200, f"HTTP {r.status}")
except HTTPError as e:
return (e.code in (401, 403), f"HTTP {e.code}")
except Exception as e:
return (False, str(e)[:60])
def check_disk(path="/"):
"""Linux df 磁盘检查。"""
try:
usage = shutil.disk_usage(path)
free_gb = usage.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 Exception as e:
return ("unknown", f"disk check failed: {e}")
def check_cron_tasks():
"""检查 crontab 中是否包含期望的定时任务。"""
results = []
try:
r = subprocess.run(["crontab", "-l"], capture_output=True, text=True, timeout=5)
cron_text = r.stdout
for name in EXPECTED_CRON:
if name in cron_text:
results.append({"name": name, "status": "ok"})
else:
results.append({"name": name, "status": "missing"})
except Exception as e:
results.append({"name": "check_failed", "status": f"error: {e}"})
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("/")
report["disk"] = {"level": disk_level, "message": disk_msg}
if disk_level in ("warn", "critical"):
report["recommendations"].append(f"磁盘空间不足: {disk_msg}")
# 1. 服务检查
for svc in SERVICES:
name = svc["name"]
host = svc["host"]
port = svc["port"]
url = svc.get("health_url")
entry = {"name": name, "desc": svc.get("desc", "")}
report["services"].append(entry)
report["summary"]["total"] += 1
port_ok = port_open(host, port)
http_ok = False
http_msg = ""
if port_ok:
http_ok, http_msg = http_check(url, timeout=8)
entry["port_ok"] = port_ok
entry["http_ok"] = http_ok
entry["http_detail"] = http_msg
if port_ok and http_ok:
entry["status"] = "ok"
report["summary"]["ok"] += 1
elif port_ok and not http_ok:
entry["status"] = "degraded"
report["summary"]["warn"] += 1
report["recommendations"].append(f"{name}: 端口通但 /health 异常({http_msg}")
else:
entry["status"] = "fail"
report["summary"]["fail"] += 1
report["recommendations"].append(f"{name}: 离线(Port={port_ok}, HTTP={http_msg}")
# 2. Crontab 检查
report["cron_tasks"] = check_cron_tasks()
for t in report["cron_tasks"]:
if t["status"] == "missing":
report["recommendations"].append(f"定时任务 {t['name']} 缺失")
# 3. 看门狗日志新鲜度
wd_log = os.path.join(LOGS, "watchdog.log")
if os.path.exists(wd_log):
mtime = os.path.getmtime(wd_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}h 未更新")
else:
report["watchdog_log_age_hours"] = None
# 4. 输出
s = report["summary"]
log(f"=== 每日健康: {s['ok']}/{s['total']} OK, {s['warn']} degraded, {s['fail']} fail ===")
for svc in report["services"]:
emoji = {"ok": "[OK]", "degraded": "[WARN]", "fail": "[DOWN]"}.get(svc["status"], "[?]")
log(f" {emoji} {svc['name']} | Port={svc['port_ok']} HTTP={svc.get('http_detail','')}")
log(f" [DISK] {report['disk']['message']}")
for t in report["cron_tasks"]:
log(f" [CRON] {t['name']}: {t['status']}")
for r in report["recommendations"]:
log(f" -> {r}")
with open(REPORT_FILE, "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
if __name__ == "__main__":
main()