Files
AgentsMeeting/gateway/scripts/agents_health_check.py
T

209 lines
6.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.
#!/usr/bin/env python3
"""
agents_health_check.py — Tier 1 快速健康检查(Linux/246 部署版)
============================================================
每 5 分钟触发。对所有注册服务做检查:PID存活 → 端口监听 → HTTP /health。
异常时写 TODO 给 self_todo_executor 消费。正常时静默。
部署:Linux crontab: */5 * * * * cd /home/hmo/AgentsMeeting/gateway/scripts && /usr/bin/python3 agents_health_check.py >> ../logs/health_check.log 2>&1
"""
import json, os, sys, socket, subprocess
from datetime import datetime
from urllib.request import urlopen, Request
from urllib.error import HTTPError, URLError
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(GATEWAY_DIR, "temp")
LOGS = os.path.join(GATEWAY_DIR, "logs")
os.makedirs(TEMP, exist_ok=True)
os.makedirs(LOGS, exist_ok=True)
NOW = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
TODO_FILE = os.path.join(TEMP, "health_todos.jsonl")
REPORT_FILE = os.path.join(TEMP, "last_health_check.json")
HEALTH_LOG = os.path.join(LOGS, "health_check_report.log")
# ── 服务注册表(Linux 246 视角)──────────────────────────────
# 本机服务用 127.0.0.1,远程服务用实际 IP + HTTP only
SERVICES = [
# === 本机服务(246===
{
"name": "dashboard",
"host": "127.0.0.1",
"port": 5803,
"health_url": "http://127.0.0.1:5803/api/health",
"fix_cmd": ["sudo", "systemctl", "restart", "agentsmeeting-dashboard"],
"remote": False,
"critical": True,
},
{
"name": "hermes_gateway_mohe",
"host": "127.0.0.1",
"port": 8642,
"health_url": "http://127.0.0.1:8642/v1/health",
"fix_cmd": ["sudo", "systemctl", "restart", "hermes-gateway@mohe"],
"remote": False,
"critical": True,
},
{
"name": "hermes_gateway_zhiwei",
"host": "127.0.0.1",
"port": 8643,
"health_url": "http://127.0.0.1:8643/v1/health",
"fix_cmd": ["sudo", "systemctl", "restart", "hermes-gateway@zhiwei"],
"remote": False,
"critical": True,
},
{
"name": "wechat_bridge",
"host": "127.0.0.1",
"port": 3001,
"health_url": None, # TCP only
"fix_cmd": ["docker", "restart", "wxBotWebhook"],
"remote": False,
"critical": True,
},
# === 远程服务(跨网 HTTP 检查)===
{
"name": "xmpp_bot_xxm",
"host": "192.168.1.16",
"port": 5802,
"health_url": "http://192.168.1.16:5802/health",
"fix_cmd": None, # 远程服务无法自动修复
"remote": True,
"critical": True,
},
{
"name": "article_processor",
"host": "192.168.1.16",
"port": 5810,
"health_url": "http://192.168.1.16:5810/health",
"fix_cmd": None,
"remote": True,
"critical": False,
},
]
# ── 检查基元(Linux 原生)────────────────────────────────
def log(msg):
ts = datetime.now().strftime("%H:%M:%S")
line = f"[{ts}] {msg}"
with open(HEALTH_LOG, "a", encoding="utf-8") as f:
f.write(line + "\n")
def pid_alive(pid):
"""发送信号 0 测试进程存活。"""
try:
os.kill(pid, 0)
return True
except (ProcessLookupError, PermissionError):
return False
def port_open(host, port, timeout=3):
"""socket connect 测试端口是否监听。"""
try:
s = socket.create_connection((host, port), timeout=timeout)
s.close()
return True
except (socket.timeout, ConnectionRefusedError, OSError):
return False
def http_check(url, timeout=5):
"""HTTP /health 检查。返回 (ok, detail)。"""
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 URLError as e:
return (False, str(e.reason)[:60])
except Exception as e:
return (False, str(e)[:60])
def write_todo(name, issue):
"""写 TODO 供 executor 消费。"""
entry = {
"created": NOW,
"service": name,
"issue": issue,
"status": "pending",
}
with open(TODO_FILE, "a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
log(f"TODO: {name}{issue}")
# ── 主检查 ────────────────────────────────────────────
def main():
report = {"time": NOW, "services": [], "summary": {"total": 0, "ok": 0, "fail": 0}}
dirty = False
for svc in SERVICES:
name = svc["name"]
host = svc["host"]
port = svc["port"]
url = svc.get("health_url")
is_remote = svc.get("remote", False)
critical = svc.get("critical", True)
entry = {"name": name, "critical": critical}
report["services"].append(entry)
report["summary"]["total"] += 1
# 1. Port checksocket connect,跨平台)
port_ok = port_open(host, port)
entry["port_ok"] = port_ok
# 2. HTTP check
if port_ok:
ok, detail = http_check(url, timeout=5 if is_remote else 8)
entry["http_ok"] = ok
entry["http_detail"] = detail
else:
entry["http_ok"] = False
entry["http_detail"] = "port_closed"
# 3. 判定:Port+HTTP OK = 健康
primary_ok = port_ok and entry.get("http_ok", False)
entry["status"] = "ok" if primary_ok else "fail"
if primary_ok:
report["summary"]["ok"] += 1
else:
report["summary"]["fail"] += 1
dirty = True
reasons = []
if not port_ok:
reasons.append("port_closed")
if port_ok and not entry.get("http_ok"):
reasons.append(f"http_{entry.get('http_detail','?')}")
issue = f"异常: {' + '.join(reasons)}"
write_todo(name, issue)
# 输出报告
summary = report["summary"]
if dirty:
log(f"=== 健康检查: {summary['ok']}/{summary['total']} OK, {summary['fail']} FAIL ===")
for s in report["services"]:
if s["status"] == "fail":
print(f"[FAIL] {s['name']}: PORT={s['port_ok']} HTTP={s.get('http_detail','?')}")
else:
log(f"=== 健康检查: {summary['ok']}/{summary['total']} 全部正常 ===")
with open(REPORT_FILE, "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2)
if __name__ == "__main__":
main()