docs(loadtest): hold/stress профили и правила безопасности IFT. Refs EventHub/EventHubBack#31 [skip ci]

This commit is contained in:
2026-07-15 16:00:33 +03:00
parent c1ba207374
commit 823274caab
10 changed files with 807 additions and 61 deletions
+155
View File
@@ -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())