"""只读请求商务接口,记录耗时、完整数据摘要及 SQL;敏感内容必须存放仓库外。""" import argparse import hashlib import json import re import statistics import time import urllib.parse import urllib.request from pathlib import Path def log_offsets(directory): return {p: p.stat().st_size for p in directory.rglob("*.log")} def read_queries(directory, offsets): queries = [] files = list(directory.rglob("*.log")) files = [file for file in files if file.parent.name == "sql"] or files for file in files: with file.open("rb") as stream: stream.seek(offsets.get(file, 0)) for line in stream: try: row = json.loads(line) except (ValueError, UnicodeDecodeError): continue if row.get("gorm_logger") and "sql" in row: queries.append(row) return queries def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--runtime", type=Path, required=True) parser.add_argument("--base-url", default="http://127.0.0.1:19004") parser.add_argument("--phase", required=True) parser.add_argument("--repeat", type=int, default=2) parser.add_argument("--cases", type=Path) args = parser.parse_args() if args.runtime.resolve().is_relative_to(Path(__file__).resolve().parents[1]): raise ValueError("完整接口响应和 SQL 必须留在仓库外") output = args.runtime / args.phase output.mkdir(exist_ok=True) cases = json.loads((args.cases or args.runtime / "cases.json").read_text(encoding="utf-8")) token = (args.runtime / "new-admin.token").read_text(encoding="utf-8").strip() report = [] for case in cases: elapsed = [] for round_number in range(args.repeat): offsets = log_offsets(args.runtime / "sql-logs") url = args.base_url + case["path"] + "?" + urllib.parse.urlencode(case["params"], doseq=True) request = urllib.request.Request(url, headers={"x-token": token, "X-Request-Id": f"perf-{args.phase}-{case['name']}-{round_number}"}) started = time.monotonic() with urllib.request.urlopen(request, timeout=300) as response: body = json.load(response) elapsed.append(round((time.monotonic() - started) * 1000, 2)) if body.get("code") != 0: raise RuntimeError("请求失败:" + case["name"]) queries = read_queries(args.runtime / "sql-logs", offsets) data = body["data"] capture = {"case": case, "data": data, "elapsedMs": elapsed[-1], "queries": queries} (output / f"{case['name']}-{round_number}.json").write_text(json.dumps(capture, ensure_ascii=False, indent=2), encoding="utf-8") digest = hashlib.sha256(json.dumps(data, sort_keys=True, ensure_ascii=False).encode()).hexdigest() slow = sorted(queries, key=lambda row: row.get("elapsed_ms", 0), reverse=True)[:5] item = {"case": case["name"], "round": round_number, "elapsedMs": elapsed[-1], "dataSha256": digest, "queryCount": len(queries), "slowQueries": [{"elapsedMs": q.get("elapsed_ms"), "rows": q.get("rows"), "tables": list(dict.fromkeys(re.findall(r"(?:FROM|JOIN)\s+`?([\w.]+)", q["sql"], re.I)))} for q in slow]} report.append(item) (output / "report.json").write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") print(json.dumps(item), flush=True) print(json.dumps({"case": case["name"], "medianMs": statistics.median(elapsed)}), flush=True) if __name__ == "__main__": main()