@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,353 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.StreamingJSONOutput = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const file_manager_1 = require("../utils/file-manager");
|
|
40
|
+
const logger_1 = require("../utils/logger");
|
|
41
|
+
class StreamingJSONOutput {
|
|
42
|
+
constructor(filePath, config) {
|
|
43
|
+
this.pendingResults = [];
|
|
44
|
+
this.totalResults = 0;
|
|
45
|
+
this.currentFileSize = 0;
|
|
46
|
+
this.rotationIndex = 0;
|
|
47
|
+
this.isFirstWrite = true;
|
|
48
|
+
this.testStartTime = Date.now();
|
|
49
|
+
this.DEFAULT_CONFIG = {
|
|
50
|
+
format: 'ndjson',
|
|
51
|
+
batchSize: 50,
|
|
52
|
+
flushInterval: 3000,
|
|
53
|
+
prettify: false,
|
|
54
|
+
includeMetadata: true,
|
|
55
|
+
rotateSize: 100 * 1024 * 1024, // 100MB
|
|
56
|
+
maxFiles: 3
|
|
57
|
+
};
|
|
58
|
+
this.filePath = filePath;
|
|
59
|
+
this.config = { ...this.DEFAULT_CONFIG, ...config };
|
|
60
|
+
}
|
|
61
|
+
async initialize() {
|
|
62
|
+
// Process template in file path
|
|
63
|
+
this.filePath = file_manager_1.FileManager.processFilePath(this.filePath);
|
|
64
|
+
// Ensure directory exists
|
|
65
|
+
const dir = path.dirname(this.filePath);
|
|
66
|
+
if (!fs.existsSync(dir)) {
|
|
67
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
// Create write stream
|
|
70
|
+
this.fileStream = fs.createWriteStream(this.filePath, { flags: 'w' });
|
|
71
|
+
// Write file header based on format
|
|
72
|
+
await this.writeHeader();
|
|
73
|
+
// Start flush timer
|
|
74
|
+
if (this.config.flushInterval > 0) {
|
|
75
|
+
this.flushTimer = setInterval(() => {
|
|
76
|
+
this.flushPending();
|
|
77
|
+
}, this.config.flushInterval);
|
|
78
|
+
}
|
|
79
|
+
logger_1.logger.info(`📊 Streaming JSON output initialized: ${this.filePath}`);
|
|
80
|
+
logger_1.logger.debug(`📊 Format: ${this.config.format}, batch: ${this.config.batchSize}`);
|
|
81
|
+
}
|
|
82
|
+
async writeHeader() {
|
|
83
|
+
if (!this.fileStream)
|
|
84
|
+
return;
|
|
85
|
+
switch (this.config.format) {
|
|
86
|
+
case 'json-array':
|
|
87
|
+
if (this.config.includeMetadata) {
|
|
88
|
+
const metadata = {
|
|
89
|
+
_metadata: {
|
|
90
|
+
format: 'perfornium-results',
|
|
91
|
+
version: '1.0',
|
|
92
|
+
test_start: new Date(this.testStartTime).toISOString(),
|
|
93
|
+
generator: 'perfornium-streaming-json'
|
|
94
|
+
},
|
|
95
|
+
results: []
|
|
96
|
+
};
|
|
97
|
+
this.fileStream.write('{\n');
|
|
98
|
+
this.fileStream.write(` "_metadata": ${JSON.stringify(metadata._metadata, null, 2).replace(/\n/g, '\n ')},\n`);
|
|
99
|
+
this.fileStream.write(' "results": [\n');
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
this.fileStream.write('[\n');
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
case 'json-stream':
|
|
106
|
+
if (this.config.includeMetadata) {
|
|
107
|
+
const metadata = {
|
|
108
|
+
type: 'metadata',
|
|
109
|
+
format: 'perfornium-results',
|
|
110
|
+
version: '1.0',
|
|
111
|
+
test_start: new Date(this.testStartTime).toISOString(),
|
|
112
|
+
generator: 'perfornium-streaming-json'
|
|
113
|
+
};
|
|
114
|
+
this.fileStream.write(JSON.stringify(metadata) + '\n');
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
case 'ndjson':
|
|
118
|
+
default:
|
|
119
|
+
// NDJSON doesn't need a header, but we can add metadata as first line
|
|
120
|
+
if (this.config.includeMetadata) {
|
|
121
|
+
const metadata = {
|
|
122
|
+
_type: 'metadata',
|
|
123
|
+
format: 'perfornium-results',
|
|
124
|
+
version: '1.0',
|
|
125
|
+
test_start: new Date(this.testStartTime).toISOString(),
|
|
126
|
+
generator: 'perfornium-streaming-json'
|
|
127
|
+
};
|
|
128
|
+
this.fileStream.write(JSON.stringify(metadata) + '\n');
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
this.currentFileSize = this.fileStream.bytesWritten || 0;
|
|
133
|
+
}
|
|
134
|
+
async writeResult(result) {
|
|
135
|
+
// Add processed timestamp and normalize data
|
|
136
|
+
const processedResult = {
|
|
137
|
+
...result,
|
|
138
|
+
timestamp: result.timestamp || Date.now(),
|
|
139
|
+
response_time: result.response_time || result.duration || 0,
|
|
140
|
+
name: result.name || result.action || 'request',
|
|
141
|
+
iteration: result.iteration || 0
|
|
142
|
+
};
|
|
143
|
+
this.pendingResults.push(processedResult);
|
|
144
|
+
this.totalResults++;
|
|
145
|
+
if (this.pendingResults.length >= this.config.batchSize) {
|
|
146
|
+
await this.flushPending();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async flushPending() {
|
|
150
|
+
if (this.pendingResults.length === 0 || !this.fileStream)
|
|
151
|
+
return;
|
|
152
|
+
try {
|
|
153
|
+
// Check if we need to rotate the file
|
|
154
|
+
if (this.config.rotateSize > 0 && this.currentFileSize > this.config.rotateSize) {
|
|
155
|
+
await this.rotateFile();
|
|
156
|
+
}
|
|
157
|
+
const recordsToWrite = [...this.pendingResults];
|
|
158
|
+
this.pendingResults = [];
|
|
159
|
+
// Write records based on format
|
|
160
|
+
await this.writeRecords(recordsToWrite);
|
|
161
|
+
this.currentFileSize = this.fileStream.bytesWritten || 0;
|
|
162
|
+
logger_1.logger.debug(`📊 Wrote ${recordsToWrite.length} JSON results (total: ${this.totalResults})`);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
logger_1.logger.error('❌ Failed to write JSON batch:', error);
|
|
166
|
+
// Re-add results to pending for retry
|
|
167
|
+
this.pendingResults.unshift(...this.pendingResults);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async writeRecords(records) {
|
|
171
|
+
if (!this.fileStream)
|
|
172
|
+
return;
|
|
173
|
+
switch (this.config.format) {
|
|
174
|
+
case 'json-array':
|
|
175
|
+
for (let i = 0; i < records.length; i++) {
|
|
176
|
+
const record = records[i];
|
|
177
|
+
const isLast = i === records.length - 1;
|
|
178
|
+
const json = this.config.prettify
|
|
179
|
+
? JSON.stringify(record, null, 2).replace(/\n/g, '\n ')
|
|
180
|
+
: JSON.stringify(record);
|
|
181
|
+
if (!this.isFirstWrite) {
|
|
182
|
+
this.fileStream.write(',\n');
|
|
183
|
+
}
|
|
184
|
+
this.fileStream.write(` ${json}`);
|
|
185
|
+
this.isFirstWrite = false;
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
case 'json-stream':
|
|
189
|
+
for (const record of records) {
|
|
190
|
+
const streamRecord = {
|
|
191
|
+
type: 'result',
|
|
192
|
+
timestamp: Date.now(),
|
|
193
|
+
data: record
|
|
194
|
+
};
|
|
195
|
+
const json = this.config.prettify
|
|
196
|
+
? JSON.stringify(streamRecord, null, 2)
|
|
197
|
+
: JSON.stringify(streamRecord);
|
|
198
|
+
this.fileStream.write(json + '\n');
|
|
199
|
+
}
|
|
200
|
+
break;
|
|
201
|
+
case 'ndjson':
|
|
202
|
+
default:
|
|
203
|
+
for (const record of records) {
|
|
204
|
+
const json = this.config.prettify
|
|
205
|
+
? JSON.stringify(record, null, 2)
|
|
206
|
+
: JSON.stringify(record);
|
|
207
|
+
this.fileStream.write(json + '\n');
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
async rotateFile() {
|
|
213
|
+
try {
|
|
214
|
+
// Close current file properly
|
|
215
|
+
await this.closeCurrentFile();
|
|
216
|
+
// Create rotated filename
|
|
217
|
+
const ext = path.extname(this.filePath);
|
|
218
|
+
const base = this.filePath.slice(0, -ext.length);
|
|
219
|
+
const rotatedPath = `${base}.${this.rotationIndex}${ext}`;
|
|
220
|
+
// Rename current file
|
|
221
|
+
if (fs.existsSync(this.filePath)) {
|
|
222
|
+
fs.renameSync(this.filePath, rotatedPath);
|
|
223
|
+
}
|
|
224
|
+
// Clean up old files
|
|
225
|
+
this.cleanupOldFiles(base, ext);
|
|
226
|
+
// Reset state
|
|
227
|
+
this.rotationIndex++;
|
|
228
|
+
this.currentFileSize = 0;
|
|
229
|
+
this.isFirstWrite = true;
|
|
230
|
+
// Reinitialize for new file
|
|
231
|
+
await this.initialize();
|
|
232
|
+
logger_1.logger.info(`🔄 Rotated JSON file: ${this.filePath}`);
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
logger_1.logger.error('❌ Failed to rotate JSON file:', error);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async closeCurrentFile() {
|
|
239
|
+
if (!this.fileStream)
|
|
240
|
+
return;
|
|
241
|
+
// Write file footer based on format
|
|
242
|
+
switch (this.config.format) {
|
|
243
|
+
case 'json-array':
|
|
244
|
+
this.fileStream.write('\n ]');
|
|
245
|
+
if (this.config.includeMetadata) {
|
|
246
|
+
const endMetadata = {
|
|
247
|
+
test_end: new Date().toISOString(),
|
|
248
|
+
total_results: this.totalResults,
|
|
249
|
+
duration_ms: Date.now() - this.testStartTime
|
|
250
|
+
};
|
|
251
|
+
this.fileStream.write(',\n "_summary": ' + JSON.stringify(endMetadata, null, 2).replace(/\n/g, '\n '));
|
|
252
|
+
}
|
|
253
|
+
this.fileStream.write('\n}\n');
|
|
254
|
+
break;
|
|
255
|
+
case 'json-stream':
|
|
256
|
+
if (this.config.includeMetadata) {
|
|
257
|
+
const endMetadata = {
|
|
258
|
+
type: 'summary',
|
|
259
|
+
test_end: new Date().toISOString(),
|
|
260
|
+
total_results: this.totalResults,
|
|
261
|
+
duration_ms: Date.now() - this.testStartTime
|
|
262
|
+
};
|
|
263
|
+
this.fileStream.write(JSON.stringify(endMetadata) + '\n');
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
266
|
+
case 'ndjson':
|
|
267
|
+
default:
|
|
268
|
+
if (this.config.includeMetadata) {
|
|
269
|
+
const endMetadata = {
|
|
270
|
+
_type: 'summary',
|
|
271
|
+
test_end: new Date().toISOString(),
|
|
272
|
+
total_results: this.totalResults,
|
|
273
|
+
duration_ms: Date.now() - this.testStartTime
|
|
274
|
+
};
|
|
275
|
+
this.fileStream.write(JSON.stringify(endMetadata) + '\n');
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
return new Promise((resolve) => {
|
|
280
|
+
this.fileStream.end(() => {
|
|
281
|
+
this.fileStream = undefined;
|
|
282
|
+
resolve();
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
cleanupOldFiles(basePath, extension) {
|
|
287
|
+
try {
|
|
288
|
+
const dir = path.dirname(basePath);
|
|
289
|
+
const basename = path.basename(basePath);
|
|
290
|
+
const files = fs.readdirSync(dir)
|
|
291
|
+
.filter(file => file.startsWith(basename) && file.endsWith(extension))
|
|
292
|
+
.map(file => ({
|
|
293
|
+
name: file,
|
|
294
|
+
path: path.join(dir, file),
|
|
295
|
+
index: this.extractRotationIndex(file)
|
|
296
|
+
}))
|
|
297
|
+
.filter(file => file.index >= 0)
|
|
298
|
+
.sort((a, b) => b.index - a.index);
|
|
299
|
+
// Remove excess files
|
|
300
|
+
const filesToRemove = files.slice(this.config.maxFiles);
|
|
301
|
+
for (const file of filesToRemove) {
|
|
302
|
+
fs.unlinkSync(file.path);
|
|
303
|
+
logger_1.logger.debug(`🗑️ Removed old JSON file: ${file.name}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
logger_1.logger.warn('⚠️ Failed to cleanup old JSON files:', error);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
extractRotationIndex(filename) {
|
|
311
|
+
const match = filename.match(/\.(\d+)\.json$/);
|
|
312
|
+
return match ? parseInt(match[1], 10) : -1;
|
|
313
|
+
}
|
|
314
|
+
async writeSummary(summary) {
|
|
315
|
+
// Write summary to separate file
|
|
316
|
+
const summaryPath = this.filePath.replace('.json', '_summary.json');
|
|
317
|
+
const summaryData = {
|
|
318
|
+
test_summary: {
|
|
319
|
+
timestamp: Date.now(),
|
|
320
|
+
test_end: new Date().toISOString(),
|
|
321
|
+
total_duration_ms: Date.now() - this.testStartTime,
|
|
322
|
+
...summary
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
const json = this.config.prettify
|
|
326
|
+
? JSON.stringify(summaryData, null, 2)
|
|
327
|
+
: JSON.stringify(summaryData);
|
|
328
|
+
fs.writeFileSync(summaryPath, json);
|
|
329
|
+
logger_1.logger.info(`📊 Written JSON summary: ${summaryPath}`);
|
|
330
|
+
}
|
|
331
|
+
async finalize() {
|
|
332
|
+
// Flush any remaining results
|
|
333
|
+
await this.flushPending();
|
|
334
|
+
// Clear flush timer
|
|
335
|
+
if (this.flushTimer) {
|
|
336
|
+
clearInterval(this.flushTimer);
|
|
337
|
+
this.flushTimer = undefined;
|
|
338
|
+
}
|
|
339
|
+
// Close file properly
|
|
340
|
+
await this.closeCurrentFile();
|
|
341
|
+
logger_1.logger.info(`📊 Streaming JSON finalized. Total results: ${this.totalResults}`);
|
|
342
|
+
}
|
|
343
|
+
getStats() {
|
|
344
|
+
return {
|
|
345
|
+
totalResults: this.totalResults,
|
|
346
|
+
pendingResults: this.pendingResults.length,
|
|
347
|
+
currentFileSize: this.currentFileSize,
|
|
348
|
+
rotationIndex: this.rotationIndex,
|
|
349
|
+
format: this.config.format || 'ndjson'
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
exports.StreamingJSONOutput = StreamingJSONOutput;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { OutputHandler } from './base';
|
|
2
|
+
import { TestResult, MetricsSummary } from '../metrics/types';
|
|
3
|
+
export declare class WebhookOutput implements OutputHandler {
|
|
4
|
+
private url;
|
|
5
|
+
private headers;
|
|
6
|
+
private format;
|
|
7
|
+
private template?;
|
|
8
|
+
private templateProcessor;
|
|
9
|
+
private results;
|
|
10
|
+
constructor(url: string, headers?: Record<string, string>, format?: string, template?: string);
|
|
11
|
+
initialize(): Promise<void>;
|
|
12
|
+
writeResult(result: TestResult): Promise<void>;
|
|
13
|
+
writeSummary(summary: MetricsSummary): Promise<void>;
|
|
14
|
+
private sendNotification;
|
|
15
|
+
finalize(): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.WebhookOutput = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const template_1 = require("../utils/template");
|
|
9
|
+
const logger_1 = require("../utils/logger");
|
|
10
|
+
class WebhookOutput {
|
|
11
|
+
constructor(url, headers = {}, format = 'json', template) {
|
|
12
|
+
this.templateProcessor = new template_1.TemplateProcessor();
|
|
13
|
+
this.results = [];
|
|
14
|
+
this.url = url;
|
|
15
|
+
this.headers = {
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
'User-Agent': 'Perfornium/1.0.0',
|
|
18
|
+
...headers
|
|
19
|
+
};
|
|
20
|
+
this.format = format;
|
|
21
|
+
this.template = template;
|
|
22
|
+
}
|
|
23
|
+
async initialize() {
|
|
24
|
+
// Test webhook connectivity
|
|
25
|
+
try {
|
|
26
|
+
await axios_1.default.head(this.url, {
|
|
27
|
+
headers: this.headers,
|
|
28
|
+
timeout: 5000
|
|
29
|
+
});
|
|
30
|
+
logger_1.logger.debug('🔗 Webhook endpoint is reachable');
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
logger_1.logger.warn('⚠️ Webhook endpoint may not be reachable:', error);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async writeResult(result) {
|
|
37
|
+
this.results.push(result);
|
|
38
|
+
// Optionally send real-time results for critical failures
|
|
39
|
+
if (!result.success && result.error && result.error.includes('timeout')) {
|
|
40
|
+
await this.sendNotification({
|
|
41
|
+
type: 'alert',
|
|
42
|
+
message: `Critical error in VU ${result.vu_id}: ${result.error}`,
|
|
43
|
+
timestamp: new Date().toISOString()
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async writeSummary(summary) {
|
|
48
|
+
try {
|
|
49
|
+
let payload;
|
|
50
|
+
if (this.template) {
|
|
51
|
+
// Use template to format payload
|
|
52
|
+
const context = {
|
|
53
|
+
summary,
|
|
54
|
+
test_name: 'Performance Test',
|
|
55
|
+
success_rate: summary.success_rate.toFixed(2),
|
|
56
|
+
avg_response_time: summary.avg_response_time.toFixed(2),
|
|
57
|
+
total_requests: summary.total_requests,
|
|
58
|
+
total_duration: summary.total_duration
|
|
59
|
+
};
|
|
60
|
+
const processedTemplate = this.templateProcessor.process(this.template, context);
|
|
61
|
+
payload = JSON.parse(processedTemplate);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
// Default payload format
|
|
65
|
+
payload = {
|
|
66
|
+
type: 'test_completed',
|
|
67
|
+
timestamp: new Date().toISOString(),
|
|
68
|
+
summary: {
|
|
69
|
+
total_requests: summary.total_requests,
|
|
70
|
+
success_rate: `${summary.success_rate.toFixed(2)}%`,
|
|
71
|
+
avg_response_time: `${summary.avg_response_time.toFixed(2)}ms`,
|
|
72
|
+
requests_per_second: summary.requests_per_second.toFixed(2),
|
|
73
|
+
duration: `${(summary.total_duration / 1000).toFixed(1)}s`,
|
|
74
|
+
status: summary.success_rate >= 95 ? 'success' : summary.success_rate >= 90 ? 'warning' : 'error'
|
|
75
|
+
},
|
|
76
|
+
errors: Object.keys(summary.error_distribution).length > 0 ? summary.error_distribution : null
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
await this.sendNotification(payload);
|
|
80
|
+
logger_1.logger.debug('🔗 Webhook notification sent successfully');
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
logger_1.logger.warn('⚠️ Failed to send webhook notification:', error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async sendNotification(payload) {
|
|
87
|
+
await axios_1.default.post(this.url, payload, {
|
|
88
|
+
headers: this.headers,
|
|
89
|
+
timeout: 10000
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async finalize() {
|
|
93
|
+
// Nothing to finalize for webhook
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
exports.WebhookOutput = WebhookOutput;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { VUContext } from '../config/types';
|
|
2
|
+
export interface ProtocolResult {
|
|
3
|
+
success: boolean;
|
|
4
|
+
status?: number;
|
|
5
|
+
status_text?: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
error_code?: string;
|
|
8
|
+
data?: any;
|
|
9
|
+
response_size?: number;
|
|
10
|
+
response_time?: number;
|
|
11
|
+
duration?: number;
|
|
12
|
+
shouldRecord?: boolean;
|
|
13
|
+
request_url?: string;
|
|
14
|
+
request_method?: string;
|
|
15
|
+
request_headers?: Record<string, string>;
|
|
16
|
+
request_body?: string;
|
|
17
|
+
response_headers?: Record<string, string>;
|
|
18
|
+
response_body?: string;
|
|
19
|
+
sample_start?: number;
|
|
20
|
+
connect_time?: number;
|
|
21
|
+
latency?: number;
|
|
22
|
+
sent_bytes?: number;
|
|
23
|
+
headers_size_sent?: number;
|
|
24
|
+
body_size_sent?: number;
|
|
25
|
+
headers_size_received?: number;
|
|
26
|
+
body_size_received?: number;
|
|
27
|
+
data_type?: 'text' | 'bin' | '';
|
|
28
|
+
custom_metrics?: Record<string, any>;
|
|
29
|
+
}
|
|
30
|
+
export interface ProtocolHandler {
|
|
31
|
+
execute(action: any, context: VUContext): Promise<ProtocolResult>;
|
|
32
|
+
cleanup?(): Promise<void>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ProtocolHandler, ProtocolResult } from '../base';
|
|
2
|
+
import { VUContext, RESTStep, DebugConfig } from '../../config/types';
|
|
3
|
+
export declare class RESTHandler implements ProtocolHandler {
|
|
4
|
+
private axiosInstance;
|
|
5
|
+
private debugConfig?;
|
|
6
|
+
private connectionTimings;
|
|
7
|
+
constructor(baseURL?: string, defaultHeaders?: Record<string, string>, timeout?: number, debugConfig?: DebugConfig);
|
|
8
|
+
execute(request: RESTStep, context: VUContext): Promise<ProtocolResult>;
|
|
9
|
+
private prepareRequestConfig;
|
|
10
|
+
/**
|
|
11
|
+
* Handle authentication configuration
|
|
12
|
+
*/
|
|
13
|
+
private handleAuthentication;
|
|
14
|
+
/**
|
|
15
|
+
* Check if Content-Type header is already set (case-insensitive)
|
|
16
|
+
*/
|
|
17
|
+
private hasContentTypeHeader;
|
|
18
|
+
/**
|
|
19
|
+
* Process body payload and detect content type
|
|
20
|
+
*/
|
|
21
|
+
private processBodyPayload;
|
|
22
|
+
/**
|
|
23
|
+
* Detect if string is valid JSON
|
|
24
|
+
*/
|
|
25
|
+
private isJsonString;
|
|
26
|
+
/**
|
|
27
|
+
* Detect if string is XML
|
|
28
|
+
*/
|
|
29
|
+
private isXmlString;
|
|
30
|
+
/**
|
|
31
|
+
* Check if body contains template expressions
|
|
32
|
+
*/
|
|
33
|
+
private isTemplateString;
|
|
34
|
+
/**
|
|
35
|
+
* Detect content type from template file extension or content
|
|
36
|
+
*/
|
|
37
|
+
private detectTemplateContentType;
|
|
38
|
+
private createSuccessResult;
|
|
39
|
+
private handleError;
|
|
40
|
+
private addDetailedInfo;
|
|
41
|
+
private logRequestDetails;
|
|
42
|
+
private logResponseDetails;
|
|
43
|
+
private runChecksOptimized;
|
|
44
|
+
private buildURL;
|
|
45
|
+
private getResponseText;
|
|
46
|
+
private getJsonPathOptimized;
|
|
47
|
+
private flattenHeaders;
|
|
48
|
+
private truncateIfNeeded;
|
|
49
|
+
private shouldCaptureDetails;
|
|
50
|
+
/**
|
|
51
|
+
* Calculate request header and body sizes
|
|
52
|
+
*/
|
|
53
|
+
private calculateRequestSizes;
|
|
54
|
+
/**
|
|
55
|
+
* Calculate response header and body sizes
|
|
56
|
+
*/
|
|
57
|
+
private calculateResponseSizes;
|
|
58
|
+
/**
|
|
59
|
+
* Detect data type from response
|
|
60
|
+
*/
|
|
61
|
+
private detectDataType;
|
|
62
|
+
/**
|
|
63
|
+
* Generate JMeter-style thread name
|
|
64
|
+
* Format: "iteration. step_name vu_id-iteration"
|
|
65
|
+
*/
|
|
66
|
+
private generateThreadName;
|
|
67
|
+
}
|