overload-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- overload/__init__.py +3 -0
- overload/__main__.py +5 -0
- overload/cli.py +393 -0
- overload/collection/__init__.py +1 -0
- overload/collection/environment.py +23 -0
- overload/collection/models.py +88 -0
- overload/collection/parser.py +220 -0
- overload/collection/variables.py +84 -0
- overload/config_file.py +73 -0
- overload/engine/__init__.py +1 -0
- overload/engine/assertions.py +151 -0
- overload/engine/auth.py +87 -0
- overload/engine/events.py +50 -0
- overload/engine/http_client.py +274 -0
- overload/engine/load_patterns.py +730 -0
- overload/engine/models.py +254 -0
- overload/engine/rate_limiter.py +124 -0
- overload/engine/runner.py +86 -0
- overload/report/__init__.py +1 -0
- overload/report/exporters.py +77 -0
- overload/report/generator.py +71 -0
- overload/report/templates/report.html +369 -0
- overload/utils/__init__.py +1 -0
- overload/utils/naming.py +26 -0
- overload/web/__init__.py +1 -0
- overload/web/app.py +38 -0
- overload/web/routes/__init__.py +1 -0
- overload/web/routes/api.py +461 -0
- overload/web/routes/ws.py +77 -0
- overload/web/static/css/app.css +242 -0
- overload/web/static/js/app.js +241 -0
- overload/web/static/js/charts.js +385 -0
- overload/web/static/js/collection.js +344 -0
- overload/web/static/js/runner.js +625 -0
- overload/web/templates/index.html +23 -0
- overload_cli-0.1.0.dist-info/METADATA +267 -0
- overload_cli-0.1.0.dist-info/RECORD +40 -0
- overload_cli-0.1.0.dist-info/WHEEL +4 -0
- overload_cli-0.1.0.dist-info/entry_points.txt +2 -0
- overload_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>Overload Report — {{ run_id }}</title>
|
|
7
|
+
<style>
|
|
8
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
9
|
+
:root{--bg:#f5f6fa;--sur:#ffffff;--sur2:#f0f1f5;--bdr:#dde1ec;--ok:#0e8a5f;--bad:#c0392b;--mid:#b7600a;--txt:#1a1d2e;--mut:#6b7280;--blue:#1d5fa8;--ok-bg:#e6f4ef;--bad-bg:#fdecea;--mid-bg:#fef3e6}
|
|
10
|
+
body{background:var(--bg);color:var(--txt);font-family:ui-monospace,'Cascadia Code','JetBrains Mono',monospace;font-size:12px;line-height:1.6;min-height:100vh}
|
|
11
|
+
.hdr{background:#fff;border-bottom:1px solid var(--bdr);padding:20px 32px 14px;box-shadow:0 1px 3px rgba(0,0,0,.06)}
|
|
12
|
+
.lbl{font-size:10px;font-weight:700;letter-spacing:.18em;text-transform:uppercase;color:var(--ok);margin-bottom:4px}
|
|
13
|
+
h1{font-size:20px;font-weight:800;color:var(--txt);letter-spacing:-.3px}
|
|
14
|
+
.meta{display:flex;gap:16px;margin-top:8px;flex-wrap:wrap}
|
|
15
|
+
.mi{color:var(--mut);font-size:11px}
|
|
16
|
+
.mi b{color:var(--txt);font-weight:600}
|
|
17
|
+
nav{display:flex;gap:2px;padding:6px 32px;background:#fff;border-bottom:1px solid var(--bdr)}
|
|
18
|
+
nav a{color:var(--mut);text-decoration:none;font-size:11px;padding:4px 10px;border-radius:4px;cursor:pointer}
|
|
19
|
+
nav a:hover{background:var(--sur2);color:var(--txt)}
|
|
20
|
+
.main{padding:22px 32px;max-width:1280px;margin:0 auto}
|
|
21
|
+
.sec{margin-bottom:32px}
|
|
22
|
+
.sh{font-size:13px;font-weight:700;color:var(--txt);margin-bottom:12px;display:flex;align-items:center;gap:8px}
|
|
23
|
+
.sh::after{content:'';flex:1;height:1px;background:var(--bdr)}
|
|
24
|
+
.verdict{display:flex;align-items:center;gap:12px;padding:12px 18px;border-radius:6px;margin-bottom:18px;border:1px solid}
|
|
25
|
+
.pass{background:var(--ok-bg);border-color:#a3d9bf}
|
|
26
|
+
.fail{background:var(--bad-bg);border-color:#f5b7b1}
|
|
27
|
+
.vi{font-size:20px}
|
|
28
|
+
.vt{font-size:13px;font-weight:700}
|
|
29
|
+
.vs{color:var(--mut);font-size:11px;margin-top:2px}
|
|
30
|
+
.kg{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:8px;margin-bottom:14px}
|
|
31
|
+
.kpi{background:var(--sur);border:1px solid var(--bdr);border-radius:6px;padding:12px 14px;position:relative;overflow:hidden}
|
|
32
|
+
.kpi::before{content:'';position:absolute;top:0;left:0;right:0;height:3px}
|
|
33
|
+
.t-ok::before{background:var(--ok)}
|
|
34
|
+
.t-bad::before{background:var(--bad)}
|
|
35
|
+
.t-mid::before{background:var(--mid)}
|
|
36
|
+
.kl{font-size:10px;color:var(--mut);text-transform:uppercase;letter-spacing:.07em;margin-bottom:5px}
|
|
37
|
+
.kv{font-size:22px;font-weight:800}
|
|
38
|
+
.v-ok{color:var(--ok)}
|
|
39
|
+
.v-bad{color:var(--bad)}
|
|
40
|
+
.v-mid{color:var(--mid)}
|
|
41
|
+
.ks{font-size:10px;color:var(--mut);margin-top:2px}
|
|
42
|
+
.two{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px}
|
|
43
|
+
@media(max-width:700px){.two{grid-template-columns:1fr}}
|
|
44
|
+
.cw{background:var(--sur);border:1px solid var(--bdr);border-radius:6px;padding:14px}
|
|
45
|
+
.ct{font-size:10px;color:var(--mut);text-transform:uppercase;letter-spacing:.1em;margin-bottom:10px}
|
|
46
|
+
canvas{display:block;width:100%!important;height:180px!important}
|
|
47
|
+
.tw{background:var(--sur);border:1px solid var(--bdr);border-radius:6px;overflow:hidden;margin-bottom:12px}
|
|
48
|
+
table{width:100%;border-collapse:collapse;font-size:11px}
|
|
49
|
+
thead th{background:var(--sur2);padding:7px 10px;text-align:left;font-size:10px;color:var(--mut);text-transform:uppercase;letter-spacing:.07em;border-bottom:1px solid var(--bdr)}
|
|
50
|
+
tbody tr{border-bottom:1px solid var(--sur2)}
|
|
51
|
+
tbody tr:last-child{border-bottom:none}
|
|
52
|
+
tbody tr:hover{background:var(--sur2)}
|
|
53
|
+
td{padding:7px 10px}
|
|
54
|
+
.badge{display:inline-block;padding:2px 7px;border-radius:3px;font-size:10px;font-weight:600}
|
|
55
|
+
.bok{background:var(--ok-bg);color:var(--ok)}
|
|
56
|
+
.brl{background:var(--bad-bg);color:var(--bad)}
|
|
57
|
+
.berr{background:var(--mid-bg);color:var(--mid)}
|
|
58
|
+
.lr{display:flex;align-items:center;gap:8px;margin-bottom:6px}
|
|
59
|
+
.ll{width:52px;color:var(--mut);font-size:11px}
|
|
60
|
+
.lbw{flex:1;background:var(--sur2);border-radius:2px;height:6px;overflow:hidden;border:1px solid var(--bdr)}
|
|
61
|
+
.lb{height:100%;border-radius:2px;background:var(--ok)}
|
|
62
|
+
.lv{width:65px;text-align:right;font-size:11px;font-weight:600}
|
|
63
|
+
.lf{display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap}
|
|
64
|
+
.lf button{background:var(--sur);border:1px solid var(--bdr);color:var(--mut);padding:4px 12px;border-radius:4px;cursor:pointer;font-size:11px;font-family:inherit}
|
|
65
|
+
.lf button.on{background:var(--blue);color:#fff;border-color:var(--blue)}
|
|
66
|
+
.footer{text-align:center;padding:20px;color:var(--mut);font-size:10px;border-top:1px solid var(--bdr);margin-top:32px}
|
|
67
|
+
</style>
|
|
68
|
+
</head>
|
|
69
|
+
<body>
|
|
70
|
+
<div class="hdr">
|
|
71
|
+
<div class="lbl">Overload — Load Test Report</div>
|
|
72
|
+
<h1 id="hTitle">Report</h1>
|
|
73
|
+
<div class="meta" id="hMeta"></div>
|
|
74
|
+
</div>
|
|
75
|
+
<nav id="nav"></nav>
|
|
76
|
+
<div class="main" id="main"><p style="padding:40px;color:var(--mut)">Loading...</p></div>
|
|
77
|
+
<div class="footer" id="foot"></div>
|
|
78
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
79
|
+
<script>
|
|
80
|
+
window.RPT_DATA={{ data_json|safe }};
|
|
81
|
+
Chart.defaults.color='#6b7280';
|
|
82
|
+
Chart.defaults.borderColor='#e5e7eb';
|
|
83
|
+
Chart.defaults.font.size=10;
|
|
84
|
+
Chart.defaults.font.family='ui-monospace,monospace';
|
|
85
|
+
|
|
86
|
+
function esc(s){return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");}
|
|
87
|
+
function fmtT(sec){if(sec<60)return Math.round(sec*10)/10+'s';if(sec<3600)return Math.round(sec/6)/10+'m';return Math.round(sec/360)/10+'h';}
|
|
88
|
+
function fmtL(ms){if(ms<1000)return Math.round(ms)+'ms';return(Math.round(ms/100)/10)+'s';}
|
|
89
|
+
function badge(st){
|
|
90
|
+
if(st<=0)return'<span class="badge berr">timeout</span>';
|
|
91
|
+
if(st<200)return'<span class="badge" style="background:#e0e7ff;color:#4338ca">'+st+'</span>';
|
|
92
|
+
if(st<300)return'<span class="badge bok">'+st+'</span>';
|
|
93
|
+
if(st<400)return'<span class="badge" style="background:var(--ok-bg);color:var(--blue)">'+st+'</span>';
|
|
94
|
+
if(st===429)return'<span class="badge brl">429</span>';
|
|
95
|
+
if(st<500)return'<span class="badge berr">'+st+'</span>';
|
|
96
|
+
return'<span class="badge brl">'+st+'</span>';
|
|
97
|
+
}
|
|
98
|
+
function statusCol(k){
|
|
99
|
+
k=Number(k);if(k<=0)return"#9ca3af";if(k<200)return"#6366f1";if(k<300)return"rgba(14,138,95,.8)";
|
|
100
|
+
if(k<400)return"rgba(29,95,168,.8)";if(k===429)return"rgba(124,58,237,.8)";if(k<500)return"rgba(183,96,10,.8)";
|
|
101
|
+
return"rgba(192,57,43,.8)";
|
|
102
|
+
}
|
|
103
|
+
function mk(id,cfg){var el=document.getElementById(id);if(!el)return;new Chart(el,cfg);}
|
|
104
|
+
|
|
105
|
+
function renderKpis(d){
|
|
106
|
+
if(!d)return"";
|
|
107
|
+
var errPct=d.total?Math.round(d.errors*100/d.total):0;
|
|
108
|
+
return'<div class="kg">'
|
|
109
|
+
+'<div class="kpi t-mid"><div class="kl">Total</div><div class="kv v-mid">'+d.total+'</div><div class="ks">requests</div></div>'
|
|
110
|
+
+'<div class="kpi t-ok"><div class="kl">Successful</div><div class="kv v-ok">'+d.ok+'</div><div class="ks">2xx/3xx</div></div>'
|
|
111
|
+
+'<div class="kpi t-bad"><div class="kl">Rate Limited</div><div class="kv v-bad">'+d.rate_limited+'</div><div class="ks">429</div></div>'
|
|
112
|
+
+'<div class="kpi t-mid"><div class="kl">Errors</div><div class="kv v-mid">'+d.errors+'</div><div class="ks">'+errPct+'%</div></div>'
|
|
113
|
+
+'<div class="kpi t-ok"><div class="kl">Avg RPS</div><div class="kv v-ok">'+d.avg_rps+'</div><div class="ks">req/sec</div></div>'
|
|
114
|
+
+'<div class="kpi t-mid"><div class="kl">Duration</div><div class="kv v-mid">'+d.duration_seconds+'s</div><div class="ks">elapsed</div></div>'
|
|
115
|
+
+'</div>';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function renderLatBars(lat){
|
|
119
|
+
if(!lat)return"";
|
|
120
|
+
var mx=lat.max;
|
|
121
|
+
function bar(lbl,val){
|
|
122
|
+
var w=mx?Math.round(val*100/mx):0;
|
|
123
|
+
return'<div class="lr"><div class="ll">'+lbl+'</div><div class="lbw"><div class="lb" style="width:'+w+'%"></div></div><div class="lv">'+val+' ms</div></div>';
|
|
124
|
+
}
|
|
125
|
+
return bar("Min",lat.min)+bar("Median",lat.median)+bar("Mean",lat.mean)+bar("P95",lat.p95)+bar("P99",lat.p99)+bar("Max",lat.max);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function renderPerSecTable(rows){
|
|
129
|
+
if(!rows||!rows.length)return"";
|
|
130
|
+
var h='<div class="tw"><table><thead><tr><th>Second</th><th>Total</th><th>OK</th><th>429</th><th>Errors</th></tr></thead><tbody>';
|
|
131
|
+
rows.forEach(function(r){
|
|
132
|
+
h+='<tr><td>'+r.second+'s</td><td>'+r.total+'</td><td>'+r.ok+'</td><td>'+r.rate_limited+'</td><td>'+r.errors+'</td></tr>';
|
|
133
|
+
});
|
|
134
|
+
return h+'</tbody></table></div>';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function renderRampTable(rows){
|
|
138
|
+
if(!rows||!rows.length)return"";
|
|
139
|
+
var h='<div class="tw"><table><thead><tr><th>Rate</th><th>OK</th><th>429</th><th>Throttle %</th><th>Result</th></tr></thead><tbody>';
|
|
140
|
+
rows.forEach(function(r){
|
|
141
|
+
var b=r.rate_limited>0?'<span class="badge brl">throttled</span>':'<span class="badge bok">passed</span>';
|
|
142
|
+
var bar='<div style="width:'+r.pct+'%;height:4px;background:var(--bad);border-radius:2px;margin-top:3px"></div>';
|
|
143
|
+
h+='<tr><td>'+r.rps+'/s</td><td>'+r.ok+'</td><td>'+r.rate_limited+'</td><td>'+r.pct+'%'+bar+'</td><td>'+b+'</td></tr>';
|
|
144
|
+
});
|
|
145
|
+
return h+'</tbody></table></div>';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
var logFilter="all";
|
|
149
|
+
function renderLogTable(log){
|
|
150
|
+
if(!log||!log.length)return"";
|
|
151
|
+
var rows=log.filter(function(r){
|
|
152
|
+
if(logFilter==="ok")return r.status>=200&&r.status<400;
|
|
153
|
+
if(logFilter==="429")return r.status===429;
|
|
154
|
+
if(logFilter==="err")return r.status!==429&&(r.status<200||r.status>=400);
|
|
155
|
+
return true;
|
|
156
|
+
});
|
|
157
|
+
var show=rows.slice(0,500);
|
|
158
|
+
var h='<div class="lf">'
|
|
159
|
+
+'<button class="'+(logFilter==="all"?"on":"")+'" onclick="setFilter(this,\'all\')">All</button>'
|
|
160
|
+
+'<button class="'+(logFilter==="ok"?"on":"")+'" onclick="setFilter(this,\'ok\')">OK</button>'
|
|
161
|
+
+'<button class="'+(logFilter==="429"?"on":"")+'" onclick="setFilter(this,\'429\')">429</button>'
|
|
162
|
+
+'<button class="'+(logFilter==="err"?"on":"")+'" onclick="setFilter(this,\'err\')">Errors</button>'
|
|
163
|
+
+'</div>';
|
|
164
|
+
h+='<div class="tw"><table><thead><tr><th>#</th><th>Time</th><th>Status</th><th>Latency</th><th>Method</th><th>Request</th></tr></thead><tbody>';
|
|
165
|
+
show.forEach(function(r,i){
|
|
166
|
+
h+='<tr><td style="color:var(--mut)">'+(i+1)+'</td>'
|
|
167
|
+
+'<td>'+r.timestamp+'s</td>'
|
|
168
|
+
+'<td>'+badge(r.status)+'</td>'
|
|
169
|
+
+'<td>'+r.latency_ms+' ms</td>'
|
|
170
|
+
+'<td>'+esc(r.method)+'</td>'
|
|
171
|
+
+'<td>'+esc(r.request_name)+'</td></tr>';
|
|
172
|
+
});
|
|
173
|
+
if(rows.length>500)h+='<tr><td colspan="6" style="text-align:center;color:var(--mut);padding:10px">Showing 500 of '+rows.length+'</td></tr>';
|
|
174
|
+
return h+'</tbody></table></div>';
|
|
175
|
+
}
|
|
176
|
+
function setFilter(btn,f){
|
|
177
|
+
logFilter=f;
|
|
178
|
+
document.querySelectorAll(".lf button").forEach(function(b){b.classList.remove("on");});
|
|
179
|
+
btn.classList.add("on");
|
|
180
|
+
var d=window.RPT_DATA.stats;
|
|
181
|
+
var lc=document.getElementById("logContent");
|
|
182
|
+
if(lc&&d&&d.request_log)lc.innerHTML=renderLogTable(d.request_log);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function drawCharts(d){
|
|
186
|
+
if(!d)return;
|
|
187
|
+
if(d.per_second&&d.per_second.length){
|
|
188
|
+
var ps=d.per_second,mxS=ps[ps.length-1].second;
|
|
189
|
+
if(mxS>120){var bsz=mxS>600?10:5;var ag={};ps.forEach(function(r){var k=Math.floor(r.second/bsz)*bsz;if(!ag[k])ag[k]={second:k,ok:0,rate_limited:0,client_errors:0,server_errors:0,conn_errors:0};ag[k].ok+=r.ok;ag[k].rate_limited+=r.rate_limited;ag[k].client_errors+=(r.client_errors||0);ag[k].server_errors+=(r.server_errors||0);ag[k].conn_errors+=(r.conn_errors||0);});ps=Object.keys(ag).sort(function(a,b){return a-b;}).map(function(k){return ag[k];});}
|
|
190
|
+
mk("cRps",{type:"bar",
|
|
191
|
+
data:{labels:ps.map(function(r){return fmtT(r.second);}),
|
|
192
|
+
datasets:[
|
|
193
|
+
{label:"2xx/3xx",data:ps.map(function(r){return r.ok;}),backgroundColor:"rgba(14,138,95,.7)",borderRadius:2},
|
|
194
|
+
{label:"4xx",data:ps.map(function(r){return r.client_errors||0;}),backgroundColor:"rgba(183,96,10,.7)",borderRadius:2},
|
|
195
|
+
{label:"429",data:ps.map(function(r){return r.rate_limited;}),backgroundColor:"rgba(124,58,237,.7)",borderRadius:2},
|
|
196
|
+
{label:"5xx",data:ps.map(function(r){return r.server_errors||0;}),backgroundColor:"rgba(192,57,43,.7)",borderRadius:2},
|
|
197
|
+
{label:"Err",data:ps.map(function(r){return r.conn_errors||0;}),backgroundColor:"#9ca3af",borderRadius:2}
|
|
198
|
+
]},
|
|
199
|
+
options:{responsive:true,maintainAspectRatio:false,
|
|
200
|
+
plugins:{legend:{labels:{color:"#6b7280"}}},
|
|
201
|
+
scales:{x:{stacked:true,grid:{color:"#e5e7eb"},ticks:{maxTicksLimit:20}},y:{stacked:true,grid:{color:"#e5e7eb"}}}}});
|
|
202
|
+
}
|
|
203
|
+
if(d.status_codes&&Object.keys(d.status_codes).length){
|
|
204
|
+
var lbs=Object.keys(d.status_codes).sort(function(a,b){return Number(a)-Number(b);});
|
|
205
|
+
var vals=lbs.map(function(k){return d.status_codes[k];});
|
|
206
|
+
var cols=lbs.map(function(k){return statusCol(k);});
|
|
207
|
+
mk("cStat",{type:"doughnut",
|
|
208
|
+
data:{labels:lbs,datasets:[{data:vals,backgroundColor:cols,borderWidth:1,borderColor:"#fff",hoverOffset:4}]},
|
|
209
|
+
options:{responsive:true,maintainAspectRatio:false,cutout:"65%",
|
|
210
|
+
plugins:{legend:{position:"right",labels:{color:"#6b7280",padding:10}}}}});
|
|
211
|
+
}
|
|
212
|
+
if(d.timeline&&d.timeline.length){
|
|
213
|
+
var lats=d.timeline.map(function(r){return r.latency_ms;}).filter(function(v){return v>=0&&v<60000;});
|
|
214
|
+
if(lats.length){
|
|
215
|
+
var LBKTS=[10,25,50,100,200,300,500,750,1000,2000,5000];
|
|
216
|
+
var mxL=Math.max.apply(null,lats);
|
|
217
|
+
var bkts=LBKTS.filter(function(b){return b<=mxL*1.2;});
|
|
218
|
+
if(!bkts.length)bkts=[10,25,50];
|
|
219
|
+
var bLabels=[],bCounts=[],prev=0;
|
|
220
|
+
bkts.forEach(function(b){
|
|
221
|
+
var lbl=prev===0?'<'+b+'ms':prev+'-'+b+'ms';
|
|
222
|
+
var cnt=0;lats.forEach(function(v){if(v>=prev&&v<b)cnt++;});
|
|
223
|
+
bLabels.push(lbl);bCounts.push(cnt);prev=b;
|
|
224
|
+
});
|
|
225
|
+
var over=0;lats.forEach(function(v){if(v>=prev)over++;});
|
|
226
|
+
if(over>0){bLabels.push('>'+prev+'ms');bCounts.push(over);}
|
|
227
|
+
var bCols=bCounts.map(function(_,i){var r=i/Math.max(bLabels.length-1,1);return r<.5?"rgba(14,138,95,.7)":r<.75?"rgba(183,96,10,.7)":"rgba(192,57,43,.7)";});
|
|
228
|
+
mk("cLat",{type:"bar",
|
|
229
|
+
data:{labels:bLabels,datasets:[{label:"Requests",data:bCounts,backgroundColor:bCols,borderRadius:3}]},
|
|
230
|
+
options:{responsive:true,maintainAspectRatio:false,
|
|
231
|
+
plugins:{legend:{display:false},tooltip:{callbacks:{label:function(ctx){var pct=lats.length?(ctx.raw*100/lats.length).toFixed(1):0;return ctx.raw+' requests ('+pct+'%)';}}}},
|
|
232
|
+
scales:{x:{grid:{display:false},ticks:{maxRotation:45,font:{size:10}}},y:{grid:{color:"#e5e7eb"},title:{display:true,text:"Count",color:"#6b7280"}}}}});
|
|
233
|
+
}
|
|
234
|
+
var tl=d.timeline,tn=tl.length,tmx=Math.max(tl[tn-1].timestamp,0.1);
|
|
235
|
+
if(tn<=50){
|
|
236
|
+
mk("cTl",{type:"line",
|
|
237
|
+
data:{labels:tl.map(function(r){return fmtT(r.timestamp);}),
|
|
238
|
+
datasets:[{label:"Latency",data:tl.map(function(r){return Math.round(r.latency_ms*10)/10;}),
|
|
239
|
+
borderColor:"rgba(14,138,95,.9)",backgroundColor:"rgba(14,138,95,.1)",fill:true,tension:.3,
|
|
240
|
+
pointRadius:tn<20?3:1,borderWidth:2,
|
|
241
|
+
pointBackgroundColor:tl.map(function(r){if(r.status<=0)return"#9ca3af";if(r.status<300)return"rgba(14,138,95,.9)";if(r.status<400)return"rgba(29,95,168,.8)";if(r.status===429)return"rgba(124,58,237,.8)";if(r.status<500)return"rgba(183,96,10,.8)";return"rgba(192,57,43,.8)";})}]},
|
|
242
|
+
options:{responsive:true,maintainAspectRatio:false,
|
|
243
|
+
plugins:{legend:{display:false},tooltip:{callbacks:{label:function(ctx){var r=tl[ctx.dataIndex];return r.status+" — "+fmtL(r.latency_ms);}}}},
|
|
244
|
+
scales:{x:{title:{display:true,text:"Time",color:"#6b7280"},grid:{color:"#e5e7eb"}},
|
|
245
|
+
y:{title:{display:true,text:"Latency",color:"#6b7280"},grid:{color:"#e5e7eb"},beginAtZero:true,ticks:{callback:function(v){return fmtL(v);}}}}}});
|
|
246
|
+
} else {
|
|
247
|
+
var bc2=Math.min(80,Math.max(15,Math.ceil(tmx))),bs2=tmx/bc2||1,bk2=[];
|
|
248
|
+
for(var bi=0;bi<bc2;bi++)bk2.push({lats:[]});
|
|
249
|
+
tl.forEach(function(r){var i=Math.min(Math.floor(r.timestamp/bs2),bc2-1);bk2[i].lats.push(r.latency_ms);});
|
|
250
|
+
var tLabels=[],tAvg=[],tP50=[],tP95=[];
|
|
251
|
+
for(var j=0;j<bc2;j++){
|
|
252
|
+
tLabels.push(fmtT((j+.5)*bs2));
|
|
253
|
+
var sl=bk2[j].lats.slice().sort(function(a,b){return a-b;});
|
|
254
|
+
if(!sl.length){tAvg.push(null);tP50.push(null);tP95.push(null);continue;}
|
|
255
|
+
var sm=0;sl.forEach(function(v){sm+=v;});
|
|
256
|
+
tAvg.push(Math.round(sm/sl.length));
|
|
257
|
+
tP50.push(Math.round(sl[Math.floor(sl.length*.5)]));
|
|
258
|
+
tP95.push(Math.round(sl[Math.min(Math.floor(sl.length*.95),sl.length-1)]));
|
|
259
|
+
}
|
|
260
|
+
mk("cTl",{type:"line",
|
|
261
|
+
data:{labels:tLabels,datasets:[
|
|
262
|
+
{label:"Avg",data:tAvg,borderColor:"rgba(14,138,95,.9)",backgroundColor:"rgba(14,138,95,.1)",fill:true,tension:.3,pointRadius:0,borderWidth:2},
|
|
263
|
+
{label:"P50",data:tP50,borderColor:"rgba(29,95,168,.8)",tension:.3,pointRadius:0,borderWidth:1.5,borderDash:[4,2]},
|
|
264
|
+
{label:"P95",data:tP95,borderColor:"rgba(192,57,43,.8)",tension:.3,pointRadius:0,borderWidth:1.5,borderDash:[4,2]}
|
|
265
|
+
]},
|
|
266
|
+
options:{responsive:true,maintainAspectRatio:false,spanGaps:true,
|
|
267
|
+
plugins:{legend:{labels:{color:"#6b7280"}}},
|
|
268
|
+
scales:{x:{title:{display:true,text:"Time",color:"#6b7280"},grid:{color:"#e5e7eb"},ticks:{maxTicksLimit:12}},
|
|
269
|
+
y:{title:{display:true,text:"Latency",color:"#6b7280"},grid:{color:"#e5e7eb"},beginAtZero:true,ticks:{callback:function(v){return fmtL(v);}}}}}});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function drawRampChart(rows){
|
|
275
|
+
if(!rows||!rows.length)return;
|
|
276
|
+
mk("cRamp",{type:"line",
|
|
277
|
+
data:{labels:rows.map(function(r){return r.rps+"/s";}),
|
|
278
|
+
datasets:[
|
|
279
|
+
{label:"OK",data:rows.map(function(r){return r.ok;}),borderColor:"rgba(14,138,95,.9)",backgroundColor:"rgba(14,138,95,.1)",tension:.3,fill:true,pointRadius:2},
|
|
280
|
+
{label:"429",data:rows.map(function(r){return r.rate_limited;}),borderColor:"rgba(192,57,43,.9)",backgroundColor:"rgba(192,57,43,.1)",tension:.3,fill:true,pointRadius:2}
|
|
281
|
+
]},
|
|
282
|
+
options:{responsive:true,maintainAspectRatio:false,
|
|
283
|
+
plugins:{legend:{labels:{color:"#6b7280"}}},
|
|
284
|
+
scales:{x:{grid:{color:"#e5e7eb"}},y:{grid:{color:"#e5e7eb"}}}}});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function buildPage(){
|
|
288
|
+
var D=window.RPT_DATA;
|
|
289
|
+
var m=D.meta,d=D.stats,rr=D.ramp_rows;
|
|
290
|
+
|
|
291
|
+
document.getElementById("hTitle").textContent="Overload Report — "+m.test_type.toUpperCase()+" — "+m.run_id;
|
|
292
|
+
var metaHtml='<div class="mi">Test <b>'+esc(m.test_type)+'</b></div>'
|
|
293
|
+
+'<div class="mi">Run <b>'+esc(m.run_id)+'</b></div>';
|
|
294
|
+
if(m.config){
|
|
295
|
+
Object.keys(m.config).forEach(function(k){
|
|
296
|
+
metaHtml+='<div class="mi">'+esc(k)+' <b>'+esc(String(m.config[k]))+'</b></div>';
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
document.getElementById("hMeta").innerHTML=metaHtml;
|
|
300
|
+
|
|
301
|
+
var nav='<a href="#sSummary">Summary</a><a href="#sLat">Latency</a><a href="#sTl">Timeline</a><a href="#sLog">Log</a>';
|
|
302
|
+
if(rr&&rr.length)nav+='<a href="#sRamp">Ramp</a>';
|
|
303
|
+
document.getElementById("nav").innerHTML=nav;
|
|
304
|
+
|
|
305
|
+
var html="";
|
|
306
|
+
|
|
307
|
+
// Verdict
|
|
308
|
+
var v=D.verdict;
|
|
309
|
+
if(v){
|
|
310
|
+
html+='<div class="verdict '+(v.passed?"pass":"fail")+'"><div class="vi">'+(v.passed?"✅":"❌")+'</div><div><div class="vt">'+(v.passed?"PASS":"FAIL")+'</div>';
|
|
311
|
+
html+='<div class="vs" style="margin-top:6px">';
|
|
312
|
+
v.results.forEach(function(r){
|
|
313
|
+
var mark=r.passed?'<span style="color:var(--ok)">✓</span>':'<span style="color:var(--bad)">✗</span>';
|
|
314
|
+
html+=mark+' '+esc(r.metric)+': '+r.actual+' '+esc(r.operator)+' '+r.expected+'<br>';
|
|
315
|
+
});
|
|
316
|
+
html+='</div></div></div>';
|
|
317
|
+
} else {
|
|
318
|
+
var errPct=d.total?Math.round(d.errors*100/d.total):0;
|
|
319
|
+
var isOk=errPct<10;
|
|
320
|
+
html+='<div class="verdict '+(isOk?"pass":"fail")+'"><div class="vi">'+(isOk?"✅":"⚠")+'</div><div><div class="vt">'+(isOk?"Test Passed":"Issues Detected")+'</div><div class="vs">'+d.total+' requests, '+errPct+'% errors, avg '+d.latency.mean+'ms latency</div></div></div>';
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Summary
|
|
324
|
+
html+='<div class="sec" id="sSummary"><div class="sh">Summary</div>';
|
|
325
|
+
html+=renderKpis(d);
|
|
326
|
+
html+='<div class="two"><div class="cw"><div class="ct">Requests per second</div><canvas id="cRps"></canvas></div>';
|
|
327
|
+
html+='<div class="cw"><div class="ct">Status distribution</div><canvas id="cStat"></canvas></div></div>';
|
|
328
|
+
html+=renderPerSecTable(d.per_second);
|
|
329
|
+
html+='</div>';
|
|
330
|
+
|
|
331
|
+
// Ramp
|
|
332
|
+
if(rr&&rr.length){
|
|
333
|
+
html+='<div class="sec" id="sRamp"><div class="sh">Rate Limit Ramp</div>';
|
|
334
|
+
html+='<div class="cw" style="margin-bottom:10px"><div class="ct">Throttle rate by RPS</div><canvas id="cRamp"></canvas></div>';
|
|
335
|
+
html+=renderRampTable(rr);
|
|
336
|
+
html+='</div>';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Latency
|
|
340
|
+
if(d.latency){
|
|
341
|
+
html+='<div class="sec" id="sLat"><div class="sh">Latency</div>';
|
|
342
|
+
html+='<div class="two"><div class="cw"><div class="ct">Percentiles</div>'+renderLatBars(d.latency)+'</div>';
|
|
343
|
+
html+='<div class="cw"><div class="ct">Histogram</div><canvas id="cLat"></canvas></div></div></div>';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Timeline
|
|
347
|
+
if(d.timeline&&d.timeline.length){
|
|
348
|
+
html+='<div class="sec" id="sTl"><div class="sh">Timeline</div>';
|
|
349
|
+
html+='<div class="cw"><div class="ct">Latency per request</div><canvas id="cTl"></canvas></div></div>';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Log
|
|
353
|
+
if(d.request_log&&d.request_log.length){
|
|
354
|
+
html+='<div class="sec" id="sLog"><div class="sh">Request Log</div>';
|
|
355
|
+
html+='<div id="logContent">'+renderLogTable(d.request_log)+'</div></div>';
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
document.getElementById("main").innerHTML=html;
|
|
359
|
+
document.getElementById("foot").textContent="Overload — "+m.test_type+" — "+m.run_id;
|
|
360
|
+
requestAnimationFrame(function(){
|
|
361
|
+
drawCharts(d);
|
|
362
|
+
drawRampChart(rr);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',buildPage);}else{buildPage();}
|
|
367
|
+
</script>
|
|
368
|
+
</body>
|
|
369
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|
overload/utils/naming.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import random
|
|
5
|
+
import string
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def generate_run_id() -> str:
|
|
10
|
+
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
11
|
+
suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
|
12
|
+
return f"{ts}_{suffix}"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def generate_correlation_id(length: int = 20) -> str:
|
|
16
|
+
return "".join(random.choices(string.ascii_letters + string.digits, k=length))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def stamped_filename(base_name: str, run_id: str, extension: str = ".html") -> str:
|
|
20
|
+
return f"{base_name}_{run_id}{extension}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def make_output_dir(base_dir: str, run_id: str) -> str:
|
|
24
|
+
path = os.path.join(base_dir, "overload_runs", run_id)
|
|
25
|
+
os.makedirs(path, exist_ok=True)
|
|
26
|
+
return path
|
overload/web/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|
overload/web/app.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
from fastapi.staticfiles import StaticFiles
|
|
9
|
+
|
|
10
|
+
from overload.web.routes.api import router as api_router, _state
|
|
11
|
+
from overload.web.routes.ws import router as ws_router
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
STATIC_DIR = Path(__file__).parent / "static"
|
|
16
|
+
TEMPLATES_DIR = Path(__file__).parent / "templates"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_app(working_dir: str | None = None) -> FastAPI:
|
|
20
|
+
app = FastAPI(
|
|
21
|
+
title="Overload",
|
|
22
|
+
description="Load testing tool for Postman collections",
|
|
23
|
+
version="0.1.0",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
_state["working_dir"] = working_dir or os.getcwd()
|
|
27
|
+
|
|
28
|
+
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
|
29
|
+
|
|
30
|
+
app.include_router(api_router, prefix="/api")
|
|
31
|
+
app.include_router(ws_router)
|
|
32
|
+
|
|
33
|
+
@app.get("/")
|
|
34
|
+
async def index():
|
|
35
|
+
from fastapi.responses import FileResponse
|
|
36
|
+
return FileResponse(str(TEMPLATES_DIR / "index.html"))
|
|
37
|
+
|
|
38
|
+
return app
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|