166 lines
6.1 KiB
Python
166 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import time
|
|
from datetime import datetime, timezone
|
|
from typing import Dict, List, Optional
|
|
|
|
|
|
def run_ssh(remote_cmd: str, timeout: int) -> str:
|
|
proc = subprocess.run(
|
|
[
|
|
"python3",
|
|
"-c",
|
|
(
|
|
"import paramiko,sys;"
|
|
"host,user,password,cmd,timeout=sys.argv[1:6];"
|
|
"client=paramiko.SSHClient();"
|
|
"client.set_missing_host_key_policy(paramiko.AutoAddPolicy());"
|
|
"client.connect(host, username=user, password=password, timeout=int(timeout));"
|
|
"stdin,stdout,stderr=client.exec_command(cmd, timeout=int(timeout));"
|
|
"sys.stdout.write(stdout.read().decode('utf-8','ignore'));"
|
|
"err=stderr.read().decode('utf-8','ignore');"
|
|
"client.close();"
|
|
"sys.stderr.write(err)"
|
|
),
|
|
os.environ["REMOTE_HOST"],
|
|
os.environ["REMOTE_USER"],
|
|
os.environ["REMOTE_PASS"],
|
|
remote_cmd,
|
|
str(timeout),
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=timeout + 10,
|
|
)
|
|
if proc.returncode != 0 and proc.stderr.strip():
|
|
raise RuntimeError(proc.stderr.strip())
|
|
return proc.stdout
|
|
|
|
|
|
def parse_cpu_busy(text: str) -> Optional[float]:
|
|
match = re.search(r"([0-9]+(?:\.[0-9]+)?)\s*id", text)
|
|
if not match:
|
|
return None
|
|
idle = float(match.group(1))
|
|
return round(max(0.0, 100.0 - idle), 2)
|
|
|
|
|
|
def collect_snapshot(gray_workers: List[str], since: str) -> dict:
|
|
units = " ".join(f"-u domaincheck-worker@{worker}.service" for worker in gray_workers)
|
|
remote_cmd = (
|
|
"echo '###CPU'; "
|
|
"top -bn1 | grep '%Cpu' || true; "
|
|
"echo '###GRAY'; "
|
|
f"journalctl {units} --since '{since}' --no-pager "
|
|
"| grep -E 'proxy_direct_fallback|站长之家调用边界|single_step_finalized|重试预算耗尽' "
|
|
"| tail -n 1200 || true; "
|
|
"echo '###GLOBAL'; "
|
|
"journalctl -u 'domaincheck-worker@*.service' "
|
|
f"--since '{since}' --no-pager "
|
|
"| grep 'detect_order=detect_chinaz' | tail -n 1200 || true"
|
|
)
|
|
out = run_ssh(remote_cmd, timeout=40)
|
|
cpu_part = ""
|
|
gray_part = ""
|
|
global_part = ""
|
|
if "###GRAY" in out:
|
|
cpu_part, rest = out.split("###GRAY", 1)
|
|
if "###GLOBAL" in rest:
|
|
gray_part, global_part = rest.split("###GLOBAL", 1)
|
|
else:
|
|
gray_part = rest
|
|
else:
|
|
cpu_part = out
|
|
cpu_busy = parse_cpu_busy(cpu_part)
|
|
|
|
counts = {
|
|
"proxy_direct_fallback": 0,
|
|
"boundary_proxy_yes": 0,
|
|
"boundary_proxy_no": 0,
|
|
"single_step_finalized": 0,
|
|
"retry_budget_exhausted": 0,
|
|
}
|
|
per_worker: Dict[str, Dict[str, int]] = {}
|
|
for line in gray_part.splitlines():
|
|
worker_match = re.search(r"domaincheck-worker@([a-z0-9]+)\[", line)
|
|
worker = worker_match.group(1) if worker_match else "-"
|
|
bucket = per_worker.setdefault(
|
|
worker,
|
|
{
|
|
"proxy_direct_fallback": 0,
|
|
"boundary_proxy_yes": 0,
|
|
"boundary_proxy_no": 0,
|
|
"single_step_finalized": 0,
|
|
"retry_budget_exhausted": 0,
|
|
},
|
|
)
|
|
if "proxy_direct_fallback" in line:
|
|
counts["proxy_direct_fallback"] += 1
|
|
bucket["proxy_direct_fallback"] += 1
|
|
if "站长之家调用边界" in line and "proxy=yes" in line:
|
|
counts["boundary_proxy_yes"] += 1
|
|
bucket["boundary_proxy_yes"] += 1
|
|
if "站长之家调用边界" in line and "proxy=no" in line:
|
|
counts["boundary_proxy_no"] += 1
|
|
bucket["boundary_proxy_no"] += 1
|
|
if "single_step_finalized" in line and "detect_key=detect_chinaz" in line:
|
|
counts["single_step_finalized"] += 1
|
|
bucket["single_step_finalized"] += 1
|
|
if "重试预算耗尽" in line and "站长之家检测" in line:
|
|
counts["retry_budget_exhausted"] += 1
|
|
bucket["retry_budget_exhausted"] += 1
|
|
|
|
global_worker_counts: Dict[str, int] = {}
|
|
if global_part.strip():
|
|
top_counts: Dict[str, int] = {}
|
|
for line in global_part.splitlines():
|
|
worker_match = re.search(r"domaincheck-worker@([a-z0-9]+)\[", line)
|
|
if not worker_match:
|
|
continue
|
|
worker = worker_match.group(1)
|
|
top_counts[worker] = top_counts.get(worker, 0) + 1
|
|
global_worker_counts = dict(
|
|
sorted(top_counts.items(), key=lambda item: item[1], reverse=True)[:12]
|
|
)
|
|
|
|
return {
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"cpu_busy_pct": cpu_busy,
|
|
"counts": counts,
|
|
"per_worker": per_worker,
|
|
"global_detect_chinaz_orders": sum(global_worker_counts.values()),
|
|
"global_detect_chinaz_top_workers": global_worker_counts,
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--output", required=True)
|
|
parser.add_argument("--workers", required=True, help="comma-separated worker suffixes")
|
|
parser.add_argument("--duration-seconds", type=int, default=3600)
|
|
parser.add_argument("--interval-seconds", type=int, default=30)
|
|
parser.add_argument("--since-window-seconds", type=int, default=45)
|
|
args = parser.parse_args()
|
|
|
|
gray_workers = [item.strip() for item in args.workers.split(",") if item.strip()]
|
|
os.makedirs(os.path.dirname(args.output), exist_ok=True)
|
|
deadline = time.time() + max(0, args.duration_seconds)
|
|
while time.time() <= deadline:
|
|
since_ts = datetime.fromtimestamp(
|
|
time.time() - max(1, args.since_window_seconds),
|
|
tz=timezone.utc,
|
|
).astimezone().strftime("%Y-%m-%d %H:%M:%S")
|
|
snapshot = collect_snapshot(gray_workers, since_ts)
|
|
with open(args.output, "a", encoding="utf-8") as fh:
|
|
fh.write(json.dumps(snapshot, ensure_ascii=False) + "\n")
|
|
time.sleep(max(1, args.interval_seconds))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|