express-api-stress-tester 2.0.2 → 2.0.3
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.
- package/package.json +2 -2
- package/src/cli.js +34 -0
- package/src/core/worker.js +6 -2
- package/src/metrics/apiMetrics.js +10 -0
- package/src/metrics/metricsCollector.js +12 -0
- package/src/reporting/htmlReport.js +1 -1
- package/src/reporting/reportWriter.js +20 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "express-api-stress-tester",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
4
4
|
"description": "High-performance distributed API stress testing platform for Express.js APIs — simulate up to 10M concurrent virtual users",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"express-api-stress-tester": "src/cli.js"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"test": "node --experimental-vm-modules node_modules
|
|
14
|
+
"test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --forceExit",
|
|
15
15
|
"stress": "node src/cli.js"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
package/src/cli.js
CHANGED
|
@@ -48,6 +48,40 @@ function printSummary(summary) {
|
|
|
48
48
|
console.log(` Result: ${resultColor(summary.result)}`);
|
|
49
49
|
console.log(chalk.bold.cyan('═══════════════════════════════════════════'));
|
|
50
50
|
console.log('');
|
|
51
|
+
|
|
52
|
+
if (summary.statusCodes && typeof summary.statusCodes === 'object') {
|
|
53
|
+
const codes = Object.entries(summary.statusCodes)
|
|
54
|
+
.filter(([, c]) => Number(c) > 0)
|
|
55
|
+
.sort((a, b) => Number(b[1]) - Number(a[1]))
|
|
56
|
+
.map(([code, count]) => `${code}:${count}`)
|
|
57
|
+
.join(' ');
|
|
58
|
+
if (codes) {
|
|
59
|
+
console.log(chalk.bold('Status Codes:'));
|
|
60
|
+
console.log(` ${codes}`);
|
|
61
|
+
console.log('');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (summary.perEndpoint && typeof summary.perEndpoint === 'object' && Object.keys(summary.perEndpoint).length > 0) {
|
|
66
|
+
console.log(chalk.bold('Per-Endpoint Metrics:'));
|
|
67
|
+
const entries = Object.entries(summary.perEndpoint).sort(
|
|
68
|
+
(a, b) => (b[1].requestsPerSec || 0) - (a[1].requestsPerSec || 0),
|
|
69
|
+
);
|
|
70
|
+
for (const [endpoint, m] of entries) {
|
|
71
|
+
const codes = m.statusCodes && typeof m.statusCodes === 'object'
|
|
72
|
+
? Object.entries(m.statusCodes)
|
|
73
|
+
.filter(([, c]) => Number(c) > 0)
|
|
74
|
+
.sort((x, y) => Number(y[1]) - Number(x[1]))
|
|
75
|
+
.slice(0, 6)
|
|
76
|
+
.map(([code, count]) => `${code}:${count}`)
|
|
77
|
+
.join(' ')
|
|
78
|
+
: '';
|
|
79
|
+
console.log(
|
|
80
|
+
` ${endpoint} | rps=${m.requestsPerSec ?? 0} avg=${m.avgResponseTime ?? 0}ms p95=${m.p95 ?? 0}ms err=${m.errorRate ?? 0}%${codes ? ` codes=${codes}` : ''}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
console.log('');
|
|
84
|
+
}
|
|
51
85
|
}
|
|
52
86
|
|
|
53
87
|
async function runCommand(configPath, opts) {
|
package/src/core/worker.js
CHANGED
|
@@ -159,12 +159,13 @@ function getEndpointMetrics(endpoint) {
|
|
|
159
159
|
maxLatency: -Infinity,
|
|
160
160
|
responseTimes: [],
|
|
161
161
|
sampleCount: 0,
|
|
162
|
+
statusCodes: {},
|
|
162
163
|
};
|
|
163
164
|
}
|
|
164
165
|
return perEndpoint[endpoint];
|
|
165
166
|
}
|
|
166
167
|
|
|
167
|
-
function recordEndpoint(endpoint, elapsedMs, isError) {
|
|
168
|
+
function recordEndpoint(endpoint, elapsedMs, isError, status) {
|
|
168
169
|
const metrics = getEndpointMetrics(endpoint);
|
|
169
170
|
metrics.totalRequests++;
|
|
170
171
|
metrics.totalResponseTime += elapsedMs;
|
|
@@ -175,6 +176,9 @@ function recordEndpoint(endpoint, elapsedMs, isError) {
|
|
|
175
176
|
} else {
|
|
176
177
|
metrics.successCount++;
|
|
177
178
|
}
|
|
179
|
+
if (status) {
|
|
180
|
+
metrics.statusCodes[status] = (metrics.statusCodes[status] || 0) + 1;
|
|
181
|
+
}
|
|
178
182
|
metrics.sampleCount++;
|
|
179
183
|
if (metrics.responseTimes.length < MAX_SAMPLE_SIZE) {
|
|
180
184
|
metrics.responseTimes.push(elapsedMs);
|
|
@@ -203,7 +207,7 @@ function recordRequestMetrics({ endpointKey, elapsedMs, isError, status }) {
|
|
|
203
207
|
successCount++;
|
|
204
208
|
}
|
|
205
209
|
|
|
206
|
-
recordEndpoint(endpointKey, elapsedMs, isError);
|
|
210
|
+
recordEndpoint(endpointKey, elapsedMs, isError, status);
|
|
207
211
|
}
|
|
208
212
|
|
|
209
213
|
async function applyHeaderPlugins(headers) {
|
|
@@ -13,6 +13,7 @@ export class ApiMetrics {
|
|
|
13
13
|
this.maxLatency = -Infinity;
|
|
14
14
|
this.responseTimes = [];
|
|
15
15
|
this._sampleCount = 0;
|
|
16
|
+
this.statusCodes = {};
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
record(responseTimeMs, isError) {
|
|
@@ -55,6 +56,14 @@ export class ApiMetrics {
|
|
|
55
56
|
if (partial.responseTimes) {
|
|
56
57
|
this.mergeResponseTimes(partial.responseTimes);
|
|
57
58
|
}
|
|
59
|
+
|
|
60
|
+
if (partial.statusCodes && typeof partial.statusCodes === 'object') {
|
|
61
|
+
for (const [code, count] of Object.entries(partial.statusCodes)) {
|
|
62
|
+
const n = Number(count) || 0;
|
|
63
|
+
if (!n) continue;
|
|
64
|
+
this.statusCodes[code] = (this.statusCodes[code] || 0) + n;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
mergeResponseTimes(times) {
|
|
@@ -111,6 +120,7 @@ export class ApiMetrics {
|
|
|
111
120
|
maxLatency: maxLat,
|
|
112
121
|
errorRate: parseFloat(errorRate),
|
|
113
122
|
successRate: parseFloat(successRate),
|
|
123
|
+
statusCodes: { ...this.statusCodes },
|
|
114
124
|
};
|
|
115
125
|
}
|
|
116
126
|
}
|
|
@@ -21,6 +21,9 @@ export class MetricsCollector {
|
|
|
21
21
|
this.responseTimes = [];
|
|
22
22
|
this._sampleCount = 0;
|
|
23
23
|
this.perEndpoint = new Map();
|
|
24
|
+
|
|
25
|
+
// Best-effort aggregate status codes across all requests
|
|
26
|
+
this.statusCodes = {};
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
start() {
|
|
@@ -73,6 +76,14 @@ export class MetricsCollector {
|
|
|
73
76
|
this.mergeResponseTimes(partial.responseTimes);
|
|
74
77
|
}
|
|
75
78
|
|
|
79
|
+
if (partial.statusCodes && typeof partial.statusCodes === 'object') {
|
|
80
|
+
for (const [code, count] of Object.entries(partial.statusCodes)) {
|
|
81
|
+
const n = Number(count) || 0;
|
|
82
|
+
if (!n) continue;
|
|
83
|
+
this.statusCodes[code] = (this.statusCodes[code] || 0) + n;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
76
87
|
if (partial.perEndpoint) {
|
|
77
88
|
for (const [endpoint, metrics] of Object.entries(partial.perEndpoint)) {
|
|
78
89
|
this._getEndpointMetrics(endpoint).merge(metrics);
|
|
@@ -186,6 +197,7 @@ export class MetricsCollector {
|
|
|
186
197
|
result: passed ? 'PASSED' : 'FAILED',
|
|
187
198
|
elapsedSeconds: elapsed.toFixed(1),
|
|
188
199
|
perEndpoint: this._buildEndpointSummaries(elapsed),
|
|
200
|
+
statusCodes: { ...this.statusCodes },
|
|
189
201
|
};
|
|
190
202
|
}
|
|
191
203
|
|
|
@@ -32,7 +32,7 @@ export function generateHtmlReport(config, summary) {
|
|
|
32
32
|
];
|
|
33
33
|
|
|
34
34
|
const configRows = [
|
|
35
|
-
['API URL', config.url || 'N/A'],
|
|
35
|
+
['API URL', config.url || config.baseUrl || 'N/A'],
|
|
36
36
|
['Method', (config.method || 'GET').toUpperCase()],
|
|
37
37
|
['Concurrent Users', config.concurrency || 1],
|
|
38
38
|
...(config.duration ? [['Duration', `${config.duration}s`]] : []),
|
|
@@ -26,12 +26,29 @@ export function writeReport(config, summary, reportPath) {
|
|
|
26
26
|
|
|
27
27
|
function buildTxtReport(config, summary) {
|
|
28
28
|
const divider = '='.repeat(50);
|
|
29
|
+
const apiUrl = config.url || config.baseUrl || 'N/A';
|
|
30
|
+
const method = (() => {
|
|
31
|
+
if (config.method) return String(config.method).toUpperCase();
|
|
32
|
+
if (Array.isArray(config.routes) && config.routes.length === 1 && config.routes[0]?.method) {
|
|
33
|
+
return String(config.routes[0].method).toUpperCase();
|
|
34
|
+
}
|
|
35
|
+
if (Array.isArray(config.routes) && config.routes.length > 1) return 'MULTI';
|
|
36
|
+
return 'GET';
|
|
37
|
+
})();
|
|
38
|
+
const statusCodesLine = summary.statusCodes && typeof summary.statusCodes === 'object'
|
|
39
|
+
? Object.entries(summary.statusCodes)
|
|
40
|
+
.filter(([, c]) => Number(c) > 0)
|
|
41
|
+
.sort((a, b) => Number(b[1]) - Number(a[1]))
|
|
42
|
+
.slice(0, 8)
|
|
43
|
+
.map(([code, c]) => `${code}:${c}`)
|
|
44
|
+
.join(' ')
|
|
45
|
+
: '';
|
|
29
46
|
const lines = [
|
|
30
47
|
divider,
|
|
31
48
|
` API Stress Test Report`,
|
|
32
49
|
divider,
|
|
33
|
-
`API URL: ${
|
|
34
|
-
`Method: ${
|
|
50
|
+
`API URL: ${apiUrl}`,
|
|
51
|
+
`Method: ${method}`,
|
|
35
52
|
`Concurrent Users: ${config.concurrency || 1}`,
|
|
36
53
|
`Duration (s): ${summary.elapsedSeconds}`,
|
|
37
54
|
`Total Requests: ${summary.totalRequests}`,
|
|
@@ -45,6 +62,7 @@ function buildTxtReport(config, summary) {
|
|
|
45
62
|
`Success Rate: ${summary.successRate}%`,
|
|
46
63
|
`CPU Usage: ${summary.cpuPercent}%`,
|
|
47
64
|
`Memory Usage: ${summary.memoryMB}MB`,
|
|
65
|
+
...(statusCodesLine ? [`Status Codes: ${statusCodesLine}`] : []),
|
|
48
66
|
`Result: ${summary.result}`,
|
|
49
67
|
divider,
|
|
50
68
|
...(buildEndpointLines(summary.perEndpoint) || []),
|