158 lines
5.8 KiB
Python
158 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import time
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
|
|
import paramiko
|
|
|
|
|
|
def _now() -> datetime:
|
|
return datetime.now()
|
|
|
|
|
|
def _parse_key_value_lines(raw: str) -> dict:
|
|
result: dict[str, object] = {"raw": raw.strip()}
|
|
for line in raw.splitlines():
|
|
if "=" not in line:
|
|
continue
|
|
key, value = line.split("=", 1)
|
|
key = key.strip()
|
|
value = value.strip()
|
|
if not key:
|
|
continue
|
|
if key in {"running_jobs", "step_counts"}:
|
|
try:
|
|
result[key] = json.loads(value)
|
|
except Exception:
|
|
result[key] = value
|
|
continue
|
|
for caster in (int, float):
|
|
try:
|
|
result[key] = caster(value)
|
|
break
|
|
except Exception:
|
|
continue
|
|
else:
|
|
result[key] = value
|
|
return result
|
|
|
|
|
|
def _remote_sample(host: str, user: str, password: str, recent_minutes: int) -> dict:
|
|
cli = paramiko.SSHClient()
|
|
cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
command = """
|
|
python3 - <<'PY'
|
|
import json, re, subprocess
|
|
|
|
def sh(cmd: str) -> str:
|
|
return subprocess.check_output(cmd, shell=True, text=True, stderr=subprocess.DEVNULL)
|
|
|
|
cpu_line = sh("top -bn1 | sed -n '3p'").strip()
|
|
idle_match = re.search(r'([0-9]+(?:\.[0-9]+)?)\s+id', cpu_line)
|
|
busy = round(100.0 - float(idle_match.group(1)), 2) if idle_match else None
|
|
print(f"cpu_busy_pct={busy if busy is not None else ''}")
|
|
|
|
worker_stats = sh(r'''ps -eo pcpu,cmd | awk '/[d]etect_worker.py/ {cpu=$1+0; total++; sum+=cpu; if(cpu>1) gt1++; if(cpu>2) gt2++; if(cpu>3) gt3++;} END {printf("workers_total=%d\\nworkers_gt_1pct=%d\\nworkers_gt_2pct=%d\\nworkers_gt_3pct=%d\\nworkers_cpu_sum=%.1f\\n", total, gt1, gt2, gt3, sum)}' ''')
|
|
print(worker_stats.strip())
|
|
|
|
running_jobs_raw = sh(r'''/www/server/pgsql/bin/psql -U postgres -d domain -At -c "select id, job_code, status from detect_jobs where status='running' order by id desc limit 16;" ''')
|
|
running_jobs = []
|
|
for line in running_jobs_raw.splitlines():
|
|
parts = line.split("|")
|
|
if len(parts) >= 3:
|
|
running_jobs.append({"job_id": int(parts[0]), "job_code": parts[1], "status": parts[2]})
|
|
print("running_jobs=" + json.dumps(running_jobs, ensure_ascii=False))
|
|
|
|
recent = __RECENT_MINUTES__
|
|
journal_cmd = f"journalctl -u domaincheck-worker -u 'domaincheck-worker@*' --since '{recent} min ago' --no-pager -o cat"
|
|
logs = sh(journal_cmd)
|
|
step_counts = {}
|
|
for match in re.finditer(r'detect_order=(detect_[a-zA-Z0-9_]+)', logs):
|
|
step = match.group(1)
|
|
step_counts[step] = step_counts.get(step, 0) + 1
|
|
print("step_counts=" + json.dumps(step_counts, ensure_ascii=False, sort_keys=True))
|
|
PY
|
|
"""
|
|
command = command.replace("__RECENT_MINUTES__", str(int(recent_minutes)))
|
|
try:
|
|
cli.connect(hostname=host, username=user, password=password, timeout=15, banner_timeout=15, auth_timeout=15)
|
|
_, stdout, stderr = cli.exec_command(command, timeout=90)
|
|
output = stdout.read().decode("utf-8", errors="replace")
|
|
error = stderr.read().decode("utf-8", errors="replace").strip()
|
|
row = _parse_key_value_lines(output)
|
|
if error:
|
|
row["stderr"] = error
|
|
return row
|
|
except Exception as exc:
|
|
return {"error": str(exc)}
|
|
finally:
|
|
try:
|
|
cli.close()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Collect remote CPU/worker/step-mix metrics into JSONL.")
|
|
parser.add_argument("--worker-host", required=True)
|
|
parser.add_argument("--worker-user", required=True)
|
|
parser.add_argument("--worker-password", required=True)
|
|
parser.add_argument("--interval-seconds", type=int, default=120)
|
|
parser.add_argument("--duration-hours", type=float, default=8.0)
|
|
parser.add_argument("--recent-minutes", type=int, default=3)
|
|
parser.add_argument("--label", default="remote-step-mix")
|
|
args = parser.parse_args()
|
|
|
|
start_time = _now()
|
|
end_time = start_time + timedelta(hours=max(0.25, float(args.duration_hours)))
|
|
run_dir = Path("/www/wwwroot/getDomain/docs/ops_center_runtime/night_runs") / (
|
|
f"step_mix_{start_time.strftime('%Y%m%d_%H%M%S')}_{args.label}"
|
|
)
|
|
run_dir.mkdir(parents=True, exist_ok=True)
|
|
metrics_path = run_dir / "metrics.jsonl"
|
|
meta_path = run_dir / "meta.json"
|
|
meta_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"label": args.label,
|
|
"started_at": start_time.isoformat(sep=" ", timespec="seconds"),
|
|
"planned_end_at": end_time.isoformat(sep=" ", timespec="seconds"),
|
|
"interval_seconds": int(args.interval_seconds),
|
|
"recent_minutes": int(args.recent_minutes),
|
|
"worker_host": args.worker_host,
|
|
},
|
|
ensure_ascii=False,
|
|
indent=2,
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
sample_index = 0
|
|
while _now() < end_time:
|
|
sample_index += 1
|
|
row = {
|
|
"timestamp": _now().isoformat(sep=" ", timespec="seconds"),
|
|
"sample_index": sample_index,
|
|
"remote_worker": _remote_sample(
|
|
host=args.worker_host,
|
|
user=args.worker_user,
|
|
password=args.worker_password,
|
|
recent_minutes=max(1, int(args.recent_minutes)),
|
|
),
|
|
}
|
|
with metrics_path.open("a", encoding="utf-8") as fp:
|
|
fp.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
time.sleep(max(30, int(args.interval_seconds)))
|
|
|
|
(run_dir / "done.flag").write_text(_now().isoformat(sep=" ", timespec="seconds"), encoding="utf-8")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|