docs(loadtest): hold/stress профили и правила безопасности IFT. Refs EventHub/EventHubBack#31 [skip ci]
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Summarize Tsung log against stress boundary 2A.
|
||||
|
||||
Boundary hit if:
|
||||
- error rate > 1% (4xx+5xx+nomatch+match_stop relative to requests), OR
|
||||
- cumulative mean request > 500 ms
|
||||
|
||||
Usage: summarize-tsung-stress.py [path/to/tsung.log]
|
||||
Default: newest ~/.tsung/log/*/tsung.log
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def latest_log() -> Path:
|
||||
root = Path.home() / ".tsung" / "log"
|
||||
dirs = sorted([p for p in root.iterdir() if p.is_dir()], reverse=True)
|
||||
for d in dirs:
|
||||
cand = d / "tsung.log"
|
||||
if cand.is_file():
|
||||
return cand
|
||||
raise SystemExit("No tsung.log under ~/.tsung/log")
|
||||
|
||||
|
||||
def last_stats(text: str, key: str) -> list[str] | None:
|
||||
ms = list(re.finditer(rf"^stats: {re.escape(key)} (.+)$", text, re.M))
|
||||
if not ms:
|
||||
return None
|
||||
return ms[-1].group(1).split()
|
||||
|
||||
|
||||
def cumul_count(fields: list[str] | None) -> int:
|
||||
"""Tsung dump: for counters usually last field is cumulative total."""
|
||||
if not fields:
|
||||
return 0
|
||||
return int(float(fields[-1]))
|
||||
|
||||
|
||||
def mean_request_ms(fields: list[str] | None) -> float | None:
|
||||
"""request line: count mean stddev max min mean_cumul count_cumul (approx)."""
|
||||
if not fields or len(fields) < 6:
|
||||
return None
|
||||
# Prefer overall mean (field index 5 in dump = running mean of means in later dumps)
|
||||
try:
|
||||
return float(fields[5])
|
||||
except (IndexError, ValueError):
|
||||
return float(fields[1])
|
||||
|
||||
|
||||
def phase_means(text: str) -> list[tuple[int, float, int]]:
|
||||
"""Per-dump mean from request lines: (dump_idx, mean_interval, cumul_count)."""
|
||||
out = []
|
||||
dumps = text.split("# stats: dump")
|
||||
for i, block in enumerate(dumps[1:], start=1):
|
||||
m = re.search(r"^stats: request (.+)$", block, re.M)
|
||||
if not m:
|
||||
continue
|
||||
f = m.group(1).split()
|
||||
if len(f) >= 2:
|
||||
try:
|
||||
out.append((i, float(f[1]), cumul_count(f)))
|
||||
except ValueError:
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
def main() -> int:
|
||||
log = Path(sys.argv[1]) if len(sys.argv) > 1 else latest_log()
|
||||
text = log.read_text(encoding="utf-8", errors="replace")
|
||||
print(f"log={log}")
|
||||
|
||||
req = last_stats(text, "request")
|
||||
users = last_stats(text, "users_count")
|
||||
finish = last_stats(text, "finish_users_count")
|
||||
match = last_stats(text, "match")
|
||||
nomatch = last_stats(text, "nomatch")
|
||||
match_stop = last_stats(text, "match_stop")
|
||||
|
||||
codes = {}
|
||||
for code in ("200", "201", "400", "401", "403", "404", "429", "500", "502", "503"):
|
||||
st = last_stats(text, code)
|
||||
if st:
|
||||
codes[code] = cumul_count(st)
|
||||
|
||||
transport_errs = {}
|
||||
for key in (
|
||||
"error_connect_closed",
|
||||
"error_timeout",
|
||||
"error_abort_max_conn_retries",
|
||||
"error_connect_econnrefused",
|
||||
"error_connect_etimedout",
|
||||
"error_unknown",
|
||||
):
|
||||
st = last_stats(text, key)
|
||||
if st:
|
||||
transport_errs[key] = cumul_count(st)
|
||||
|
||||
n_req = cumul_count(req)
|
||||
n_nomatch = cumul_count(nomatch)
|
||||
n_stop = cumul_count(match_stop)
|
||||
n_4xx = sum(v for k, v in codes.items() if k.startswith("4"))
|
||||
n_5xx = sum(v for k, v in codes.items() if k.startswith("5"))
|
||||
n_2xx = sum(v for k, v in codes.items() if k.startswith("2"))
|
||||
n_transport = sum(transport_errs.values())
|
||||
# Denominator: completed HTTP + transport failures (attempts that never got status)
|
||||
attempts = max(n_req + n_transport, 1)
|
||||
err = n_4xx + n_5xx + n_nomatch + n_stop + n_transport
|
||||
err_rate = err / attempts * 100.0
|
||||
mean_ms = mean_request_ms(req)
|
||||
|
||||
print(f"users_started={cumul_count(users)} finished={cumul_count(finish)}")
|
||||
print(f"requests={n_req} match={cumul_count(match)} nomatch={n_nomatch} match_stop={n_stop}")
|
||||
print(f"http_2xx={n_2xx} http_4xx={n_4xx} http_5xx={n_5xx} codes={codes}")
|
||||
print(f"transport_errors={transport_errs} transport_total={n_transport}")
|
||||
print(f"attempts≈{n_req + n_transport} error_units={err} error_rate_pct={err_rate:.3f}")
|
||||
print(f"mean_request_ms_cumul={mean_ms}")
|
||||
|
||||
# Hold-ish: last third of dumps (late phases)
|
||||
means = phase_means(text)
|
||||
if means:
|
||||
tail = means[max(0, len(means) * 2 // 3) :]
|
||||
hold_means = [m for _, m, _ in tail if m > 0]
|
||||
hold_mean = sum(hold_means) / len(hold_means) if hold_means else None
|
||||
print(f"dumps={len(means)} late_interval_mean_ms={hold_mean}")
|
||||
peak_interval = max(means, key=lambda t: t[1])
|
||||
print(f"peak_interval_mean_ms={peak_interval[1]} at_dump={peak_interval[0]}")
|
||||
else:
|
||||
hold_mean = None
|
||||
|
||||
boundary = False
|
||||
reasons = []
|
||||
if err_rate > 1.0:
|
||||
boundary = True
|
||||
reasons.append(f"error_rate {err_rate:.3f}% > 1%")
|
||||
check_mean = mean_ms if mean_ms is not None else hold_mean
|
||||
if check_mean is not None and check_mean > 500:
|
||||
boundary = True
|
||||
reasons.append(f"mean_request {check_mean:.1f} ms > 500 ms")
|
||||
if hold_mean is not None and hold_mean > 500:
|
||||
boundary = True
|
||||
if f"mean_request {hold_mean:.1f} ms > 500 ms" not in reasons:
|
||||
reasons.append(f"late_phase_mean {hold_mean:.1f} ms > 500 ms")
|
||||
|
||||
verdict = "BOUNDARY_HIT" if boundary else "UNDER_THRESHOLD"
|
||||
print(f"verdict={verdict}")
|
||||
if reasons:
|
||||
print("reasons=" + "; ".join(reasons))
|
||||
return 0 if not boundary else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user