Files
AgentsMeeting/gateway/scripts/agents_health_check.py
T

193 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.
"""
agents_health_check.py — Tier 1 快速健康检查
每 5 分钟(Task Scheduler)触发。对所有注册服务做三合一检查:
PID存活 → 端口监听 → HTTP /health
异常时写 TODOhealth_todos.jsonl),由 self_todo_executor 消费。
正常时不输出任何噪音(silent-by-default)。
"""
import json, os, sys, time, subprocess, urllib.request, urllib.error
from datetime import datetime
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
# 从共享注册表导入服务定义
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(TEMP, exist_ok=True)
os.makedirs(LOGS, exist_ok=True)
# 转成 Tier 1 需要的格式(补充 fix_* 字段)
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),
"fix_script": svc["script"],
"fix_args": svc["args"],
"fix_cwd": svc["workdir"],
"remote": svc.get("remote", False),
})
TODO_FILE = os.path.join(TEMP, "health_todos.jsonl")
HEALTH_LOG = os.path.join(LOGS, "health_check_report.log")
NOW = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# ── Helpers ──────────────────────────────────────────────
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 read_pid(path):
try:
with open(path) as f:
return int(f.read().strip())
except:
return 0
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):
try:
r = urllib.request.urlopen(url, timeout=timeout)
return (r.status == 200, f"HTTP {r.status}")
except urllib.error.HTTPError as e:
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 write_todo(name, issue, fix_script, fix_args, fix_cwd):
"""写一条 TODO 给 self_todo_executor 消费。
JSONL 每行一条,含 service/issue/时间/fix_action。
"""
entry = {
"created": NOW,
"service": name,
"issue": issue,
"fix_script": fix_script,
"fix_args": fix_args,
"fix_cwd": fix_cwd,
"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}")
# ── Main check ───────────────────────────────────────────
def main():
report = {"time": NOW, "services": [], "summary": {"total": 0, "ok": 0, "fail": 0}}
dirty = False
for svc in SERVICES:
name = svc["name"]
entry = {"name": name}
report["services"].append(entry)
report["summary"]["total"] += 1
# 1. PID check(允许无 PID 文件的服务,仅靠 port+HTTP 判断)
pid_file = svc.get("pid_file")
if pid_file:
pid = read_pid(pid_file)
pid_ok = pid > 0 and pid_alive(pid)
else:
pid = 0
pid_ok = True # 无 PID 文件不视为异常
entry["pid"] = pid if pid_ok else 0
entry["pid_ok"] = pid_ok
# 2. Port check(远程服务跳过本地端口检测)
is_remote = svc.get("remote", False)
if is_remote:
port_ok = True # 远程服务不检查本地端口
else:
port_ok = port_open(svc["port"])
entry["port_ok"] = port_ok
# 3. HTTP health check(远程服务直接用 HTTP 判断)
http_msg = ""
if port_ok or is_remote:
ok, detail = http_check(svc["health_url"])
# accept 401/403 as healthy if configured
if not ok and svc.get("accept_401") and ("401" in detail or "403" in detail):
http_msg = "auth (alive)"
ok = True
else:
http_msg = detail if not ok else "ok"
entry["http_ok"] = ok
entry["http_detail"] = http_msg
else:
entry["http_ok"] = False
entry["http_detail"] = "port_closed"
# 健康判定:port+HTTP 都 OK → 健康;PID 仅作为辅助信号,不阻塞判定
primary_ok = port_ok and entry.get("http_ok", False)
entry["status"] = "ok" if primary_ok else "fail"
if primary_ok:
if not pid_ok:
entry["pid_warning"] = True # PID 异常但服务在正常运行
report["summary"]["ok"] += 1
else:
report["summary"]["fail"] += 1
dirty = True
# 收集具体失败原因
reasons = []
if not port_ok and not is_remote: reasons.append("port_closed")
if not entry.get("http_ok"): reasons.append(f"http_{http_msg}")
issue = f"异常: {' + '.join(reasons)}"
write_todo(name, issue, svc["fix_script"], svc.get("fix_args", []), svc["fix_cwd"])
# 只在有异常时写完整报告 + 输出到 stdout(给 Task Scheduler 日志)
if dirty:
summary = report["summary"]
log(f"=== 健康检查: {summary['ok']}/{summary['total']} 正常, {summary['fail']} 异常 ===")
# 只输出异常服务到 stdoutsilent-by-default
for svc in report["services"]:
if svc["status"] != "ok":
print(f"[FAIL] {svc['name']}: PORT={svc['port_ok']} HTTP={svc.get('http_detail','?')}")
elif svc.get("pid_warning"):
print(f"[WARN] {svc['name']}: PID异常但服务正常")
else:
# 全正常→完全静默(silent-by-default
pass
# 保留报告到文件(供 dashboard 读取)
with open(os.path.join(TEMP, "last_health_check.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False)
if __name__ == "__main__":
main()