@testsmith/perfornium 0.6.4 → 0.6.5
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/dist/cli/commands/distributed.js +2 -2
- package/dist/cli/commands/report.js +2 -2
- package/dist/cli/commands/run.js +2 -0
- package/dist/config/parser.js +2 -2
- package/dist/config/types/global-config.d.ts +82 -2
- package/dist/config/types/scenario-config.d.ts +2 -2
- package/dist/core/data/data-manager.d.ts +70 -0
- package/dist/core/data/data-manager.js +186 -0
- package/dist/core/data/data-provider.d.ts +85 -0
- package/dist/core/data/data-provider.js +468 -0
- package/dist/core/data/index.d.ts +8 -0
- package/dist/core/data/index.js +13 -0
- package/dist/core/execution/check-evaluator.d.ts +10 -0
- package/dist/core/execution/check-evaluator.js +79 -0
- package/dist/core/execution/data-extractor.d.ts +6 -0
- package/dist/core/execution/data-extractor.js +70 -0
- package/dist/core/execution/index.d.ts +3 -0
- package/dist/core/execution/index.js +9 -0
- package/dist/core/execution/json-payload-processor.d.ts +7 -0
- package/dist/core/execution/json-payload-processor.js +140 -0
- package/dist/core/factories/index.d.ts +2 -0
- package/dist/core/factories/index.js +7 -0
- package/dist/core/factories/output-handler-factory.d.ts +10 -0
- package/dist/core/factories/output-handler-factory.js +91 -0
- package/dist/core/factories/protocol-handler-factory.d.ts +12 -0
- package/dist/core/factories/protocol-handler-factory.js +96 -0
- package/dist/core/index.d.ts +3 -2
- package/dist/core/index.js +8 -3
- package/dist/core/reporting/dashboard-reporter.d.ts +17 -0
- package/dist/core/reporting/dashboard-reporter.js +127 -0
- package/dist/core/reporting/index.d.ts +1 -0
- package/dist/core/reporting/index.js +5 -0
- package/dist/core/step-executor.d.ts +6 -20
- package/dist/core/step-executor.js +72 -366
- package/dist/core/strategies/index.d.ts +2 -0
- package/dist/core/strategies/index.js +7 -0
- package/dist/core/strategies/scenario-selector.d.ts +13 -0
- package/dist/core/strategies/scenario-selector.js +37 -0
- package/dist/core/strategies/think-time-strategy.d.ts +15 -0
- package/dist/core/strategies/think-time-strategy.js +71 -0
- package/dist/core/test-runner.d.ts +4 -11
- package/dist/core/test-runner.js +105 -312
- package/dist/core/virtual-user.d.ts +7 -37
- package/dist/core/virtual-user.js +29 -269
- package/dist/dashboard/routes/api.d.ts +64 -0
- package/dist/dashboard/routes/api.js +569 -0
- package/dist/dashboard/routes/index.d.ts +2 -0
- package/dist/dashboard/routes/index.js +7 -0
- package/dist/dashboard/routes/static.d.ts +6 -0
- package/dist/dashboard/routes/static.js +76 -0
- package/dist/dashboard/server.d.ts +8 -84
- package/dist/dashboard/server.js +76 -2007
- package/dist/dashboard/services/file-scanner.d.ts +7 -0
- package/dist/dashboard/services/file-scanner.js +114 -0
- package/dist/dashboard/services/index.d.ts +5 -0
- package/dist/dashboard/services/index.js +13 -0
- package/dist/dashboard/services/influxdb-service.d.ts +41 -0
- package/dist/dashboard/services/influxdb-service.js +329 -0
- package/dist/dashboard/services/metrics-parser.d.ts +12 -0
- package/dist/dashboard/services/metrics-parser.js +209 -0
- package/dist/dashboard/services/results-manager.d.ts +17 -0
- package/dist/dashboard/services/results-manager.js +311 -0
- package/dist/dashboard/services/test-executor.d.ts +41 -0
- package/dist/dashboard/services/test-executor.js +250 -0
- package/dist/dashboard/services/workers-manager.d.ts +13 -0
- package/dist/dashboard/services/workers-manager.js +81 -0
- package/dist/dashboard/templates/index.html +122 -0
- package/dist/dashboard/templates/scripts/main.js +3280 -0
- package/dist/dashboard/templates/styles.css +402 -0
- package/dist/dashboard/types.d.ts +168 -0
- package/dist/dashboard/types.js +2 -0
- package/dist/distributed/result-aggregator.js +1 -3
- package/dist/metrics/batch/batch-processor.d.ts +27 -0
- package/dist/metrics/batch/batch-processor.js +85 -0
- package/dist/metrics/batch/index.d.ts +1 -0
- package/dist/metrics/batch/index.js +5 -0
- package/dist/metrics/collector.d.ts +46 -45
- package/dist/metrics/collector.js +179 -640
- package/dist/metrics/core/error-tracker.d.ts +9 -0
- package/dist/metrics/core/error-tracker.js +52 -0
- package/dist/metrics/core/index.d.ts +3 -0
- package/dist/metrics/core/index.js +9 -0
- package/dist/metrics/core/result-storage.d.ts +19 -0
- package/dist/metrics/core/result-storage.js +56 -0
- package/dist/metrics/core/statistics-engine.d.ts +27 -0
- package/dist/metrics/core/statistics-engine.js +91 -0
- package/dist/metrics/output/file-writer.d.ts +19 -0
- package/dist/metrics/output/file-writer.js +129 -0
- package/dist/metrics/output/index.d.ts +2 -0
- package/dist/metrics/output/index.js +10 -0
- package/dist/metrics/output/influxdb-writer.d.ts +89 -0
- package/dist/metrics/output/influxdb-writer.js +404 -0
- package/dist/metrics/realtime/dispatcher.d.ts +18 -0
- package/dist/metrics/realtime/dispatcher.js +45 -0
- package/dist/metrics/realtime/endpoints/graphite.d.ts +3 -0
- package/dist/metrics/realtime/endpoints/graphite.js +61 -0
- package/dist/metrics/realtime/endpoints/influxdb.d.ts +3 -0
- package/dist/metrics/realtime/endpoints/influxdb.js +35 -0
- package/dist/metrics/realtime/endpoints/webhook.d.ts +3 -0
- package/dist/metrics/realtime/endpoints/webhook.js +22 -0
- package/dist/metrics/realtime/endpoints/websocket.d.ts +3 -0
- package/dist/metrics/realtime/endpoints/websocket.js +25 -0
- package/dist/metrics/realtime/index.d.ts +5 -0
- package/dist/metrics/realtime/index.js +13 -0
- package/dist/metrics/reporting/index.d.ts +3 -0
- package/dist/metrics/reporting/index.js +9 -0
- package/dist/metrics/reporting/step-statistics.d.ts +6 -0
- package/dist/metrics/reporting/step-statistics.js +59 -0
- package/dist/metrics/reporting/summary-generator.d.ts +16 -0
- package/dist/metrics/reporting/summary-generator.js +46 -0
- package/dist/metrics/reporting/timeline-calculator.d.ts +7 -0
- package/dist/metrics/reporting/timeline-calculator.js +86 -0
- package/dist/metrics/types.d.ts +58 -0
- package/dist/outputs/csv.d.ts +2 -0
- package/dist/outputs/csv.js +21 -2
- package/dist/outputs/json.js +6 -2
- package/dist/protocols/rest/handler.d.ts +4 -53
- package/dist/protocols/rest/handler.js +73 -454
- package/dist/protocols/rest/request/auth-handler.d.ts +4 -0
- package/dist/protocols/rest/request/auth-handler.js +30 -0
- package/dist/protocols/rest/request/body-processor.d.ts +11 -0
- package/dist/protocols/rest/request/body-processor.js +62 -0
- package/dist/protocols/rest/request/index.d.ts +2 -0
- package/dist/protocols/rest/request/index.js +7 -0
- package/dist/protocols/rest/response/checks.d.ts +6 -0
- package/dist/protocols/rest/response/checks.js +71 -0
- package/dist/protocols/rest/response/index.d.ts +2 -0
- package/dist/protocols/rest/response/index.js +7 -0
- package/dist/protocols/rest/response/size-calculator.d.ts +12 -0
- package/dist/protocols/rest/response/size-calculator.js +64 -0
- package/dist/protocols/web/browser/highlight.d.ts +7 -0
- package/dist/protocols/web/browser/highlight.js +47 -0
- package/dist/protocols/web/browser/index.d.ts +4 -0
- package/dist/protocols/web/browser/index.js +11 -0
- package/dist/protocols/web/browser/manager.d.ts +20 -0
- package/dist/protocols/web/browser/manager.js +189 -0
- package/dist/protocols/web/browser/screenshot.d.ts +8 -0
- package/dist/protocols/web/browser/screenshot.js +69 -0
- package/dist/protocols/web/browser/storage.d.ts +5 -0
- package/dist/protocols/web/browser/storage.js +45 -0
- package/dist/protocols/web/commands/index.d.ts +5 -0
- package/dist/protocols/web/commands/index.js +11 -0
- package/dist/protocols/web/commands/interaction.d.ts +13 -0
- package/dist/protocols/web/commands/interaction.js +68 -0
- package/dist/protocols/web/commands/measurement.d.ts +16 -0
- package/dist/protocols/web/commands/measurement.js +33 -0
- package/dist/protocols/web/commands/navigation.d.ts +11 -0
- package/dist/protocols/web/commands/navigation.js +43 -0
- package/dist/protocols/web/commands/types.d.ts +12 -0
- package/dist/protocols/web/commands/types.js +2 -0
- package/dist/protocols/web/commands/verification.d.ts +11 -0
- package/dist/protocols/web/commands/verification.js +98 -0
- package/dist/protocols/web/handler.d.ts +19 -30
- package/dist/protocols/web/handler.js +160 -650
- package/dist/protocols/web/network/capture.d.ts +19 -0
- package/dist/protocols/web/network/capture.js +225 -0
- package/dist/protocols/web/network/filters.d.ts +5 -0
- package/dist/protocols/web/network/filters.js +49 -0
- package/dist/protocols/web/network/index.d.ts +4 -0
- package/dist/protocols/web/network/index.js +9 -0
- package/dist/protocols/web/network/types.d.ts +13 -0
- package/dist/protocols/web/network/types.js +2 -0
- package/dist/protocols/web/network/utils.d.ts +8 -0
- package/dist/protocols/web/network/utils.js +29 -0
- package/dist/reporting/chart-data/index.d.ts +5 -0
- package/dist/reporting/chart-data/index.js +13 -0
- package/dist/reporting/chart-data/network.d.ts +25 -0
- package/dist/reporting/chart-data/network.js +78 -0
- package/dist/reporting/chart-data/scenario.d.ts +37 -0
- package/dist/reporting/chart-data/scenario.js +76 -0
- package/dist/reporting/chart-data/step-statistics.d.ts +24 -0
- package/dist/reporting/chart-data/step-statistics.js +94 -0
- package/dist/reporting/chart-data/throughput.d.ts +16 -0
- package/dist/reporting/chart-data/throughput.js +24 -0
- package/dist/reporting/chart-data/timeline.d.ts +17 -0
- package/dist/reporting/chart-data/timeline.js +46 -0
- package/dist/reporting/handlebars-helpers.d.ts +1 -0
- package/dist/reporting/handlebars-helpers.js +63 -0
- package/dist/reporting/{enhanced-html-generator.d.ts → html-generator.d.ts} +1 -1
- package/dist/reporting/{enhanced-html-generator.js → html-generator.js} +10 -7
- package/dist/reporting/templates/{enhanced-report.hbs → report.hbs} +9 -9
- package/dist/utils/data-utils.d.ts +17 -0
- package/dist/utils/data-utils.js +129 -0
- package/dist/utils/template.js +2 -2
- package/package.json +5 -2
- package/dist/core/csv-data-provider.d.ts +0 -47
- package/dist/core/csv-data-provider.js +0 -265
- package/dist/reporting/generator.d.ts +0 -42
- package/dist/reporting/generator.js +0 -1217
- package/dist/reporting/templates/html.hbs +0 -2453
|
@@ -3,178 +3,184 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.MetricsCollector = void 0;
|
|
4
4
|
const events_1 = require("events");
|
|
5
5
|
const logger_1 = require("../utils/logger");
|
|
6
|
+
const statistics_engine_1 = require("./core/statistics-engine");
|
|
7
|
+
const error_tracker_1 = require("./core/error-tracker");
|
|
8
|
+
const result_storage_1 = require("./core/result-storage");
|
|
9
|
+
const batch_processor_1 = require("./batch/batch-processor");
|
|
10
|
+
const file_writer_1 = require("./output/file-writer");
|
|
11
|
+
const dispatcher_1 = require("./realtime/dispatcher");
|
|
12
|
+
const summary_generator_1 = require("./reporting/summary-generator");
|
|
13
|
+
const influxdb_writer_1 = require("./output/influxdb-writer");
|
|
6
14
|
class MetricsCollector extends events_1.EventEmitter {
|
|
7
15
|
constructor(realtimeConfig) {
|
|
8
16
|
super();
|
|
9
|
-
this.results = [];
|
|
10
17
|
this.startTime = 0;
|
|
11
|
-
this.
|
|
12
|
-
|
|
13
|
-
this.loadPatternType = 'basic';
|
|
14
|
-
// Running statistics (accurate even when individual results are dropped)
|
|
15
|
-
this.runningStats = {
|
|
16
|
-
totalRequests: 0,
|
|
17
|
-
successfulRequests: 0,
|
|
18
|
-
failedRequests: 0,
|
|
19
|
-
totalDuration: 0, // Sum of all durations for averaging
|
|
20
|
-
minDuration: Infinity,
|
|
21
|
-
maxDuration: 0,
|
|
22
|
-
durations: [], // For percentile calculation (limited size)
|
|
23
|
-
};
|
|
24
|
-
this.maxDurationsForPercentiles = 10000; // Keep last N for percentiles
|
|
25
|
-
this.maxStoredResults = 50000; // Max individual results to keep in memory
|
|
26
|
-
this.batchBuffer = [];
|
|
27
|
-
this.batchTimer = null;
|
|
28
|
-
this.batchCounter = 0;
|
|
29
|
-
this.csvHeaderWritten = false;
|
|
30
|
-
// Default output paths
|
|
18
|
+
this.influxDBWriter = null;
|
|
19
|
+
// Default output path for live results (used by dashboard)
|
|
31
20
|
this.defaultJsonPath = 'results/live-results.json';
|
|
32
|
-
|
|
33
|
-
|
|
21
|
+
// Current test info for InfluxDB
|
|
22
|
+
this.currentTestId = '';
|
|
23
|
+
this.currentTestName = '';
|
|
24
|
+
// Initialize core modules
|
|
25
|
+
this.statisticsEngine = new statistics_engine_1.StatisticsEngine();
|
|
26
|
+
this.errorTracker = new error_tracker_1.ErrorTracker();
|
|
27
|
+
this.resultStorage = new result_storage_1.ResultStorage();
|
|
28
|
+
this.fileWriter = new file_writer_1.FileWriter();
|
|
29
|
+
this.realtimeDispatcher = new dispatcher_1.RealtimeDispatcher();
|
|
30
|
+
this.summaryGenerator = new summary_generator_1.SummaryGenerator();
|
|
31
|
+
// Enable incremental files by default for dashboard support
|
|
34
32
|
this.realtimeConfig = {
|
|
35
33
|
enabled: true,
|
|
36
|
-
batch_size: 10,
|
|
34
|
+
batch_size: 10,
|
|
37
35
|
incremental_files: {
|
|
38
36
|
enabled: true,
|
|
39
|
-
json_path: this.defaultJsonPath
|
|
40
|
-
csv_path: this.defaultCsvPath,
|
|
41
|
-
update_summary: true
|
|
37
|
+
json_path: this.defaultJsonPath
|
|
42
38
|
},
|
|
43
|
-
...realtimeConfig
|
|
39
|
+
...realtimeConfig
|
|
44
40
|
};
|
|
41
|
+
// Initialize batch processor with flush handler
|
|
42
|
+
this.batchProcessor = new batch_processor_1.BatchProcessor({
|
|
43
|
+
batchSize: this.realtimeConfig.batch_size || 10,
|
|
44
|
+
intervalMs: this.realtimeConfig.interval_ms,
|
|
45
|
+
maxBufferSize: 1000
|
|
46
|
+
});
|
|
47
|
+
this.batchProcessor.setFlushHandler(async (batch, batchNumber) => {
|
|
48
|
+
await this.handleBatchFlush(batch, batchNumber);
|
|
49
|
+
});
|
|
50
|
+
// Note: initializeRealtime is called separately via initialize() to properly await async operations
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Initialize async components (call before starting metrics collection)
|
|
54
|
+
*/
|
|
55
|
+
async initialize() {
|
|
45
56
|
if (this.realtimeConfig.enabled) {
|
|
46
|
-
this.initializeRealtime();
|
|
57
|
+
await this.initializeRealtime();
|
|
47
58
|
}
|
|
48
59
|
}
|
|
49
|
-
initializeRealtime() {
|
|
50
|
-
// Use interval-based batching if specified, otherwise use count-based
|
|
60
|
+
async initializeRealtime() {
|
|
51
61
|
if (this.realtimeConfig.interval_ms) {
|
|
52
|
-
this.
|
|
53
|
-
logger_1.logger.info(
|
|
62
|
+
this.batchProcessor.start();
|
|
63
|
+
logger_1.logger.info(`Real-time metrics enabled with ${this.realtimeConfig.interval_ms}ms intervals`);
|
|
54
64
|
}
|
|
55
65
|
else {
|
|
56
66
|
const batchSize = this.realtimeConfig.batch_size || 10;
|
|
57
|
-
logger_1.logger.info(
|
|
67
|
+
logger_1.logger.info(`Real-time metrics enabled with batch size: ${batchSize}`);
|
|
58
68
|
}
|
|
59
69
|
if (this.realtimeConfig.file_output?.enabled) {
|
|
60
|
-
logger_1.logger.info(
|
|
70
|
+
logger_1.logger.info(`Real-time file output enabled: ${this.realtimeConfig.file_output.path}`);
|
|
61
71
|
}
|
|
62
72
|
if (this.realtimeConfig.incremental_files?.enabled) {
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
const config = this.realtimeConfig.incremental_files;
|
|
74
|
+
logger_1.logger.info(`Live results file enabled: ${config.json_path}`);
|
|
75
|
+
await this.fileWriter.initialize({
|
|
76
|
+
enabled: true,
|
|
77
|
+
jsonPath: config.json_path
|
|
78
|
+
});
|
|
65
79
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
if (this.realtimeConfig.endpoints) {
|
|
81
|
+
this.realtimeDispatcher.setEndpoints(this.realtimeConfig.endpoints);
|
|
82
|
+
}
|
|
83
|
+
// Initialize InfluxDB writer if configured
|
|
84
|
+
if (this.realtimeConfig.influxdb?.enabled) {
|
|
85
|
+
logger_1.logger.info(`Initializing InfluxDB connection to ${this.realtimeConfig.influxdb.url || 'default URL'}...`);
|
|
86
|
+
this.influxDBWriter = new influxdb_writer_1.InfluxDBWriter({
|
|
87
|
+
url: this.realtimeConfig.influxdb.url,
|
|
88
|
+
token: this.realtimeConfig.influxdb.token,
|
|
89
|
+
org: this.realtimeConfig.influxdb.org,
|
|
90
|
+
bucket: this.realtimeConfig.influxdb.bucket,
|
|
91
|
+
batchSize: this.realtimeConfig.influxdb.batch_size,
|
|
92
|
+
flushInterval: this.realtimeConfig.influxdb.flush_interval
|
|
93
|
+
});
|
|
94
|
+
const connected = await this.influxDBWriter.connect();
|
|
95
|
+
if (connected) {
|
|
96
|
+
logger_1.logger.info('InfluxDB test metrics storage enabled and connected');
|
|
72
97
|
}
|
|
73
|
-
|
|
98
|
+
else {
|
|
99
|
+
logger_1.logger.warn('InfluxDB configured but connection failed - metrics will only be stored in files');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
logger_1.logger.debug('InfluxDB not configured - test metrics will be stored in files only');
|
|
104
|
+
}
|
|
74
105
|
}
|
|
75
|
-
async
|
|
76
|
-
const config = this.realtimeConfig.incremental_files;
|
|
106
|
+
async handleBatchFlush(batch, batchNumber) {
|
|
77
107
|
try {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
await fs.writeFile(config.json_path, '[]');
|
|
86
|
-
logger_1.logger.debug(`📄 Initialized incremental JSON file: ${config.json_path}`);
|
|
108
|
+
// Write to file if configured
|
|
109
|
+
if (this.realtimeConfig.file_output?.enabled) {
|
|
110
|
+
await this.fileWriter.writeBatchToFile(batch, {
|
|
111
|
+
enabled: true,
|
|
112
|
+
path: this.realtimeConfig.file_output.path,
|
|
113
|
+
format: this.realtimeConfig.file_output.format
|
|
114
|
+
}, batchNumber);
|
|
87
115
|
}
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
await fs.writeFile(config.csv_path, csvHeader);
|
|
94
|
-
this.csvHeaderWritten = true;
|
|
95
|
-
logger_1.logger.debug(`📄 Initialized incremental CSV file: ${config.csv_path}`);
|
|
116
|
+
// Send to real-time endpoints
|
|
117
|
+
await this.realtimeDispatcher.dispatch(batch, batchNumber);
|
|
118
|
+
// Update live results file (for dashboard)
|
|
119
|
+
if (this.realtimeConfig.incremental_files?.enabled) {
|
|
120
|
+
await this.fileWriter.updateIncrementalFiles(batch);
|
|
96
121
|
}
|
|
122
|
+
// Write to InfluxDB if enabled
|
|
123
|
+
if (this.influxDBWriter?.isEnabled()) {
|
|
124
|
+
await this.influxDBWriter.writeBatch(batch);
|
|
125
|
+
}
|
|
126
|
+
// Emit batch event for custom listeners
|
|
127
|
+
this.emit('batch', {
|
|
128
|
+
batch_number: batchNumber,
|
|
129
|
+
batch_size: batch.length,
|
|
130
|
+
results: batch,
|
|
131
|
+
timestamp: Date.now()
|
|
132
|
+
});
|
|
97
133
|
}
|
|
98
134
|
catch (error) {
|
|
99
|
-
logger_1.logger.error('
|
|
135
|
+
logger_1.logger.error('Failed to flush metrics batch:', error);
|
|
100
136
|
}
|
|
101
137
|
}
|
|
102
|
-
start() {
|
|
138
|
+
start(testName) {
|
|
103
139
|
this.startTime = Date.now();
|
|
104
|
-
this.
|
|
105
|
-
this.
|
|
106
|
-
|
|
107
|
-
this.
|
|
108
|
-
this.
|
|
109
|
-
this.
|
|
110
|
-
|
|
111
|
-
this.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
maxDuration: 0,
|
|
118
|
-
durations: [],
|
|
119
|
-
};
|
|
140
|
+
this.currentTestId = `test-${this.startTime}`;
|
|
141
|
+
this.currentTestName = testName || 'unnamed-test';
|
|
142
|
+
// Reset all modules
|
|
143
|
+
this.statisticsEngine.reset();
|
|
144
|
+
this.errorTracker.clear();
|
|
145
|
+
this.resultStorage.clear();
|
|
146
|
+
this.batchProcessor.reset();
|
|
147
|
+
this.fileWriter.reset();
|
|
148
|
+
this.realtimeDispatcher.setStartTime(this.startTime);
|
|
149
|
+
// Start InfluxDB test if enabled
|
|
150
|
+
if (this.influxDBWriter?.isEnabled()) {
|
|
151
|
+
this.influxDBWriter.startTest(this.currentTestId, this.currentTestName);
|
|
152
|
+
}
|
|
120
153
|
if (this.realtimeConfig.enabled && this.realtimeConfig.interval_ms) {
|
|
121
|
-
this.
|
|
154
|
+
this.batchProcessor.start();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Set the test name/ID for InfluxDB tagging
|
|
159
|
+
*/
|
|
160
|
+
setTestInfo(testName, testId) {
|
|
161
|
+
this.currentTestName = testName;
|
|
162
|
+
this.currentTestId = testId || `test-${Date.now()}`;
|
|
163
|
+
if (this.influxDBWriter?.isEnabled()) {
|
|
164
|
+
this.influxDBWriter.startTest(this.currentTestId, this.currentTestName);
|
|
122
165
|
}
|
|
123
166
|
}
|
|
124
167
|
recordVUStart(vuId) {
|
|
125
|
-
this.
|
|
126
|
-
vu_id: vuId,
|
|
127
|
-
start_time: Date.now(),
|
|
128
|
-
load_pattern: this.loadPatternType
|
|
129
|
-
});
|
|
168
|
+
this.resultStorage.recordVUStart(vuId);
|
|
130
169
|
}
|
|
131
170
|
recordResult(result) {
|
|
132
|
-
// Update running statistics
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
this.runningStats.totalDuration += duration;
|
|
138
|
-
this.runningStats.minDuration = Math.min(this.runningStats.minDuration, duration);
|
|
139
|
-
this.runningStats.maxDuration = Math.max(this.runningStats.maxDuration, duration);
|
|
140
|
-
// Keep limited durations for percentile calculation (reservoir sampling)
|
|
141
|
-
if (this.runningStats.durations.length < this.maxDurationsForPercentiles) {
|
|
142
|
-
this.runningStats.durations.push(duration);
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
// Randomly replace an existing duration (reservoir sampling)
|
|
146
|
-
const replaceIndex = Math.floor(Math.random() * this.runningStats.totalRequests);
|
|
147
|
-
if (replaceIndex < this.maxDurationsForPercentiles) {
|
|
148
|
-
this.runningStats.durations[replaceIndex] = duration;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
this.runningStats.failedRequests++;
|
|
154
|
-
}
|
|
155
|
-
// Store result only if under limit (for detailed analysis)
|
|
156
|
-
if (this.results.length < this.maxStoredResults) {
|
|
157
|
-
this.results.push(result);
|
|
158
|
-
}
|
|
171
|
+
// Update running statistics
|
|
172
|
+
const duration = result.duration || 0;
|
|
173
|
+
this.statisticsEngine.recordResult(duration, result.success);
|
|
174
|
+
// Store result
|
|
175
|
+
this.resultStorage.addResult(result);
|
|
159
176
|
this.emit('result', result);
|
|
160
|
-
// Track
|
|
177
|
+
// Track error details
|
|
161
178
|
if (!result.success) {
|
|
162
|
-
this.
|
|
179
|
+
this.errorTracker.trackError(result);
|
|
163
180
|
}
|
|
164
|
-
// Add to batch
|
|
181
|
+
// Add to batch for real-time processing
|
|
165
182
|
if (this.realtimeConfig.enabled) {
|
|
166
|
-
|
|
167
|
-
if (this.batchBuffer.length >= 1000) {
|
|
168
|
-
this.flushBatch();
|
|
169
|
-
}
|
|
170
|
-
this.batchBuffer.push(result);
|
|
171
|
-
// Check if we should flush based on batch size (if not using intervals)
|
|
172
|
-
if (!this.realtimeConfig.interval_ms) {
|
|
173
|
-
const batchSize = this.realtimeConfig.batch_size || 10;
|
|
174
|
-
if (this.batchBuffer.length >= batchSize) {
|
|
175
|
-
this.flushBatch();
|
|
176
|
-
}
|
|
177
|
-
}
|
|
183
|
+
this.batchProcessor.add(result);
|
|
178
184
|
}
|
|
179
185
|
}
|
|
180
186
|
recordError(vuId, scenario, action, error) {
|
|
@@ -191,528 +197,61 @@ class MetricsCollector extends events_1.EventEmitter {
|
|
|
191
197
|
};
|
|
192
198
|
this.recordResult(result);
|
|
193
199
|
}
|
|
194
|
-
async flushBatch() {
|
|
195
|
-
if (this.batchBuffer.length === 0)
|
|
196
|
-
return;
|
|
197
|
-
const batch = [...this.batchBuffer];
|
|
198
|
-
this.batchBuffer = [];
|
|
199
|
-
this.batchCounter++;
|
|
200
|
-
logger_1.logger.debug(`📤 Flushing batch #${this.batchCounter} with ${batch.length} results`);
|
|
201
|
-
try {
|
|
202
|
-
// Write to file if configured
|
|
203
|
-
if (this.realtimeConfig.file_output?.enabled) {
|
|
204
|
-
await this.writeBatchToFile(batch);
|
|
205
|
-
}
|
|
206
|
-
// Send to real-time endpoints
|
|
207
|
-
if (this.realtimeConfig.endpoints) {
|
|
208
|
-
await this.sendToRealTimeEndpoints(batch);
|
|
209
|
-
}
|
|
210
|
-
// Update incremental JSON/CSV files
|
|
211
|
-
if (this.realtimeConfig.incremental_files?.enabled) {
|
|
212
|
-
await this.updateIncrementalFiles(batch);
|
|
213
|
-
}
|
|
214
|
-
// Emit batch event for custom listeners
|
|
215
|
-
this.emit('batch', {
|
|
216
|
-
batch_number: this.batchCounter,
|
|
217
|
-
batch_size: batch.length,
|
|
218
|
-
results: batch,
|
|
219
|
-
timestamp: Date.now()
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
catch (error) {
|
|
223
|
-
logger_1.logger.error('❌ Failed to flush metrics batch:', error);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
async writeBatchToFile(batch) {
|
|
227
|
-
const config = this.realtimeConfig.file_output;
|
|
228
|
-
try {
|
|
229
|
-
const fs = require('fs').promises;
|
|
230
|
-
const path = require('path');
|
|
231
|
-
// Ensure directory exists
|
|
232
|
-
const dir = path.dirname(config.path);
|
|
233
|
-
await fs.mkdir(dir, { recursive: true });
|
|
234
|
-
let content;
|
|
235
|
-
if (config.format === 'csv') {
|
|
236
|
-
content = this.formatBatchAsCSV(batch);
|
|
237
|
-
}
|
|
238
|
-
else {
|
|
239
|
-
// JSONL format (default)
|
|
240
|
-
content = batch.map(result => JSON.stringify({
|
|
241
|
-
...result,
|
|
242
|
-
timestamp: new Date(result.timestamp).toISOString(),
|
|
243
|
-
batch_number: this.batchCounter
|
|
244
|
-
})).join('\n') + '\n';
|
|
245
|
-
}
|
|
246
|
-
await fs.appendFile(config.path, content);
|
|
247
|
-
}
|
|
248
|
-
catch (error) {
|
|
249
|
-
logger_1.logger.error('❌ Failed to write batch to file:', error);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
formatBatchAsCSV(batch) {
|
|
253
|
-
return batch.map(result => [
|
|
254
|
-
new Date(result.timestamp).toISOString(),
|
|
255
|
-
this.batchCounter,
|
|
256
|
-
result.vu_id,
|
|
257
|
-
result.scenario,
|
|
258
|
-
result.action,
|
|
259
|
-
result.step_name || '',
|
|
260
|
-
result.duration,
|
|
261
|
-
result.success,
|
|
262
|
-
result.status || '',
|
|
263
|
-
(result.error || '').replace(/"/g, '""') // Escape quotes
|
|
264
|
-
].join(',')).join('\n') + '\n';
|
|
265
|
-
}
|
|
266
|
-
async updateIncrementalFiles(batch) {
|
|
267
|
-
const config = this.realtimeConfig.incremental_files;
|
|
268
|
-
try {
|
|
269
|
-
// Update incremental JSON file
|
|
270
|
-
if (config.json_path) {
|
|
271
|
-
await this.updateIncrementalJSON(batch, config.json_path);
|
|
272
|
-
}
|
|
273
|
-
// Update incremental CSV file
|
|
274
|
-
if (config.csv_path) {
|
|
275
|
-
await this.updateIncrementalCSV(batch, config.csv_path);
|
|
276
|
-
}
|
|
277
|
-
// Update summary files if configured
|
|
278
|
-
if (config.update_summary) {
|
|
279
|
-
await this.updateIncrementalSummary();
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
catch (error) {
|
|
283
|
-
logger_1.logger.error('❌ Failed to update incremental files:', error);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
async updateIncrementalJSON(batch, filePath) {
|
|
287
|
-
const fs = require('fs').promises;
|
|
288
|
-
try {
|
|
289
|
-
// Read existing file
|
|
290
|
-
const existingContent = await fs.readFile(filePath, 'utf8');
|
|
291
|
-
let existingData = [];
|
|
292
|
-
if (existingContent.trim()) {
|
|
293
|
-
existingData = JSON.parse(existingContent);
|
|
294
|
-
}
|
|
295
|
-
// Append new batch
|
|
296
|
-
const updatedData = [...existingData, ...batch];
|
|
297
|
-
// Write back to file
|
|
298
|
-
await fs.writeFile(filePath, JSON.stringify(updatedData, null, 2));
|
|
299
|
-
}
|
|
300
|
-
catch (error) {
|
|
301
|
-
// If file doesn't exist or is corrupted, start fresh
|
|
302
|
-
await fs.writeFile(filePath, JSON.stringify(batch, null, 2));
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
async updateIncrementalCSV(batch, filePath) {
|
|
306
|
-
const fs = require('fs').promises;
|
|
307
|
-
const csvRows = batch.map(result => [
|
|
308
|
-
new Date(result.timestamp).toISOString(),
|
|
309
|
-
result.vu_id,
|
|
310
|
-
result.scenario,
|
|
311
|
-
result.action,
|
|
312
|
-
result.step_name || '',
|
|
313
|
-
result.duration,
|
|
314
|
-
result.success,
|
|
315
|
-
result.status || '',
|
|
316
|
-
(result.error || '').replace(/"/g, '""'), // Escape quotes
|
|
317
|
-
result.request_url || ''
|
|
318
|
-
].map(field => `"${field}"`).join(',')).join('\n') + '\n';
|
|
319
|
-
await fs.appendFile(filePath, csvRows);
|
|
320
|
-
}
|
|
321
|
-
async updateIncrementalSummary() {
|
|
322
|
-
const summary = this.getSummary();
|
|
323
|
-
const fs = require('fs').promises;
|
|
324
|
-
const path = require('path');
|
|
325
|
-
const config = this.realtimeConfig.incremental_files;
|
|
326
|
-
// Generate summary file paths based on the JSON path
|
|
327
|
-
const basePath = config.json_path ? path.dirname(config.json_path) : 'results';
|
|
328
|
-
const summaryJsonPath = path.join(basePath, 'summary-incremental.json');
|
|
329
|
-
const summaryHtmlPath = path.join(basePath, 'summary-incremental.html');
|
|
330
|
-
try {
|
|
331
|
-
// Write JSON summary
|
|
332
|
-
await fs.writeFile(summaryJsonPath, JSON.stringify({
|
|
333
|
-
last_updated: new Date().toISOString(),
|
|
334
|
-
test_duration: summary.total_duration,
|
|
335
|
-
...summary
|
|
336
|
-
}, null, 2));
|
|
337
|
-
// Generate simple HTML summary
|
|
338
|
-
const htmlSummary = this.generateSimpleHTMLSummary(summary);
|
|
339
|
-
await fs.writeFile(summaryHtmlPath, htmlSummary);
|
|
340
|
-
}
|
|
341
|
-
catch (error) {
|
|
342
|
-
logger_1.logger.error('❌ Failed to update incremental summary:', error);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
generateSimpleHTMLSummary(summary) {
|
|
346
|
-
const lastUpdated = new Date().toISOString();
|
|
347
|
-
return `<!DOCTYPE html>
|
|
348
|
-
<html>
|
|
349
|
-
<head>
|
|
350
|
-
<title>Load Test Summary (Live)</title>
|
|
351
|
-
<meta http-equiv="refresh" content="5">
|
|
352
|
-
<style>
|
|
353
|
-
body { font-family: Arial, sans-serif; margin: 20px; }
|
|
354
|
-
.metric { margin: 10px 0; padding: 10px; background: #f5f5f5; border-radius: 4px; }
|
|
355
|
-
.success { color: #28a745; }
|
|
356
|
-
.error { color: #dc3545; }
|
|
357
|
-
.header { background: #007bff; color: white; padding: 15px; border-radius: 4px; }
|
|
358
|
-
</style>
|
|
359
|
-
</head>
|
|
360
|
-
<body>
|
|
361
|
-
<div class="header">
|
|
362
|
-
<h1>🚀 Load Test Progress</h1>
|
|
363
|
-
<p>Last Updated: ${lastUpdated}</p>
|
|
364
|
-
<p>Test Duration: ${summary.total_duration.toFixed(1)}s</p>
|
|
365
|
-
</div>
|
|
366
|
-
|
|
367
|
-
<div class="metric">
|
|
368
|
-
<h3>📊 Overall Statistics</h3>
|
|
369
|
-
<p><strong>Total Requests:</strong> ${summary.total_requests}</p>
|
|
370
|
-
<p><strong class="success">Successful:</strong> ${summary.successful_requests}</p>
|
|
371
|
-
<p><strong class="error">Failed:</strong> ${summary.failed_requests}</p>
|
|
372
|
-
<p><strong>Success Rate:</strong> ${summary.success_rate.toFixed(2)}%</p>
|
|
373
|
-
</div>
|
|
374
|
-
|
|
375
|
-
<div class="metric">
|
|
376
|
-
<h3>⏱️ Response Times</h3>
|
|
377
|
-
<p><strong>Average:</strong> ${summary.avg_response_time.toFixed(0)}ms</p>
|
|
378
|
-
<p><strong>Min:</strong> ${summary.min_response_time}ms</p>
|
|
379
|
-
<p><strong>Max:</strong> ${summary.max_response_time}ms</p>
|
|
380
|
-
<p><strong>95th Percentile:</strong> ${summary.percentiles[95] || 0}ms</p>
|
|
381
|
-
</div>
|
|
382
|
-
|
|
383
|
-
<div class="metric">
|
|
384
|
-
<h3>🔄 Throughput</h3>
|
|
385
|
-
<p><strong>Requests/sec:</strong> ${summary.requests_per_second.toFixed(2)}</p>
|
|
386
|
-
<p><strong>Bytes/sec:</strong> ${(summary.bytes_per_second || 0).toFixed(0)}</p>
|
|
387
|
-
</div>
|
|
388
|
-
|
|
389
|
-
${summary.step_statistics.length > 0 ? `
|
|
390
|
-
<div class="metric">
|
|
391
|
-
<h3>📝 Step Statistics</h3>
|
|
392
|
-
${summary.step_statistics.slice(0, 5).map(step => `
|
|
393
|
-
<div style="margin: 10px 0; padding: 8px; background: white; border-left: 4px solid ${step.success_rate > 95 ? '#28a745' : '#ffc107'};">
|
|
394
|
-
<strong>${step.step_name}</strong> (${step.scenario})
|
|
395
|
-
<br>Success: ${step.success_rate.toFixed(1)}% | Avg: ${step.avg_response_time.toFixed(0)}ms | Count: ${step.total_requests}
|
|
396
|
-
</div>
|
|
397
|
-
`).join('')}
|
|
398
|
-
</div>
|
|
399
|
-
` : ''}
|
|
400
|
-
|
|
401
|
-
<div class="metric">
|
|
402
|
-
<small>Auto-refreshes every 5 seconds</small>
|
|
403
|
-
</div>
|
|
404
|
-
</body>
|
|
405
|
-
</html>`;
|
|
406
|
-
}
|
|
407
|
-
async sendToRealTimeEndpoints(batch) {
|
|
408
|
-
if (!this.realtimeConfig.endpoints)
|
|
409
|
-
return;
|
|
410
|
-
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)));
|
|
411
|
-
await Promise.allSettled(promises);
|
|
412
|
-
}
|
|
413
|
-
async sendToEndpoint(batch, endpoint) {
|
|
414
|
-
switch (endpoint.type) {
|
|
415
|
-
case 'graphite':
|
|
416
|
-
await this.sendToGraphite(batch, endpoint);
|
|
417
|
-
break;
|
|
418
|
-
case 'webhook':
|
|
419
|
-
await this.sendToWebhook(batch, endpoint);
|
|
420
|
-
break;
|
|
421
|
-
case 'influxdb':
|
|
422
|
-
await this.sendToInfluxDB(batch, endpoint);
|
|
423
|
-
break;
|
|
424
|
-
case 'websocket':
|
|
425
|
-
await this.sendToWebSocket(batch, endpoint);
|
|
426
|
-
break;
|
|
427
|
-
default:
|
|
428
|
-
logger_1.logger.warn(`⚠️ Unknown endpoint type: ${endpoint.type}`);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
async sendToGraphite(batch, config) {
|
|
432
|
-
const net = require('net');
|
|
433
|
-
return new Promise((resolve, reject) => {
|
|
434
|
-
const client = net.createConnection(config.port, config.host);
|
|
435
|
-
client.on('connect', () => {
|
|
436
|
-
const metrics = batch.map(result => {
|
|
437
|
-
const timestamp = Math.floor(result.timestamp / 1000);
|
|
438
|
-
const metricName = `loadtest.${result.scenario}.${result.step_name || result.action}`;
|
|
439
|
-
return [
|
|
440
|
-
`${metricName}.duration ${result.duration} ${timestamp}`,
|
|
441
|
-
`${metricName}.success ${result.success ? 1 : 0} ${timestamp}`,
|
|
442
|
-
`${metricName}.count 1 ${timestamp}`
|
|
443
|
-
].join('\n');
|
|
444
|
-
}).join('\n') + '\n';
|
|
445
|
-
client.write(metrics);
|
|
446
|
-
client.end();
|
|
447
|
-
});
|
|
448
|
-
client.on('close', () => resolve());
|
|
449
|
-
client.on('error', reject);
|
|
450
|
-
setTimeout(() => {
|
|
451
|
-
client.destroy();
|
|
452
|
-
reject(new Error('Graphite connection timeout'));
|
|
453
|
-
}, 5000);
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
async sendToWebhook(batch, config) {
|
|
457
|
-
const response = await fetch(config.url, {
|
|
458
|
-
method: 'POST',
|
|
459
|
-
headers: {
|
|
460
|
-
'Content-Type': 'application/json',
|
|
461
|
-
...config.headers
|
|
462
|
-
},
|
|
463
|
-
body: JSON.stringify({
|
|
464
|
-
timestamp: new Date().toISOString(),
|
|
465
|
-
batch_number: this.batchCounter,
|
|
466
|
-
batch_size: batch.length,
|
|
467
|
-
test_start_time: new Date(this.startTime).toISOString(),
|
|
468
|
-
results: batch
|
|
469
|
-
})
|
|
470
|
-
});
|
|
471
|
-
if (!response.ok) {
|
|
472
|
-
throw new Error(`Webhook failed: ${response.status} ${response.statusText}`);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
async sendToInfluxDB(batch, config) {
|
|
476
|
-
const lines = batch.map(result => {
|
|
477
|
-
const tags = [
|
|
478
|
-
`scenario=${result.scenario}`,
|
|
479
|
-
`step=${result.step_name || result.action}`,
|
|
480
|
-
`vu_id=${result.vu_id}`,
|
|
481
|
-
`success=${result.success}`
|
|
482
|
-
].join(',');
|
|
483
|
-
const fields = [
|
|
484
|
-
`duration=${result.duration}`,
|
|
485
|
-
`success=${result.success ? 'true' : 'false'}`,
|
|
486
|
-
`batch_number=${this.batchCounter}i`
|
|
487
|
-
];
|
|
488
|
-
if (result.status) {
|
|
489
|
-
fields.push(`status=${result.status}i`);
|
|
490
|
-
}
|
|
491
|
-
const timestamp = result.timestamp * 1000000; // Convert to nanoseconds
|
|
492
|
-
return `loadtest,${tags} ${fields.join(',')} ${timestamp}`;
|
|
493
|
-
}).join('\n');
|
|
494
|
-
const response = await fetch(`${config.url}/write?db=${config.database}`, {
|
|
495
|
-
method: 'POST',
|
|
496
|
-
headers: {
|
|
497
|
-
'Authorization': `Bearer ${config.token}`,
|
|
498
|
-
'Content-Type': 'text/plain'
|
|
499
|
-
},
|
|
500
|
-
body: lines
|
|
501
|
-
});
|
|
502
|
-
if (!response.ok) {
|
|
503
|
-
const errorText = await response.text();
|
|
504
|
-
throw new Error(`InfluxDB write failed: ${response.status} ${errorText}`);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
async sendToWebSocket(batch, config) {
|
|
508
|
-
return new Promise((resolve, reject) => {
|
|
509
|
-
const WebSocket = require('ws');
|
|
510
|
-
const ws = new WebSocket(config.url);
|
|
511
|
-
ws.on('open', () => {
|
|
512
|
-
ws.send(JSON.stringify({
|
|
513
|
-
type: 'metrics_batch',
|
|
514
|
-
timestamp: new Date().toISOString(),
|
|
515
|
-
batch_number: this.batchCounter,
|
|
516
|
-
test_start_time: new Date(this.startTime).toISOString(),
|
|
517
|
-
data: batch
|
|
518
|
-
}));
|
|
519
|
-
ws.close();
|
|
520
|
-
resolve();
|
|
521
|
-
});
|
|
522
|
-
ws.on('error', reject);
|
|
523
|
-
setTimeout(() => {
|
|
524
|
-
ws.close();
|
|
525
|
-
reject(new Error('WebSocket connection timeout'));
|
|
526
|
-
}, 5000);
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
// Force flush when test completes or stops
|
|
530
200
|
async finalize() {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
}
|
|
539
|
-
logger_1.logger.info(`📊 Metrics collection finalized. Total batches: ${this.batchCounter}, Total results: ${this.results.length}`);
|
|
201
|
+
await this.batchProcessor.finalize();
|
|
202
|
+
// Write summary to InfluxDB and finalize
|
|
203
|
+
if (this.influxDBWriter?.isEnabled()) {
|
|
204
|
+
const summary = this.getSummary();
|
|
205
|
+
await this.influxDBWriter.writeSummary(summary);
|
|
206
|
+
await this.influxDBWriter.finalize();
|
|
207
|
+
}
|
|
208
|
+
logger_1.logger.info(`Metrics collection finalized. Total batches: ${this.batchProcessor.getBatchCounter()}, Total results: ${this.resultStorage.getResultCount()}`);
|
|
540
209
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
else {
|
|
548
|
-
this.errorDetails.set(errorKey, {
|
|
549
|
-
timestamp: result.timestamp,
|
|
550
|
-
vu_id: result.vu_id,
|
|
551
|
-
scenario: result.scenario,
|
|
552
|
-
action: result.action,
|
|
553
|
-
status: result.status,
|
|
554
|
-
error: result.error || 'Unknown error',
|
|
555
|
-
request_url: result.request_url,
|
|
556
|
-
response_body: result.response_body,
|
|
557
|
-
count: 1
|
|
558
|
-
});
|
|
210
|
+
/**
|
|
211
|
+
* Record a network call to InfluxDB (if enabled)
|
|
212
|
+
*/
|
|
213
|
+
recordNetworkCall(call) {
|
|
214
|
+
if (this.influxDBWriter?.isEnabled()) {
|
|
215
|
+
this.influxDBWriter.writeNetworkCall(call);
|
|
559
216
|
}
|
|
560
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Get the InfluxDB writer instance (for direct access if needed)
|
|
220
|
+
*/
|
|
221
|
+
getInfluxDBWriter() {
|
|
222
|
+
return this.influxDBWriter;
|
|
223
|
+
}
|
|
561
224
|
getResults() {
|
|
562
|
-
return
|
|
225
|
+
return this.resultStorage.getResults();
|
|
563
226
|
}
|
|
564
227
|
getSummary() {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
const totalDuration = (Date.now() - this.startTime) / 1000;
|
|
572
|
-
// Calculate average from running totals (accurate even with limited stored results)
|
|
573
|
-
const avgResponseTime = successfulRequests > 0
|
|
574
|
-
? this.runningStats.totalDuration / successfulRequests
|
|
575
|
-
: 0;
|
|
576
|
-
// Error distribution from stored results (may be limited but representative)
|
|
577
|
-
const errorDistribution = {};
|
|
578
|
-
this.results.filter(r => !r.success).forEach(r => {
|
|
579
|
-
const error = r.error || 'Unknown error';
|
|
580
|
-
errorDistribution[error] = (errorDistribution[error] || 0) + 1;
|
|
581
|
-
});
|
|
582
|
-
// Status code distribution from stored results
|
|
583
|
-
const statusDistribution = {};
|
|
584
|
-
this.results.forEach(r => {
|
|
585
|
-
if (r.status) {
|
|
586
|
-
statusDistribution[r.status] = (statusDistribution[r.status] || 0) + 1;
|
|
587
|
-
}
|
|
228
|
+
return this.summaryGenerator.generate({
|
|
229
|
+
statisticsEngine: this.statisticsEngine,
|
|
230
|
+
errorTracker: this.errorTracker,
|
|
231
|
+
results: this.resultStorage.getResults(),
|
|
232
|
+
vuStartEvents: this.resultStorage.getVUStartEvents(),
|
|
233
|
+
startTime: this.startTime
|
|
588
234
|
});
|
|
589
|
-
const responseSizes = this.results
|
|
590
|
-
.filter(r => r.response_size)
|
|
591
|
-
.map(r => r.response_size);
|
|
592
|
-
return {
|
|
593
|
-
total_requests: totalRequests,
|
|
594
|
-
successful_requests: successfulRequests,
|
|
595
|
-
failed_requests: failedRequests,
|
|
596
|
-
success_rate: totalRequests > 0 ? (successfulRequests / totalRequests) * 100 : 0,
|
|
597
|
-
avg_response_time: avgResponseTime,
|
|
598
|
-
min_response_time: this.runningStats.minDuration === Infinity ? 0 : this.runningStats.minDuration,
|
|
599
|
-
max_response_time: this.runningStats.maxDuration,
|
|
600
|
-
percentiles: this.calculatePercentiles(durations),
|
|
601
|
-
requests_per_second: totalDuration > 0 ? (totalRequests / totalDuration) : 0,
|
|
602
|
-
bytes_per_second: responseSizes.length > 0 && totalDuration > 0
|
|
603
|
-
? (responseSizes.reduce((a, b) => a + b, 0) / totalDuration) : 0,
|
|
604
|
-
total_duration: totalDuration,
|
|
605
|
-
error_distribution: errorDistribution,
|
|
606
|
-
status_distribution: statusDistribution,
|
|
607
|
-
error_details: Array.from(this.errorDetails.values()).sort((a, b) => b.count - a.count),
|
|
608
|
-
// New enhanced statistics
|
|
609
|
-
step_statistics: this.calculateStepStatistics(),
|
|
610
|
-
vu_ramp_up: this.vuStartEvents,
|
|
611
|
-
timeline_data: this.calculateTimelineData()
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
calculateStepStatistics() {
|
|
615
|
-
const stepGroups = new Map();
|
|
616
|
-
// Group results by step name and scenario
|
|
617
|
-
this.results.forEach(result => {
|
|
618
|
-
const key = `${result.scenario}:${result.step_name || result.action}`;
|
|
619
|
-
if (!stepGroups.has(key)) {
|
|
620
|
-
stepGroups.set(key, []);
|
|
621
|
-
}
|
|
622
|
-
stepGroups.get(key).push(result);
|
|
623
|
-
});
|
|
624
|
-
const stepStats = [];
|
|
625
|
-
for (const [key, results] of stepGroups) {
|
|
626
|
-
const [scenario, stepName] = key.split(':');
|
|
627
|
-
const successfulResults = results.filter(r => r.success);
|
|
628
|
-
// Include ALL results (both successful and failed) for response time calculations
|
|
629
|
-
// Failed requests also have response times that should be included in statistics
|
|
630
|
-
const responseTimes = results
|
|
631
|
-
.map(r => r.response_time || r.duration || 0)
|
|
632
|
-
.filter(rt => rt > 0);
|
|
633
|
-
// Error distribution for this step
|
|
634
|
-
const errorDistribution = {};
|
|
635
|
-
results.filter(r => !r.success).forEach(r => {
|
|
636
|
-
const error = r.error || 'Unknown error';
|
|
637
|
-
errorDistribution[error] = (errorDistribution[error] || 0) + 1;
|
|
638
|
-
});
|
|
639
|
-
// Status distribution for this step
|
|
640
|
-
const statusDistribution = {};
|
|
641
|
-
results.forEach(r => {
|
|
642
|
-
if (r.status) {
|
|
643
|
-
statusDistribution[r.status] = (statusDistribution[r.status] || 0) + 1;
|
|
644
|
-
}
|
|
645
|
-
});
|
|
646
|
-
stepStats.push({
|
|
647
|
-
step_name: stepName,
|
|
648
|
-
scenario: scenario,
|
|
649
|
-
total_requests: results.length,
|
|
650
|
-
successful_requests: successfulResults.length,
|
|
651
|
-
failed_requests: results.length - successfulResults.length,
|
|
652
|
-
success_rate: results.length > 0 ? (successfulResults.length / results.length) * 100 : 0,
|
|
653
|
-
avg_response_time: responseTimes.length > 0 ? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length : 0,
|
|
654
|
-
min_response_time: responseTimes.length > 0 ? Math.min(...responseTimes) : 0,
|
|
655
|
-
max_response_time: responseTimes.length > 0 ? Math.max(...responseTimes) : 0,
|
|
656
|
-
percentiles: this.calculatePercentiles(responseTimes),
|
|
657
|
-
response_times: responseTimes,
|
|
658
|
-
error_distribution: errorDistribution,
|
|
659
|
-
status_distribution: statusDistribution
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
return stepStats.sort((a, b) => b.total_requests - a.total_requests);
|
|
663
|
-
}
|
|
664
|
-
calculateTimelineData() {
|
|
665
|
-
if (this.results.length === 0)
|
|
666
|
-
return [];
|
|
667
|
-
const intervalMs = 5000; // 5 second intervals
|
|
668
|
-
const startTime = this.startTime;
|
|
669
|
-
const endTime = Date.now();
|
|
670
|
-
const timeline = [];
|
|
671
|
-
for (let time = startTime; time <= endTime; time += intervalMs) {
|
|
672
|
-
const intervalResults = this.results.filter(r => r.timestamp >= time && r.timestamp < time + intervalMs);
|
|
673
|
-
const successfulResults = intervalResults.filter(r => r.success);
|
|
674
|
-
// Calculate active VUs at this time
|
|
675
|
-
const activeVUs = this.vuStartEvents.filter(vu => vu.start_time <= time).length;
|
|
676
|
-
timeline.push({
|
|
677
|
-
timestamp: time,
|
|
678
|
-
time_label: new Date(time).toISOString(),
|
|
679
|
-
active_vus: activeVUs,
|
|
680
|
-
requests_count: intervalResults.length,
|
|
681
|
-
avg_response_time: successfulResults.length > 0
|
|
682
|
-
? successfulResults.reduce((sum, r) => sum + r.duration, 0) / successfulResults.length
|
|
683
|
-
: 0,
|
|
684
|
-
success_rate: intervalResults.length > 0
|
|
685
|
-
? (successfulResults.length / intervalResults.length) * 100
|
|
686
|
-
: 0,
|
|
687
|
-
throughput: intervalResults.length / (intervalMs / 1000)
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
return timeline;
|
|
691
|
-
}
|
|
692
|
-
calculatePercentiles(values) {
|
|
693
|
-
if (values.length === 0)
|
|
694
|
-
return {};
|
|
695
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
696
|
-
const percentiles = [50, 90, 95, 99, 99.9, 99.99];
|
|
697
|
-
const result = {};
|
|
698
|
-
percentiles.forEach(p => {
|
|
699
|
-
const index = Math.ceil((p / 100) * sorted.length) - 1;
|
|
700
|
-
result[p] = sorted[Math.max(0, index)];
|
|
701
|
-
});
|
|
702
|
-
return result;
|
|
703
235
|
}
|
|
704
236
|
clear() {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
this.
|
|
710
|
-
this.
|
|
711
|
-
this.vuStartEvents = [];
|
|
712
|
-
this.batchBuffer = [];
|
|
713
|
-
this.batchCounter = 0;
|
|
714
|
-
this.csvHeaderWritten = false;
|
|
237
|
+
this.batchProcessor.stop();
|
|
238
|
+
this.statisticsEngine.reset();
|
|
239
|
+
this.errorTracker.clear();
|
|
240
|
+
this.resultStorage.clear();
|
|
241
|
+
this.batchProcessor.reset();
|
|
242
|
+
this.fileWriter.reset();
|
|
715
243
|
this.startTime = 0;
|
|
244
|
+
this.currentTestId = '';
|
|
245
|
+
this.currentTestName = '';
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Close all connections (call when done with the collector)
|
|
249
|
+
*/
|
|
250
|
+
async close() {
|
|
251
|
+
this.clear();
|
|
252
|
+
if (this.influxDBWriter) {
|
|
253
|
+
await this.influxDBWriter.close();
|
|
254
|
+
}
|
|
716
255
|
}
|
|
717
256
|
}
|
|
718
257
|
exports.MetricsCollector = MetricsCollector;
|