@testsmith/perfornium 0.1.0
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/README.md +360 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.js +192 -0
- package/dist/cli/commands/distributed.d.ts +11 -0
- package/dist/cli/commands/distributed.js +179 -0
- package/dist/cli/commands/import.d.ts +23 -0
- package/dist/cli/commands/import.js +461 -0
- package/dist/cli/commands/init.d.ts +7 -0
- package/dist/cli/commands/init.js +923 -0
- package/dist/cli/commands/mock.d.ts +7 -0
- package/dist/cli/commands/mock.js +281 -0
- package/dist/cli/commands/report.d.ts +5 -0
- package/dist/cli/commands/report.js +70 -0
- package/dist/cli/commands/run.d.ts +12 -0
- package/dist/cli/commands/run.js +260 -0
- package/dist/cli/commands/validate.d.ts +3 -0
- package/dist/cli/commands/validate.js +35 -0
- package/dist/cli/commands/worker.d.ts +27 -0
- package/dist/cli/commands/worker.js +320 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.js +20 -0
- package/dist/config/parser.d.ts +19 -0
- package/dist/config/parser.js +330 -0
- package/dist/config/types/global-config.d.ts +74 -0
- package/dist/config/types/global-config.js +2 -0
- package/dist/config/types/hooks.d.ts +58 -0
- package/dist/config/types/hooks.js +3 -0
- package/dist/config/types/import-types.d.ts +33 -0
- package/dist/config/types/import-types.js +2 -0
- package/dist/config/types/index.d.ts +11 -0
- package/dist/config/types/index.js +27 -0
- package/dist/config/types/load-config.d.ts +32 -0
- package/dist/config/types/load-config.js +9 -0
- package/dist/config/types/output-config.d.ts +10 -0
- package/dist/config/types/output-config.js +2 -0
- package/dist/config/types/report-config.d.ts +10 -0
- package/dist/config/types/report-config.js +2 -0
- package/dist/config/types/runtime-types.d.ts +6 -0
- package/dist/config/types/runtime-types.js +2 -0
- package/dist/config/types/scenario-config.d.ts +30 -0
- package/dist/config/types/scenario-config.js +2 -0
- package/dist/config/types/step-types.d.ts +139 -0
- package/dist/config/types/step-types.js +2 -0
- package/dist/config/types/test-configuration.d.ts +18 -0
- package/dist/config/types/test-configuration.js +2 -0
- package/dist/config/types/worker-config.d.ts +12 -0
- package/dist/config/types/worker-config.js +2 -0
- package/dist/config/validator.d.ts +19 -0
- package/dist/config/validator.js +198 -0
- package/dist/core/csv-data-provider.d.ts +47 -0
- package/dist/core/csv-data-provider.js +265 -0
- package/dist/core/hooks-manager.d.ts +33 -0
- package/dist/core/hooks-manager.js +129 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +11 -0
- package/dist/core/script-executor.d.ts +14 -0
- package/dist/core/script-executor.js +290 -0
- package/dist/core/step-executor.d.ts +41 -0
- package/dist/core/step-executor.js +680 -0
- package/dist/core/test-runner.d.ts +34 -0
- package/dist/core/test-runner.js +465 -0
- package/dist/core/threshold-evaluator.d.ts +43 -0
- package/dist/core/threshold-evaluator.js +170 -0
- package/dist/core/virtual-user-pool.d.ts +42 -0
- package/dist/core/virtual-user-pool.js +136 -0
- package/dist/core/virtual-user.d.ts +51 -0
- package/dist/core/virtual-user.js +488 -0
- package/dist/distributed/coordinator.d.ts +34 -0
- package/dist/distributed/coordinator.js +158 -0
- package/dist/distributed/health-monitor.d.ts +18 -0
- package/dist/distributed/health-monitor.js +72 -0
- package/dist/distributed/load-distributor.d.ts +17 -0
- package/dist/distributed/load-distributor.js +106 -0
- package/dist/distributed/remote-worker.d.ts +37 -0
- package/dist/distributed/remote-worker.js +241 -0
- package/dist/distributed/result-aggregator.d.ts +43 -0
- package/dist/distributed/result-aggregator.js +146 -0
- package/dist/dsl/index.d.ts +3 -0
- package/dist/dsl/index.js +11 -0
- package/dist/dsl/test-builder.d.ts +111 -0
- package/dist/dsl/test-builder.js +514 -0
- package/dist/importers/har-importer.d.ts +17 -0
- package/dist/importers/har-importer.js +172 -0
- package/dist/importers/open-api-importer.d.ts +23 -0
- package/dist/importers/open-api-importer.js +181 -0
- package/dist/importers/wsdl-importer.d.ts +42 -0
- package/dist/importers/wsdl-importer.js +440 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +17 -0
- package/dist/load-patterns/arrivals.d.ts +7 -0
- package/dist/load-patterns/arrivals.js +118 -0
- package/dist/load-patterns/base.d.ts +9 -0
- package/dist/load-patterns/base.js +2 -0
- package/dist/load-patterns/basic.d.ts +7 -0
- package/dist/load-patterns/basic.js +117 -0
- package/dist/load-patterns/stepping.d.ts +6 -0
- package/dist/load-patterns/stepping.js +122 -0
- package/dist/metrics/collector.d.ts +72 -0
- package/dist/metrics/collector.js +662 -0
- package/dist/metrics/types.d.ts +135 -0
- package/dist/metrics/types.js +2 -0
- package/dist/outputs/base.d.ts +7 -0
- package/dist/outputs/base.js +2 -0
- package/dist/outputs/csv.d.ts +13 -0
- package/dist/outputs/csv.js +163 -0
- package/dist/outputs/graphite.d.ts +13 -0
- package/dist/outputs/graphite.js +126 -0
- package/dist/outputs/influxdb.d.ts +12 -0
- package/dist/outputs/influxdb.js +82 -0
- package/dist/outputs/json.d.ts +14 -0
- package/dist/outputs/json.js +107 -0
- package/dist/outputs/streaming-csv.d.ts +37 -0
- package/dist/outputs/streaming-csv.js +254 -0
- package/dist/outputs/streaming-json.d.ts +43 -0
- package/dist/outputs/streaming-json.js +353 -0
- package/dist/outputs/webhook.d.ts +16 -0
- package/dist/outputs/webhook.js +96 -0
- package/dist/protocols/base.d.ts +33 -0
- package/dist/protocols/base.js +2 -0
- package/dist/protocols/rest/handler.d.ts +67 -0
- package/dist/protocols/rest/handler.js +776 -0
- package/dist/protocols/soap/handler.d.ts +12 -0
- package/dist/protocols/soap/handler.js +165 -0
- package/dist/protocols/web/core-web-vitals.d.ts +121 -0
- package/dist/protocols/web/core-web-vitals.js +373 -0
- package/dist/protocols/web/handler.d.ts +50 -0
- package/dist/protocols/web/handler.js +706 -0
- package/dist/recorder/native-recorder.d.ts +14 -0
- package/dist/recorder/native-recorder.js +533 -0
- package/dist/recorder/scenario-recorder.d.ts +55 -0
- package/dist/recorder/scenario-recorder.js +296 -0
- package/dist/reporting/constants.d.ts +94 -0
- package/dist/reporting/constants.js +82 -0
- package/dist/reporting/enhanced-html-generator.d.ts +55 -0
- package/dist/reporting/enhanced-html-generator.js +965 -0
- package/dist/reporting/generator.d.ts +42 -0
- package/dist/reporting/generator.js +1217 -0
- package/dist/reporting/statistics.d.ts +144 -0
- package/dist/reporting/statistics.js +742 -0
- package/dist/reporting/templates/enhanced-report.hbs +2812 -0
- package/dist/reporting/templates/html.hbs +2453 -0
- package/dist/utils/faker-manager.d.ts +55 -0
- package/dist/utils/faker-manager.js +166 -0
- package/dist/utils/file-manager.d.ts +33 -0
- package/dist/utils/file-manager.js +154 -0
- package/dist/utils/handlebars-manager.d.ts +42 -0
- package/dist/utils/handlebars-manager.js +172 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.js +46 -0
- package/dist/utils/template.d.ts +80 -0
- package/dist/utils/template.js +513 -0
- package/dist/utils/test-output-writer.d.ts +56 -0
- package/dist/utils/test-output-writer.js +643 -0
- package/dist/utils/time.d.ts +3 -0
- package/dist/utils/time.js +23 -0
- package/dist/utils/timestamp-helper.d.ts +17 -0
- package/dist/utils/timestamp-helper.js +53 -0
- package/dist/workers/manager.d.ts +18 -0
- package/dist/workers/manager.js +95 -0
- package/dist/workers/server.d.ts +21 -0
- package/dist/workers/server.js +205 -0
- package/dist/workers/worker.d.ts +19 -0
- package/dist/workers/worker.js +147 -0
- package/package.json +102 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MetricsCollector = void 0;
|
|
4
|
+
const events_1 = require("events");
|
|
5
|
+
const logger_1 = require("../utils/logger");
|
|
6
|
+
class MetricsCollector extends events_1.EventEmitter {
|
|
7
|
+
constructor(realtimeConfig) {
|
|
8
|
+
super();
|
|
9
|
+
this.results = [];
|
|
10
|
+
this.startTime = 0;
|
|
11
|
+
this.errorDetails = new Map();
|
|
12
|
+
this.vuStartEvents = [];
|
|
13
|
+
this.loadPatternType = 'basic';
|
|
14
|
+
this.batchBuffer = [];
|
|
15
|
+
this.batchTimer = null;
|
|
16
|
+
this.batchCounter = 0;
|
|
17
|
+
this.csvHeaderWritten = false;
|
|
18
|
+
// Default output paths
|
|
19
|
+
this.defaultJsonPath = 'results/live-results.json';
|
|
20
|
+
this.defaultCsvPath = 'results/live-results.csv';
|
|
21
|
+
// Enable incremental files by default with sensible defaults
|
|
22
|
+
this.realtimeConfig = {
|
|
23
|
+
enabled: true,
|
|
24
|
+
batch_size: 10, // Default batch size
|
|
25
|
+
incremental_files: {
|
|
26
|
+
enabled: true,
|
|
27
|
+
json_path: this.defaultJsonPath,
|
|
28
|
+
csv_path: this.defaultCsvPath,
|
|
29
|
+
update_summary: true
|
|
30
|
+
},
|
|
31
|
+
...realtimeConfig // Override with provided config if any
|
|
32
|
+
};
|
|
33
|
+
if (this.realtimeConfig.enabled) {
|
|
34
|
+
this.initializeRealtime();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
initializeRealtime() {
|
|
38
|
+
// Use interval-based batching if specified, otherwise use count-based
|
|
39
|
+
if (this.realtimeConfig.interval_ms) {
|
|
40
|
+
this.startBatchTimer();
|
|
41
|
+
logger_1.logger.info(`📊 Real-time metrics enabled with ${this.realtimeConfig.interval_ms}ms intervals`);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const batchSize = this.realtimeConfig.batch_size || 10;
|
|
45
|
+
logger_1.logger.info(`📊 Real-time metrics enabled with batch size: ${batchSize}`);
|
|
46
|
+
}
|
|
47
|
+
if (this.realtimeConfig.file_output?.enabled) {
|
|
48
|
+
logger_1.logger.info(`📁 Real-time file output enabled: ${this.realtimeConfig.file_output.path}`);
|
|
49
|
+
}
|
|
50
|
+
if (this.realtimeConfig.incremental_files?.enabled) {
|
|
51
|
+
logger_1.logger.info(`📄 Incremental JSON/CSV files enabled (JSON: ${this.realtimeConfig.incremental_files.json_path}, CSV: ${this.realtimeConfig.incremental_files.csv_path})`);
|
|
52
|
+
this.initializeIncrementalFiles();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
startBatchTimer() {
|
|
56
|
+
const interval = this.realtimeConfig.interval_ms || 5000;
|
|
57
|
+
this.batchTimer = setInterval(() => {
|
|
58
|
+
if (this.batchBuffer.length > 0) {
|
|
59
|
+
this.flushBatch();
|
|
60
|
+
}
|
|
61
|
+
}, interval);
|
|
62
|
+
}
|
|
63
|
+
async initializeIncrementalFiles() {
|
|
64
|
+
const config = this.realtimeConfig.incremental_files;
|
|
65
|
+
try {
|
|
66
|
+
const fs = require('fs').promises;
|
|
67
|
+
const path = require('path');
|
|
68
|
+
// Initialize JSON file
|
|
69
|
+
if (config.json_path) {
|
|
70
|
+
const dir = path.dirname(config.json_path);
|
|
71
|
+
await fs.mkdir(dir, { recursive: true });
|
|
72
|
+
// Start with empty array
|
|
73
|
+
await fs.writeFile(config.json_path, '[]');
|
|
74
|
+
logger_1.logger.debug(`📄 Initialized incremental JSON file: ${config.json_path}`);
|
|
75
|
+
}
|
|
76
|
+
// Initialize CSV file with header
|
|
77
|
+
if (config.csv_path) {
|
|
78
|
+
const dir = path.dirname(config.csv_path);
|
|
79
|
+
await fs.mkdir(dir, { recursive: true });
|
|
80
|
+
const csvHeader = 'timestamp,vu_id,scenario,action,step_name,duration,success,status,error,request_url\n';
|
|
81
|
+
await fs.writeFile(config.csv_path, csvHeader);
|
|
82
|
+
this.csvHeaderWritten = true;
|
|
83
|
+
logger_1.logger.debug(`📄 Initialized incremental CSV file: ${config.csv_path}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
logger_1.logger.error('❌ Failed to initialize incremental files:', error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
start() {
|
|
91
|
+
this.startTime = Date.now();
|
|
92
|
+
this.results = [];
|
|
93
|
+
this.errorDetails.clear();
|
|
94
|
+
this.vuStartEvents = [];
|
|
95
|
+
this.batchBuffer = [];
|
|
96
|
+
this.batchCounter = 0;
|
|
97
|
+
this.csvHeaderWritten = false;
|
|
98
|
+
if (this.realtimeConfig.enabled && this.realtimeConfig.interval_ms) {
|
|
99
|
+
this.startBatchTimer();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
recordVUStart(vuId) {
|
|
103
|
+
this.vuStartEvents.push({
|
|
104
|
+
vu_id: vuId,
|
|
105
|
+
start_time: Date.now(),
|
|
106
|
+
load_pattern: this.loadPatternType
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
recordResult(result) {
|
|
110
|
+
this.results.push(result);
|
|
111
|
+
this.emit('result', result);
|
|
112
|
+
// Track detailed error information
|
|
113
|
+
if (!result.success) {
|
|
114
|
+
this.trackErrorDetail(result);
|
|
115
|
+
}
|
|
116
|
+
// Add to batch buffer for real-time processing
|
|
117
|
+
if (this.realtimeConfig.enabled) {
|
|
118
|
+
this.batchBuffer.push(result);
|
|
119
|
+
// Check if we should flush based on batch size (if not using intervals)
|
|
120
|
+
if (!this.realtimeConfig.interval_ms) {
|
|
121
|
+
const batchSize = this.realtimeConfig.batch_size || 10;
|
|
122
|
+
if (this.batchBuffer.length >= batchSize) {
|
|
123
|
+
this.flushBatch();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
recordError(vuId, scenario, action, error) {
|
|
129
|
+
const result = {
|
|
130
|
+
id: `${vuId}-${Date.now()}`,
|
|
131
|
+
vu_id: vuId,
|
|
132
|
+
iteration: 0,
|
|
133
|
+
scenario,
|
|
134
|
+
action,
|
|
135
|
+
timestamp: Date.now(),
|
|
136
|
+
duration: 0,
|
|
137
|
+
success: false,
|
|
138
|
+
error: error.message
|
|
139
|
+
};
|
|
140
|
+
this.recordResult(result);
|
|
141
|
+
}
|
|
142
|
+
async flushBatch() {
|
|
143
|
+
if (this.batchBuffer.length === 0)
|
|
144
|
+
return;
|
|
145
|
+
const batch = [...this.batchBuffer];
|
|
146
|
+
this.batchBuffer = [];
|
|
147
|
+
this.batchCounter++;
|
|
148
|
+
logger_1.logger.debug(`📤 Flushing batch #${this.batchCounter} with ${batch.length} results`);
|
|
149
|
+
try {
|
|
150
|
+
// Write to file if configured
|
|
151
|
+
if (this.realtimeConfig.file_output?.enabled) {
|
|
152
|
+
await this.writeBatchToFile(batch);
|
|
153
|
+
}
|
|
154
|
+
// Send to real-time endpoints
|
|
155
|
+
if (this.realtimeConfig.endpoints) {
|
|
156
|
+
await this.sendToRealTimeEndpoints(batch);
|
|
157
|
+
}
|
|
158
|
+
// Update incremental JSON/CSV files
|
|
159
|
+
if (this.realtimeConfig.incremental_files?.enabled) {
|
|
160
|
+
await this.updateIncrementalFiles(batch);
|
|
161
|
+
}
|
|
162
|
+
// Emit batch event for custom listeners
|
|
163
|
+
this.emit('batch', {
|
|
164
|
+
batch_number: this.batchCounter,
|
|
165
|
+
batch_size: batch.length,
|
|
166
|
+
results: batch,
|
|
167
|
+
timestamp: Date.now()
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
logger_1.logger.error('❌ Failed to flush metrics batch:', error);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async writeBatchToFile(batch) {
|
|
175
|
+
const config = this.realtimeConfig.file_output;
|
|
176
|
+
try {
|
|
177
|
+
const fs = require('fs').promises;
|
|
178
|
+
const path = require('path');
|
|
179
|
+
// Ensure directory exists
|
|
180
|
+
const dir = path.dirname(config.path);
|
|
181
|
+
await fs.mkdir(dir, { recursive: true });
|
|
182
|
+
let content;
|
|
183
|
+
if (config.format === 'csv') {
|
|
184
|
+
content = this.formatBatchAsCSV(batch);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// JSONL format (default)
|
|
188
|
+
content = batch.map(result => JSON.stringify({
|
|
189
|
+
...result,
|
|
190
|
+
timestamp: new Date(result.timestamp).toISOString(),
|
|
191
|
+
batch_number: this.batchCounter
|
|
192
|
+
})).join('\n') + '\n';
|
|
193
|
+
}
|
|
194
|
+
await fs.appendFile(config.path, content);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
logger_1.logger.error('❌ Failed to write batch to file:', error);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
formatBatchAsCSV(batch) {
|
|
201
|
+
return batch.map(result => [
|
|
202
|
+
new Date(result.timestamp).toISOString(),
|
|
203
|
+
this.batchCounter,
|
|
204
|
+
result.vu_id,
|
|
205
|
+
result.scenario,
|
|
206
|
+
result.action,
|
|
207
|
+
result.step_name || '',
|
|
208
|
+
result.duration,
|
|
209
|
+
result.success,
|
|
210
|
+
result.status || '',
|
|
211
|
+
(result.error || '').replace(/"/g, '""') // Escape quotes
|
|
212
|
+
].join(',')).join('\n') + '\n';
|
|
213
|
+
}
|
|
214
|
+
async updateIncrementalFiles(batch) {
|
|
215
|
+
const config = this.realtimeConfig.incremental_files;
|
|
216
|
+
try {
|
|
217
|
+
// Update incremental JSON file
|
|
218
|
+
if (config.json_path) {
|
|
219
|
+
await this.updateIncrementalJSON(batch, config.json_path);
|
|
220
|
+
}
|
|
221
|
+
// Update incremental CSV file
|
|
222
|
+
if (config.csv_path) {
|
|
223
|
+
await this.updateIncrementalCSV(batch, config.csv_path);
|
|
224
|
+
}
|
|
225
|
+
// Update summary files if configured
|
|
226
|
+
if (config.update_summary) {
|
|
227
|
+
await this.updateIncrementalSummary();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
logger_1.logger.error('❌ Failed to update incremental files:', error);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
async updateIncrementalJSON(batch, filePath) {
|
|
235
|
+
const fs = require('fs').promises;
|
|
236
|
+
try {
|
|
237
|
+
// Read existing file
|
|
238
|
+
const existingContent = await fs.readFile(filePath, 'utf8');
|
|
239
|
+
let existingData = [];
|
|
240
|
+
if (existingContent.trim()) {
|
|
241
|
+
existingData = JSON.parse(existingContent);
|
|
242
|
+
}
|
|
243
|
+
// Append new batch
|
|
244
|
+
const updatedData = [...existingData, ...batch];
|
|
245
|
+
// Write back to file
|
|
246
|
+
await fs.writeFile(filePath, JSON.stringify(updatedData, null, 2));
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
// If file doesn't exist or is corrupted, start fresh
|
|
250
|
+
await fs.writeFile(filePath, JSON.stringify(batch, null, 2));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async updateIncrementalCSV(batch, filePath) {
|
|
254
|
+
const fs = require('fs').promises;
|
|
255
|
+
const csvRows = batch.map(result => [
|
|
256
|
+
new Date(result.timestamp).toISOString(),
|
|
257
|
+
result.vu_id,
|
|
258
|
+
result.scenario,
|
|
259
|
+
result.action,
|
|
260
|
+
result.step_name || '',
|
|
261
|
+
result.duration,
|
|
262
|
+
result.success,
|
|
263
|
+
result.status || '',
|
|
264
|
+
(result.error || '').replace(/"/g, '""'), // Escape quotes
|
|
265
|
+
result.request_url || ''
|
|
266
|
+
].map(field => `"${field}"`).join(',')).join('\n') + '\n';
|
|
267
|
+
await fs.appendFile(filePath, csvRows);
|
|
268
|
+
}
|
|
269
|
+
async updateIncrementalSummary() {
|
|
270
|
+
const summary = this.getSummary();
|
|
271
|
+
const fs = require('fs').promises;
|
|
272
|
+
const path = require('path');
|
|
273
|
+
const config = this.realtimeConfig.incremental_files;
|
|
274
|
+
// Generate summary file paths based on the JSON path
|
|
275
|
+
const basePath = config.json_path ? path.dirname(config.json_path) : 'results';
|
|
276
|
+
const summaryJsonPath = path.join(basePath, 'summary-incremental.json');
|
|
277
|
+
const summaryHtmlPath = path.join(basePath, 'summary-incremental.html');
|
|
278
|
+
try {
|
|
279
|
+
// Write JSON summary
|
|
280
|
+
await fs.writeFile(summaryJsonPath, JSON.stringify({
|
|
281
|
+
last_updated: new Date().toISOString(),
|
|
282
|
+
test_duration: summary.total_duration,
|
|
283
|
+
...summary
|
|
284
|
+
}, null, 2));
|
|
285
|
+
// Generate simple HTML summary
|
|
286
|
+
const htmlSummary = this.generateSimpleHTMLSummary(summary);
|
|
287
|
+
await fs.writeFile(summaryHtmlPath, htmlSummary);
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
logger_1.logger.error('❌ Failed to update incremental summary:', error);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
generateSimpleHTMLSummary(summary) {
|
|
294
|
+
const lastUpdated = new Date().toISOString();
|
|
295
|
+
return `<!DOCTYPE html>
|
|
296
|
+
<html>
|
|
297
|
+
<head>
|
|
298
|
+
<title>Load Test Summary (Live)</title>
|
|
299
|
+
<meta http-equiv="refresh" content="5">
|
|
300
|
+
<style>
|
|
301
|
+
body { font-family: Arial, sans-serif; margin: 20px; }
|
|
302
|
+
.metric { margin: 10px 0; padding: 10px; background: #f5f5f5; border-radius: 4px; }
|
|
303
|
+
.success { color: #28a745; }
|
|
304
|
+
.error { color: #dc3545; }
|
|
305
|
+
.header { background: #007bff; color: white; padding: 15px; border-radius: 4px; }
|
|
306
|
+
</style>
|
|
307
|
+
</head>
|
|
308
|
+
<body>
|
|
309
|
+
<div class="header">
|
|
310
|
+
<h1>🚀 Load Test Progress</h1>
|
|
311
|
+
<p>Last Updated: ${lastUpdated}</p>
|
|
312
|
+
<p>Test Duration: ${summary.total_duration.toFixed(1)}s</p>
|
|
313
|
+
</div>
|
|
314
|
+
|
|
315
|
+
<div class="metric">
|
|
316
|
+
<h3>📊 Overall Statistics</h3>
|
|
317
|
+
<p><strong>Total Requests:</strong> ${summary.total_requests}</p>
|
|
318
|
+
<p><strong class="success">Successful:</strong> ${summary.successful_requests}</p>
|
|
319
|
+
<p><strong class="error">Failed:</strong> ${summary.failed_requests}</p>
|
|
320
|
+
<p><strong>Success Rate:</strong> ${summary.success_rate.toFixed(2)}%</p>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<div class="metric">
|
|
324
|
+
<h3>⏱️ Response Times</h3>
|
|
325
|
+
<p><strong>Average:</strong> ${summary.avg_response_time.toFixed(0)}ms</p>
|
|
326
|
+
<p><strong>Min:</strong> ${summary.min_response_time}ms</p>
|
|
327
|
+
<p><strong>Max:</strong> ${summary.max_response_time}ms</p>
|
|
328
|
+
<p><strong>95th Percentile:</strong> ${summary.percentiles[95] || 0}ms</p>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<div class="metric">
|
|
332
|
+
<h3>🔄 Throughput</h3>
|
|
333
|
+
<p><strong>Requests/sec:</strong> ${summary.requests_per_second.toFixed(2)}</p>
|
|
334
|
+
<p><strong>Bytes/sec:</strong> ${(summary.bytes_per_second || 0).toFixed(0)}</p>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
${summary.step_statistics.length > 0 ? `
|
|
338
|
+
<div class="metric">
|
|
339
|
+
<h3>📝 Step Statistics</h3>
|
|
340
|
+
${summary.step_statistics.slice(0, 5).map(step => `
|
|
341
|
+
<div style="margin: 10px 0; padding: 8px; background: white; border-left: 4px solid ${step.success_rate > 95 ? '#28a745' : '#ffc107'};">
|
|
342
|
+
<strong>${step.step_name}</strong> (${step.scenario})
|
|
343
|
+
<br>Success: ${step.success_rate.toFixed(1)}% | Avg: ${step.avg_response_time.toFixed(0)}ms | Count: ${step.total_requests}
|
|
344
|
+
</div>
|
|
345
|
+
`).join('')}
|
|
346
|
+
</div>
|
|
347
|
+
` : ''}
|
|
348
|
+
|
|
349
|
+
<div class="metric">
|
|
350
|
+
<small>Auto-refreshes every 5 seconds</small>
|
|
351
|
+
</div>
|
|
352
|
+
</body>
|
|
353
|
+
</html>`;
|
|
354
|
+
}
|
|
355
|
+
async sendToRealTimeEndpoints(batch) {
|
|
356
|
+
if (!this.realtimeConfig.endpoints)
|
|
357
|
+
return;
|
|
358
|
+
const promises = this.realtimeConfig.endpoints.map(endpoint => this.sendToEndpoint(batch, endpoint).catch(error => logger_1.logger.warn(`⚠️ Failed to send to ${endpoint.type} endpoint:`, error)));
|
|
359
|
+
await Promise.allSettled(promises);
|
|
360
|
+
}
|
|
361
|
+
async sendToEndpoint(batch, endpoint) {
|
|
362
|
+
switch (endpoint.type) {
|
|
363
|
+
case 'graphite':
|
|
364
|
+
await this.sendToGraphite(batch, endpoint);
|
|
365
|
+
break;
|
|
366
|
+
case 'webhook':
|
|
367
|
+
await this.sendToWebhook(batch, endpoint);
|
|
368
|
+
break;
|
|
369
|
+
case 'influxdb':
|
|
370
|
+
await this.sendToInfluxDB(batch, endpoint);
|
|
371
|
+
break;
|
|
372
|
+
case 'websocket':
|
|
373
|
+
await this.sendToWebSocket(batch, endpoint);
|
|
374
|
+
break;
|
|
375
|
+
default:
|
|
376
|
+
logger_1.logger.warn(`⚠️ Unknown endpoint type: ${endpoint.type}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
async sendToGraphite(batch, config) {
|
|
380
|
+
const net = require('net');
|
|
381
|
+
return new Promise((resolve, reject) => {
|
|
382
|
+
const client = net.createConnection(config.port, config.host);
|
|
383
|
+
client.on('connect', () => {
|
|
384
|
+
const metrics = batch.map(result => {
|
|
385
|
+
const timestamp = Math.floor(result.timestamp / 1000);
|
|
386
|
+
const metricName = `loadtest.${result.scenario}.${result.step_name || result.action}`;
|
|
387
|
+
return [
|
|
388
|
+
`${metricName}.duration ${result.duration} ${timestamp}`,
|
|
389
|
+
`${metricName}.success ${result.success ? 1 : 0} ${timestamp}`,
|
|
390
|
+
`${metricName}.count 1 ${timestamp}`
|
|
391
|
+
].join('\n');
|
|
392
|
+
}).join('\n') + '\n';
|
|
393
|
+
client.write(metrics);
|
|
394
|
+
client.end();
|
|
395
|
+
});
|
|
396
|
+
client.on('close', () => resolve());
|
|
397
|
+
client.on('error', reject);
|
|
398
|
+
setTimeout(() => {
|
|
399
|
+
client.destroy();
|
|
400
|
+
reject(new Error('Graphite connection timeout'));
|
|
401
|
+
}, 5000);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
async sendToWebhook(batch, config) {
|
|
405
|
+
const response = await fetch(config.url, {
|
|
406
|
+
method: 'POST',
|
|
407
|
+
headers: {
|
|
408
|
+
'Content-Type': 'application/json',
|
|
409
|
+
...config.headers
|
|
410
|
+
},
|
|
411
|
+
body: JSON.stringify({
|
|
412
|
+
timestamp: new Date().toISOString(),
|
|
413
|
+
batch_number: this.batchCounter,
|
|
414
|
+
batch_size: batch.length,
|
|
415
|
+
test_start_time: new Date(this.startTime).toISOString(),
|
|
416
|
+
results: batch
|
|
417
|
+
})
|
|
418
|
+
});
|
|
419
|
+
if (!response.ok) {
|
|
420
|
+
throw new Error(`Webhook failed: ${response.status} ${response.statusText}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
async sendToInfluxDB(batch, config) {
|
|
424
|
+
const lines = batch.map(result => {
|
|
425
|
+
const tags = [
|
|
426
|
+
`scenario=${result.scenario}`,
|
|
427
|
+
`step=${result.step_name || result.action}`,
|
|
428
|
+
`vu_id=${result.vu_id}`,
|
|
429
|
+
`success=${result.success}`
|
|
430
|
+
].join(',');
|
|
431
|
+
const fields = [
|
|
432
|
+
`duration=${result.duration}`,
|
|
433
|
+
`success=${result.success ? 'true' : 'false'}`,
|
|
434
|
+
`batch_number=${this.batchCounter}i`
|
|
435
|
+
];
|
|
436
|
+
if (result.status) {
|
|
437
|
+
fields.push(`status=${result.status}i`);
|
|
438
|
+
}
|
|
439
|
+
const timestamp = result.timestamp * 1000000; // Convert to nanoseconds
|
|
440
|
+
return `loadtest,${tags} ${fields.join(',')} ${timestamp}`;
|
|
441
|
+
}).join('\n');
|
|
442
|
+
const response = await fetch(`${config.url}/write?db=${config.database}`, {
|
|
443
|
+
method: 'POST',
|
|
444
|
+
headers: {
|
|
445
|
+
'Authorization': `Bearer ${config.token}`,
|
|
446
|
+
'Content-Type': 'text/plain'
|
|
447
|
+
},
|
|
448
|
+
body: lines
|
|
449
|
+
});
|
|
450
|
+
if (!response.ok) {
|
|
451
|
+
const errorText = await response.text();
|
|
452
|
+
throw new Error(`InfluxDB write failed: ${response.status} ${errorText}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
async sendToWebSocket(batch, config) {
|
|
456
|
+
return new Promise((resolve, reject) => {
|
|
457
|
+
const WebSocket = require('ws');
|
|
458
|
+
const ws = new WebSocket(config.url);
|
|
459
|
+
ws.on('open', () => {
|
|
460
|
+
ws.send(JSON.stringify({
|
|
461
|
+
type: 'metrics_batch',
|
|
462
|
+
timestamp: new Date().toISOString(),
|
|
463
|
+
batch_number: this.batchCounter,
|
|
464
|
+
test_start_time: new Date(this.startTime).toISOString(),
|
|
465
|
+
data: batch
|
|
466
|
+
}));
|
|
467
|
+
ws.close();
|
|
468
|
+
resolve();
|
|
469
|
+
});
|
|
470
|
+
ws.on('error', reject);
|
|
471
|
+
setTimeout(() => {
|
|
472
|
+
ws.close();
|
|
473
|
+
reject(new Error('WebSocket connection timeout'));
|
|
474
|
+
}, 5000);
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
// Force flush when test completes or stops
|
|
478
|
+
async finalize() {
|
|
479
|
+
if (this.batchTimer) {
|
|
480
|
+
clearInterval(this.batchTimer);
|
|
481
|
+
this.batchTimer = null;
|
|
482
|
+
}
|
|
483
|
+
// Flush any remaining results
|
|
484
|
+
if (this.batchBuffer.length > 0) {
|
|
485
|
+
await this.flushBatch();
|
|
486
|
+
}
|
|
487
|
+
logger_1.logger.info(`📊 Metrics collection finalized. Total batches: ${this.batchCounter}, Total results: ${this.results.length}`);
|
|
488
|
+
}
|
|
489
|
+
trackErrorDetail(result) {
|
|
490
|
+
const errorKey = `${result.scenario}:${result.action}:${result.status || 'NO_STATUS'}:${result.error}`;
|
|
491
|
+
const existing = this.errorDetails.get(errorKey);
|
|
492
|
+
if (existing) {
|
|
493
|
+
existing.count++;
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
this.errorDetails.set(errorKey, {
|
|
497
|
+
timestamp: result.timestamp,
|
|
498
|
+
vu_id: result.vu_id,
|
|
499
|
+
scenario: result.scenario,
|
|
500
|
+
action: result.action,
|
|
501
|
+
status: result.status,
|
|
502
|
+
error: result.error || 'Unknown error',
|
|
503
|
+
request_url: result.request_url,
|
|
504
|
+
response_body: result.response_body,
|
|
505
|
+
count: 1
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
getResults() {
|
|
510
|
+
return [...this.results];
|
|
511
|
+
}
|
|
512
|
+
// Add method to configure output paths without recreating the collector
|
|
513
|
+
// Add method to disable incremental files if needed
|
|
514
|
+
getSummary() {
|
|
515
|
+
const totalRequests = this.results.length;
|
|
516
|
+
const successfulRequests = this.results.filter(r => r.success).length;
|
|
517
|
+
const failedRequests = totalRequests - successfulRequests;
|
|
518
|
+
const durations = this.results.filter(r => r.success).map(r => r.duration);
|
|
519
|
+
const totalDuration = (Date.now() - this.startTime) / 1000;
|
|
520
|
+
// Error distribution by error message
|
|
521
|
+
const errorDistribution = {};
|
|
522
|
+
this.results.filter(r => !r.success).forEach(r => {
|
|
523
|
+
const error = r.error || 'Unknown error';
|
|
524
|
+
errorDistribution[error] = (errorDistribution[error] || 0) + 1;
|
|
525
|
+
});
|
|
526
|
+
// Status code distribution
|
|
527
|
+
const statusDistribution = {};
|
|
528
|
+
this.results.forEach(r => {
|
|
529
|
+
if (r.status) {
|
|
530
|
+
statusDistribution[r.status] = (statusDistribution[r.status] || 0) + 1;
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
const responseSizes = this.results
|
|
534
|
+
.filter(r => r.response_size)
|
|
535
|
+
.map(r => r.response_size);
|
|
536
|
+
return {
|
|
537
|
+
total_requests: totalRequests,
|
|
538
|
+
successful_requests: successfulRequests,
|
|
539
|
+
failed_requests: failedRequests,
|
|
540
|
+
success_rate: totalRequests > 0 ? (successfulRequests / totalRequests) * 100 : 0,
|
|
541
|
+
avg_response_time: durations.length > 0 ? durations.reduce((a, b) => a + b, 0) / durations.length : 0,
|
|
542
|
+
min_response_time: durations.length > 0 ? Math.min(...durations) : 0,
|
|
543
|
+
max_response_time: durations.length > 0 ? Math.max(...durations) : 0,
|
|
544
|
+
percentiles: this.calculatePercentiles(durations),
|
|
545
|
+
requests_per_second: totalDuration > 0 ? (totalRequests / totalDuration) : 0,
|
|
546
|
+
bytes_per_second: responseSizes.length > 0 && totalDuration > 0
|
|
547
|
+
? (responseSizes.reduce((a, b) => a + b, 0) / totalDuration) : 0,
|
|
548
|
+
total_duration: totalDuration,
|
|
549
|
+
error_distribution: errorDistribution,
|
|
550
|
+
status_distribution: statusDistribution,
|
|
551
|
+
error_details: Array.from(this.errorDetails.values()).sort((a, b) => b.count - a.count),
|
|
552
|
+
// New enhanced statistics
|
|
553
|
+
step_statistics: this.calculateStepStatistics(),
|
|
554
|
+
vu_ramp_up: this.vuStartEvents,
|
|
555
|
+
timeline_data: this.calculateTimelineData()
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
calculateStepStatistics() {
|
|
559
|
+
const stepGroups = new Map();
|
|
560
|
+
// Group results by step name and scenario
|
|
561
|
+
this.results.forEach(result => {
|
|
562
|
+
const key = `${result.scenario}:${result.step_name || result.action}`;
|
|
563
|
+
if (!stepGroups.has(key)) {
|
|
564
|
+
stepGroups.set(key, []);
|
|
565
|
+
}
|
|
566
|
+
stepGroups.get(key).push(result);
|
|
567
|
+
});
|
|
568
|
+
const stepStats = [];
|
|
569
|
+
for (const [key, results] of stepGroups) {
|
|
570
|
+
const [scenario, stepName] = key.split(':');
|
|
571
|
+
const successfulResults = results.filter(r => r.success);
|
|
572
|
+
// Include ALL results (both successful and failed) for response time calculations
|
|
573
|
+
// Failed requests also have response times that should be included in statistics
|
|
574
|
+
const responseTimes = results
|
|
575
|
+
.map(r => r.response_time || r.duration || 0)
|
|
576
|
+
.filter(rt => rt > 0);
|
|
577
|
+
// Error distribution for this step
|
|
578
|
+
const errorDistribution = {};
|
|
579
|
+
results.filter(r => !r.success).forEach(r => {
|
|
580
|
+
const error = r.error || 'Unknown error';
|
|
581
|
+
errorDistribution[error] = (errorDistribution[error] || 0) + 1;
|
|
582
|
+
});
|
|
583
|
+
// Status distribution for this step
|
|
584
|
+
const statusDistribution = {};
|
|
585
|
+
results.forEach(r => {
|
|
586
|
+
if (r.status) {
|
|
587
|
+
statusDistribution[r.status] = (statusDistribution[r.status] || 0) + 1;
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
stepStats.push({
|
|
591
|
+
step_name: stepName,
|
|
592
|
+
scenario: scenario,
|
|
593
|
+
total_requests: results.length,
|
|
594
|
+
successful_requests: successfulResults.length,
|
|
595
|
+
failed_requests: results.length - successfulResults.length,
|
|
596
|
+
success_rate: results.length > 0 ? (successfulResults.length / results.length) * 100 : 0,
|
|
597
|
+
avg_response_time: responseTimes.length > 0 ? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length : 0,
|
|
598
|
+
min_response_time: responseTimes.length > 0 ? Math.min(...responseTimes) : 0,
|
|
599
|
+
max_response_time: responseTimes.length > 0 ? Math.max(...responseTimes) : 0,
|
|
600
|
+
percentiles: this.calculatePercentiles(responseTimes),
|
|
601
|
+
response_times: responseTimes,
|
|
602
|
+
error_distribution: errorDistribution,
|
|
603
|
+
status_distribution: statusDistribution
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
return stepStats.sort((a, b) => b.total_requests - a.total_requests);
|
|
607
|
+
}
|
|
608
|
+
calculateTimelineData() {
|
|
609
|
+
if (this.results.length === 0)
|
|
610
|
+
return [];
|
|
611
|
+
const intervalMs = 5000; // 5 second intervals
|
|
612
|
+
const startTime = this.startTime;
|
|
613
|
+
const endTime = Date.now();
|
|
614
|
+
const timeline = [];
|
|
615
|
+
for (let time = startTime; time <= endTime; time += intervalMs) {
|
|
616
|
+
const intervalResults = this.results.filter(r => r.timestamp >= time && r.timestamp < time + intervalMs);
|
|
617
|
+
const successfulResults = intervalResults.filter(r => r.success);
|
|
618
|
+
// Calculate active VUs at this time
|
|
619
|
+
const activeVUs = this.vuStartEvents.filter(vu => vu.start_time <= time).length;
|
|
620
|
+
timeline.push({
|
|
621
|
+
timestamp: time,
|
|
622
|
+
time_label: new Date(time).toISOString(),
|
|
623
|
+
active_vus: activeVUs,
|
|
624
|
+
requests_count: intervalResults.length,
|
|
625
|
+
avg_response_time: successfulResults.length > 0
|
|
626
|
+
? successfulResults.reduce((sum, r) => sum + r.duration, 0) / successfulResults.length
|
|
627
|
+
: 0,
|
|
628
|
+
success_rate: intervalResults.length > 0
|
|
629
|
+
? (successfulResults.length / intervalResults.length) * 100
|
|
630
|
+
: 0,
|
|
631
|
+
throughput: intervalResults.length / (intervalMs / 1000)
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
return timeline;
|
|
635
|
+
}
|
|
636
|
+
calculatePercentiles(values) {
|
|
637
|
+
if (values.length === 0)
|
|
638
|
+
return {};
|
|
639
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
640
|
+
const percentiles = [50, 90, 95, 99, 99.9, 99.99];
|
|
641
|
+
const result = {};
|
|
642
|
+
percentiles.forEach(p => {
|
|
643
|
+
const index = Math.ceil((p / 100) * sorted.length) - 1;
|
|
644
|
+
result[p] = sorted[Math.max(0, index)];
|
|
645
|
+
});
|
|
646
|
+
return result;
|
|
647
|
+
}
|
|
648
|
+
clear() {
|
|
649
|
+
if (this.batchTimer) {
|
|
650
|
+
clearInterval(this.batchTimer);
|
|
651
|
+
this.batchTimer = null;
|
|
652
|
+
}
|
|
653
|
+
this.results = [];
|
|
654
|
+
this.errorDetails.clear();
|
|
655
|
+
this.vuStartEvents = [];
|
|
656
|
+
this.batchBuffer = [];
|
|
657
|
+
this.batchCounter = 0;
|
|
658
|
+
this.csvHeaderWritten = false;
|
|
659
|
+
this.startTime = 0;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
exports.MetricsCollector = MetricsCollector;
|