77 lines
3.6 KiB
Python
77 lines
3.6 KiB
Python
"""只读请求商务接口,记录耗时、完整数据摘要及 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()
|