@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.
Files changed (190) hide show
  1. package/dist/cli/commands/distributed.js +2 -2
  2. package/dist/cli/commands/report.js +2 -2
  3. package/dist/cli/commands/run.js +2 -0
  4. package/dist/config/parser.js +2 -2
  5. package/dist/config/types/global-config.d.ts +82 -2
  6. package/dist/config/types/scenario-config.d.ts +2 -2
  7. package/dist/core/data/data-manager.d.ts +70 -0
  8. package/dist/core/data/data-manager.js +186 -0
  9. package/dist/core/data/data-provider.d.ts +85 -0
  10. package/dist/core/data/data-provider.js +468 -0
  11. package/dist/core/data/index.d.ts +8 -0
  12. package/dist/core/data/index.js +13 -0
  13. package/dist/core/execution/check-evaluator.d.ts +10 -0
  14. package/dist/core/execution/check-evaluator.js +79 -0
  15. package/dist/core/execution/data-extractor.d.ts +6 -0
  16. package/dist/core/execution/data-extractor.js +70 -0
  17. package/dist/core/execution/index.d.ts +3 -0
  18. package/dist/core/execution/index.js +9 -0
  19. package/dist/core/execution/json-payload-processor.d.ts +7 -0
  20. package/dist/core/execution/json-payload-processor.js +140 -0
  21. package/dist/core/factories/index.d.ts +2 -0
  22. package/dist/core/factories/index.js +7 -0
  23. package/dist/core/factories/output-handler-factory.d.ts +10 -0
  24. package/dist/core/factories/output-handler-factory.js +91 -0
  25. package/dist/core/factories/protocol-handler-factory.d.ts +12 -0
  26. package/dist/core/factories/protocol-handler-factory.js +96 -0
  27. package/dist/core/index.d.ts +3 -2
  28. package/dist/core/index.js +8 -3
  29. package/dist/core/reporting/dashboard-reporter.d.ts +17 -0
  30. package/dist/core/reporting/dashboard-reporter.js +127 -0
  31. package/dist/core/reporting/index.d.ts +1 -0
  32. package/dist/core/reporting/index.js +5 -0
  33. package/dist/core/step-executor.d.ts +6 -20
  34. package/dist/core/step-executor.js +72 -366
  35. package/dist/core/strategies/index.d.ts +2 -0
  36. package/dist/core/strategies/index.js +7 -0
  37. package/dist/core/strategies/scenario-selector.d.ts +13 -0
  38. package/dist/core/strategies/scenario-selector.js +37 -0
  39. package/dist/core/strategies/think-time-strategy.d.ts +15 -0
  40. package/dist/core/strategies/think-time-strategy.js +71 -0
  41. package/dist/core/test-runner.d.ts +4 -11
  42. package/dist/core/test-runner.js +105 -312
  43. package/dist/core/virtual-user.d.ts +7 -37
  44. package/dist/core/virtual-user.js +29 -269
  45. package/dist/dashboard/routes/api.d.ts +64 -0
  46. package/dist/dashboard/routes/api.js +569 -0
  47. package/dist/dashboard/routes/index.d.ts +2 -0
  48. package/dist/dashboard/routes/index.js +7 -0
  49. package/dist/dashboard/routes/static.d.ts +6 -0
  50. package/dist/dashboard/routes/static.js +76 -0
  51. package/dist/dashboard/server.d.ts +8 -84
  52. package/dist/dashboard/server.js +76 -2007
  53. package/dist/dashboard/services/file-scanner.d.ts +7 -0
  54. package/dist/dashboard/services/file-scanner.js +114 -0
  55. package/dist/dashboard/services/index.d.ts +5 -0
  56. package/dist/dashboard/services/index.js +13 -0
  57. package/dist/dashboard/services/influxdb-service.d.ts +41 -0
  58. package/dist/dashboard/services/influxdb-service.js +329 -0
  59. package/dist/dashboard/services/metrics-parser.d.ts +12 -0
  60. package/dist/dashboard/services/metrics-parser.js +209 -0
  61. package/dist/dashboard/services/results-manager.d.ts +17 -0
  62. package/dist/dashboard/services/results-manager.js +311 -0
  63. package/dist/dashboard/services/test-executor.d.ts +41 -0
  64. package/dist/dashboard/services/test-executor.js +250 -0
  65. package/dist/dashboard/services/workers-manager.d.ts +13 -0
  66. package/dist/dashboard/services/workers-manager.js +81 -0
  67. package/dist/dashboard/templates/index.html +122 -0
  68. package/dist/dashboard/templates/scripts/main.js +3280 -0
  69. package/dist/dashboard/templates/styles.css +402 -0
  70. package/dist/dashboard/types.d.ts +168 -0
  71. package/dist/dashboard/types.js +2 -0
  72. package/dist/distributed/result-aggregator.js +1 -3
  73. package/dist/metrics/batch/batch-processor.d.ts +27 -0
  74. package/dist/metrics/batch/batch-processor.js +85 -0
  75. package/dist/metrics/batch/index.d.ts +1 -0
  76. package/dist/metrics/batch/index.js +5 -0
  77. package/dist/metrics/collector.d.ts +46 -45
  78. package/dist/metrics/collector.js +179 -640
  79. package/dist/metrics/core/error-tracker.d.ts +9 -0
  80. package/dist/metrics/core/error-tracker.js +52 -0
  81. package/dist/metrics/core/index.d.ts +3 -0
  82. package/dist/metrics/core/index.js +9 -0
  83. package/dist/metrics/core/result-storage.d.ts +19 -0
  84. package/dist/metrics/core/result-storage.js +56 -0
  85. package/dist/metrics/core/statistics-engine.d.ts +27 -0
  86. package/dist/metrics/core/statistics-engine.js +91 -0
  87. package/dist/metrics/output/file-writer.d.ts +19 -0
  88. package/dist/metrics/output/file-writer.js +129 -0
  89. package/dist/metrics/output/index.d.ts +2 -0
  90. package/dist/metrics/output/index.js +10 -0
  91. package/dist/metrics/output/influxdb-writer.d.ts +89 -0
  92. package/dist/metrics/output/influxdb-writer.js +404 -0
  93. package/dist/metrics/realtime/dispatcher.d.ts +18 -0
  94. package/dist/metrics/realtime/dispatcher.js +45 -0
  95. package/dist/metrics/realtime/endpoints/graphite.d.ts +3 -0
  96. package/dist/metrics/realtime/endpoints/graphite.js +61 -0
  97. package/dist/metrics/realtime/endpoints/influxdb.d.ts +3 -0
  98. package/dist/metrics/realtime/endpoints/influxdb.js +35 -0
  99. package/dist/metrics/realtime/endpoints/webhook.d.ts +3 -0
  100. package/dist/metrics/realtime/endpoints/webhook.js +22 -0
  101. package/dist/metrics/realtime/endpoints/websocket.d.ts +3 -0
  102. package/dist/metrics/realtime/endpoints/websocket.js +25 -0
  103. package/dist/metrics/realtime/index.d.ts +5 -0
  104. package/dist/metrics/realtime/index.js +13 -0
  105. package/dist/metrics/reporting/index.d.ts +3 -0
  106. package/dist/metrics/reporting/index.js +9 -0
  107. package/dist/metrics/reporting/step-statistics.d.ts +6 -0
  108. package/dist/metrics/reporting/step-statistics.js +59 -0
  109. package/dist/metrics/reporting/summary-generator.d.ts +16 -0
  110. package/dist/metrics/reporting/summary-generator.js +46 -0
  111. package/dist/metrics/reporting/timeline-calculator.d.ts +7 -0
  112. package/dist/metrics/reporting/timeline-calculator.js +86 -0
  113. package/dist/metrics/types.d.ts +58 -0
  114. package/dist/outputs/csv.d.ts +2 -0
  115. package/dist/outputs/csv.js +21 -2
  116. package/dist/outputs/json.js +6 -2
  117. package/dist/protocols/rest/handler.d.ts +4 -53
  118. package/dist/protocols/rest/handler.js +73 -454
  119. package/dist/protocols/rest/request/auth-handler.d.ts +4 -0
  120. package/dist/protocols/rest/request/auth-handler.js +30 -0
  121. package/dist/protocols/rest/request/body-processor.d.ts +11 -0
  122. package/dist/protocols/rest/request/body-processor.js +62 -0
  123. package/dist/protocols/rest/request/index.d.ts +2 -0
  124. package/dist/protocols/rest/request/index.js +7 -0
  125. package/dist/protocols/rest/response/checks.d.ts +6 -0
  126. package/dist/protocols/rest/response/checks.js +71 -0
  127. package/dist/protocols/rest/response/index.d.ts +2 -0
  128. package/dist/protocols/rest/response/index.js +7 -0
  129. package/dist/protocols/rest/response/size-calculator.d.ts +12 -0
  130. package/dist/protocols/rest/response/size-calculator.js +64 -0
  131. package/dist/protocols/web/browser/highlight.d.ts +7 -0
  132. package/dist/protocols/web/browser/highlight.js +47 -0
  133. package/dist/protocols/web/browser/index.d.ts +4 -0
  134. package/dist/protocols/web/browser/index.js +11 -0
  135. package/dist/protocols/web/browser/manager.d.ts +20 -0
  136. package/dist/protocols/web/browser/manager.js +189 -0
  137. package/dist/protocols/web/browser/screenshot.d.ts +8 -0
  138. package/dist/protocols/web/browser/screenshot.js +69 -0
  139. package/dist/protocols/web/browser/storage.d.ts +5 -0
  140. package/dist/protocols/web/browser/storage.js +45 -0
  141. package/dist/protocols/web/commands/index.d.ts +5 -0
  142. package/dist/protocols/web/commands/index.js +11 -0
  143. package/dist/protocols/web/commands/interaction.d.ts +13 -0
  144. package/dist/protocols/web/commands/interaction.js +68 -0
  145. package/dist/protocols/web/commands/measurement.d.ts +16 -0
  146. package/dist/protocols/web/commands/measurement.js +33 -0
  147. package/dist/protocols/web/commands/navigation.d.ts +11 -0
  148. package/dist/protocols/web/commands/navigation.js +43 -0
  149. package/dist/protocols/web/commands/types.d.ts +12 -0
  150. package/dist/protocols/web/commands/types.js +2 -0
  151. package/dist/protocols/web/commands/verification.d.ts +11 -0
  152. package/dist/protocols/web/commands/verification.js +98 -0
  153. package/dist/protocols/web/handler.d.ts +19 -30
  154. package/dist/protocols/web/handler.js +160 -650
  155. package/dist/protocols/web/network/capture.d.ts +19 -0
  156. package/dist/protocols/web/network/capture.js +225 -0
  157. package/dist/protocols/web/network/filters.d.ts +5 -0
  158. package/dist/protocols/web/network/filters.js +49 -0
  159. package/dist/protocols/web/network/index.d.ts +4 -0
  160. package/dist/protocols/web/network/index.js +9 -0
  161. package/dist/protocols/web/network/types.d.ts +13 -0
  162. package/dist/protocols/web/network/types.js +2 -0
  163. package/dist/protocols/web/network/utils.d.ts +8 -0
  164. package/dist/protocols/web/network/utils.js +29 -0
  165. package/dist/reporting/chart-data/index.d.ts +5 -0
  166. package/dist/reporting/chart-data/index.js +13 -0
  167. package/dist/reporting/chart-data/network.d.ts +25 -0
  168. package/dist/reporting/chart-data/network.js +78 -0
  169. package/dist/reporting/chart-data/scenario.d.ts +37 -0
  170. package/dist/reporting/chart-data/scenario.js +76 -0
  171. package/dist/reporting/chart-data/step-statistics.d.ts +24 -0
  172. package/dist/reporting/chart-data/step-statistics.js +94 -0
  173. package/dist/reporting/chart-data/throughput.d.ts +16 -0
  174. package/dist/reporting/chart-data/throughput.js +24 -0
  175. package/dist/reporting/chart-data/timeline.d.ts +17 -0
  176. package/dist/reporting/chart-data/timeline.js +46 -0
  177. package/dist/reporting/handlebars-helpers.d.ts +1 -0
  178. package/dist/reporting/handlebars-helpers.js +63 -0
  179. package/dist/reporting/{enhanced-html-generator.d.ts → html-generator.d.ts} +1 -1
  180. package/dist/reporting/{enhanced-html-generator.js → html-generator.js} +10 -7
  181. package/dist/reporting/templates/{enhanced-report.hbs → report.hbs} +9 -9
  182. package/dist/utils/data-utils.d.ts +17 -0
  183. package/dist/utils/data-utils.js +129 -0
  184. package/dist/utils/template.js +2 -2
  185. package/package.json +5 -2
  186. package/dist/core/csv-data-provider.d.ts +0 -47
  187. package/dist/core/csv-data-provider.js +0 -265
  188. package/dist/reporting/generator.d.ts +0 -42
  189. package/dist/reporting/generator.js +0 -1217
  190. package/dist/reporting/templates/html.hbs +0 -2453
@@ -0,0 +1,7 @@
1
+ import { TestFile } from '../types';
2
+ export declare class FileScanner {
3
+ private testsDir;
4
+ constructor(testsDir: string);
5
+ scanTestFiles(): Promise<TestFile[]>;
6
+ private scanDirForTests;
7
+ }
@@ -0,0 +1,114 @@
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.FileScanner = void 0;
37
+ const fs = __importStar(require("fs/promises"));
38
+ const path = __importStar(require("path"));
39
+ class FileScanner {
40
+ constructor(testsDir) {
41
+ this.testsDir = testsDir;
42
+ }
43
+ async scanTestFiles() {
44
+ const tests = [];
45
+ // Only scan dedicated test directories
46
+ const searchDirs = [
47
+ path.join(this.testsDir, 'tests'),
48
+ path.join(this.testsDir, 'tmp/tests')
49
+ ];
50
+ for (const dir of searchDirs) {
51
+ try {
52
+ await this.scanDirForTests(dir, tests, this.testsDir);
53
+ }
54
+ catch (e) {
55
+ // Directory might not exist
56
+ }
57
+ }
58
+ return tests.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
59
+ }
60
+ async scanDirForTests(dir, tests, baseDir) {
61
+ try {
62
+ const entries = await fs.readdir(dir, { withFileTypes: true });
63
+ for (const entry of entries) {
64
+ const fullPath = path.join(dir, entry.name);
65
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules' &&
66
+ entry.name !== 'data' && entry.name !== 'config' && entry.name !== 'environments') {
67
+ await this.scanDirForTests(fullPath, tests, baseDir);
68
+ }
69
+ else if (entry.isFile() && (entry.name.endsWith('.yml') || entry.name.endsWith('.yaml') || entry.name.endsWith('.json'))) {
70
+ // Skip obvious non-test files
71
+ if (entry.name.includes('package') || entry.name.includes('tsconfig') ||
72
+ entry.name.includes('env') || entry.name === 'CNAME' ||
73
+ entry.name.includes('credentials') || entry.name.includes('config'))
74
+ continue;
75
+ try {
76
+ const content = await fs.readFile(fullPath, 'utf-8');
77
+ // Only include files that look like actual test configs (have scenarios or steps)
78
+ if (!content.includes('scenarios:') && !content.includes('steps:') &&
79
+ !content.includes('"scenarios"') && !content.includes('"steps"')) {
80
+ continue;
81
+ }
82
+ const stat = await fs.stat(fullPath);
83
+ const relativePath = path.relative(baseDir, fullPath);
84
+ // Detect test type from content or path (use forward slashes for cross-platform matching)
85
+ const normalizedPath = fullPath.replace(/\\/g, '/');
86
+ let testType = 'api';
87
+ if (content.includes('protocol: web') || content.includes('playwright') || normalizedPath.includes('/web/')) {
88
+ testType = 'web';
89
+ }
90
+ else if (content.includes('protocol: http') || content.includes('protocol: https') || normalizedPath.includes('/api/')) {
91
+ testType = 'api';
92
+ }
93
+ // Normalize paths to forward slashes for cross-platform compatibility
94
+ const normalizedRelativePath = relativePath.replace(/\\/g, '/');
95
+ tests.push({
96
+ name: entry.name.replace(/\.(yml|yaml|json)$/, ''),
97
+ path: fullPath, // Keep native path for filesystem operations
98
+ relativePath: normalizedRelativePath, // Use forward slashes for display/URLs
99
+ type: testType,
100
+ lastModified: stat.mtime.toISOString()
101
+ });
102
+ }
103
+ catch (e) {
104
+ // Skip unreadable files
105
+ }
106
+ }
107
+ }
108
+ }
109
+ catch (e) {
110
+ // Skip inaccessible directories
111
+ }
112
+ }
113
+ }
114
+ exports.FileScanner = FileScanner;
@@ -0,0 +1,5 @@
1
+ export { FileScanner } from './file-scanner';
2
+ export { ResultsManager } from './results-manager';
3
+ export { MetricsParser, MetricsParserOptions } from './metrics-parser';
4
+ export { TestExecutor, TestRunOptions, TestExecutorCallbacks, TestExecutorOptions } from './test-executor';
5
+ export { WorkersManager, WorkersInfo } from './workers-manager';
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WorkersManager = exports.TestExecutor = exports.MetricsParser = exports.ResultsManager = exports.FileScanner = void 0;
4
+ var file_scanner_1 = require("./file-scanner");
5
+ Object.defineProperty(exports, "FileScanner", { enumerable: true, get: function () { return file_scanner_1.FileScanner; } });
6
+ var results_manager_1 = require("./results-manager");
7
+ Object.defineProperty(exports, "ResultsManager", { enumerable: true, get: function () { return results_manager_1.ResultsManager; } });
8
+ var metrics_parser_1 = require("./metrics-parser");
9
+ Object.defineProperty(exports, "MetricsParser", { enumerable: true, get: function () { return metrics_parser_1.MetricsParser; } });
10
+ var test_executor_1 = require("./test-executor");
11
+ Object.defineProperty(exports, "TestExecutor", { enumerable: true, get: function () { return test_executor_1.TestExecutor; } });
12
+ var workers_manager_1 = require("./workers-manager");
13
+ Object.defineProperty(exports, "WorkersManager", { enumerable: true, get: function () { return workers_manager_1.WorkersManager; } });
@@ -0,0 +1,41 @@
1
+ import { InfrastructureMetrics } from '../types';
2
+ export interface InfluxDBConfig {
3
+ url: string;
4
+ token: string;
5
+ org: string;
6
+ bucket: string;
7
+ }
8
+ export interface InfraQueryOptions {
9
+ host?: string;
10
+ startTime?: Date;
11
+ endTime?: Date;
12
+ testId?: string;
13
+ limit?: number;
14
+ }
15
+ export declare class InfluxDBService {
16
+ private client;
17
+ private writeApi;
18
+ private queryApi;
19
+ private config;
20
+ private enabled;
21
+ private fallbackBuffer;
22
+ private readonly MAX_BUFFER_SIZE;
23
+ constructor(config?: Partial<InfluxDBConfig>);
24
+ connect(): Promise<boolean>;
25
+ isEnabled(): boolean;
26
+ writeMetrics(data: InfrastructureMetrics): Promise<void>;
27
+ queryMetrics(options?: InfraQueryOptions): Promise<InfrastructureMetrics[]>;
28
+ queryMetricsByTestRun(testId: string, startTime: Date, endTime: Date): Promise<Record<string, InfrastructureMetrics[]>>;
29
+ getHosts(): Promise<string[]>;
30
+ getLatestMetrics(): Promise<Record<string, InfrastructureMetrics | null>>;
31
+ exportMetrics(options: InfraQueryOptions, format?: 'json' | 'csv'): Promise<string>;
32
+ importMetrics(data: string, format?: 'json' | 'csv'): Promise<number>;
33
+ deleteMetrics(options: InfraQueryOptions): Promise<void>;
34
+ getSnapshot(): Record<string, InfrastructureMetrics[]>;
35
+ private addToFallbackBuffer;
36
+ private queryFallbackBuffer;
37
+ private rowsToMetrics;
38
+ private metricsToCSV;
39
+ private csvToMetrics;
40
+ close(): Promise<void>;
41
+ }
@@ -0,0 +1,329 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InfluxDBService = void 0;
4
+ const influxdb_client_1 = require("@influxdata/influxdb-client");
5
+ const logger_1 = require("../../utils/logger");
6
+ class InfluxDBService {
7
+ constructor(config) {
8
+ this.client = null;
9
+ this.writeApi = null;
10
+ this.queryApi = null;
11
+ this.enabled = false;
12
+ this.fallbackBuffer = new Map();
13
+ this.MAX_BUFFER_SIZE = 120;
14
+ this.config = {
15
+ url: config?.url || process.env.INFLUXDB_URL || 'http://localhost:8086',
16
+ token: config?.token || process.env.INFLUXDB_TOKEN || '',
17
+ org: config?.org || process.env.INFLUXDB_ORG || 'perfornium',
18
+ bucket: config?.bucket || process.env.INFLUXDB_BUCKET || 'metrics'
19
+ };
20
+ }
21
+ async connect() {
22
+ if (!this.config.token) {
23
+ logger_1.logger.info('InfluxDB token not configured, using in-memory storage');
24
+ return false;
25
+ }
26
+ try {
27
+ this.client = new influxdb_client_1.InfluxDB({
28
+ url: this.config.url,
29
+ token: this.config.token
30
+ });
31
+ this.writeApi = this.client.getWriteApi(this.config.org, this.config.bucket, 'ms');
32
+ this.queryApi = this.client.getQueryApi(this.config.org);
33
+ // Test connection with a simple query
34
+ const query = `from(bucket: "${this.config.bucket}") |> range(start: -1s) |> limit(n: 1)`;
35
+ await this.queryApi.collectRows(query);
36
+ this.enabled = true;
37
+ logger_1.logger.info(`Connected to InfluxDB at ${this.config.url}`);
38
+ return true;
39
+ }
40
+ catch (error) {
41
+ logger_1.logger.warn(`Failed to connect to InfluxDB: ${error.message}. Using in-memory storage.`);
42
+ this.enabled = false;
43
+ return false;
44
+ }
45
+ }
46
+ isEnabled() {
47
+ return this.enabled;
48
+ }
49
+ async writeMetrics(data) {
50
+ // Always store in fallback buffer for real-time display
51
+ this.addToFallbackBuffer(data);
52
+ if (!this.enabled || !this.writeApi) {
53
+ return;
54
+ }
55
+ try {
56
+ const point = new influxdb_client_1.Point('infrastructure_metrics')
57
+ .tag('host', data.host)
58
+ .floatField('cpu_usage', data.metrics?.cpu?.usage_percent || 0)
59
+ .floatField('memory_usage', data.metrics?.memory?.usage_percent || 0)
60
+ .floatField('memory_used_mb', data.metrics?.memory?.used_mb || 0)
61
+ .floatField('memory_total_mb', data.metrics?.memory?.total_mb || 0)
62
+ .floatField('disk_usage', data.metrics?.disk?.usage_percent || 0)
63
+ .floatField('disk_used_gb', data.metrics?.disk?.used_gb || 0)
64
+ .floatField('disk_total_gb', data.metrics?.disk?.total_gb || 0)
65
+ .stringField('disk_path', data.metrics?.disk?.path || '/')
66
+ .floatField('network_bytes_in', data.metrics?.network?.bytes_in || 0)
67
+ .floatField('network_bytes_out', data.metrics?.network?.bytes_out || 0)
68
+ .stringField('network_interface', data.metrics?.network?.interface || 'eth0')
69
+ .intField('interval_seconds', data.interval_seconds || 5)
70
+ .timestamp(new Date(data.timestamp));
71
+ this.writeApi.writePoint(point);
72
+ await this.writeApi.flush();
73
+ }
74
+ catch (error) {
75
+ logger_1.logger.error(`Failed to write to InfluxDB: ${error.message}`);
76
+ }
77
+ }
78
+ async queryMetrics(options = {}) {
79
+ if (!this.enabled || !this.queryApi) {
80
+ return this.queryFallbackBuffer(options);
81
+ }
82
+ try {
83
+ const startTime = options.startTime || new Date(Date.now() - 10 * 60 * 1000); // Default 10 minutes
84
+ const endTime = options.endTime || new Date();
85
+ // Validate time range - start must be before end
86
+ if (startTime.getTime() >= endTime.getTime()) {
87
+ logger_1.logger.debug('Invalid time range for InfluxDB query, using fallback buffer');
88
+ return this.queryFallbackBuffer(options);
89
+ }
90
+ let query = `from(bucket: "${this.config.bucket}")
91
+ |> range(start: ${startTime.toISOString()}, stop: ${endTime.toISOString()})
92
+ |> filter(fn: (r) => r._measurement == "infrastructure_metrics")`;
93
+ if (options.host) {
94
+ query += `\n |> filter(fn: (r) => r.host == "${options.host}")`;
95
+ }
96
+ query += `\n |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")`;
97
+ if (options.limit) {
98
+ query += `\n |> limit(n: ${options.limit})`;
99
+ }
100
+ const rows = await this.queryApi.collectRows(query);
101
+ return this.rowsToMetrics(rows);
102
+ }
103
+ catch (error) {
104
+ logger_1.logger.error(`Failed to query InfluxDB: ${error.message}`);
105
+ return this.queryFallbackBuffer(options);
106
+ }
107
+ }
108
+ async queryMetricsByTestRun(testId, startTime, endTime) {
109
+ const metrics = await this.queryMetrics({ startTime, endTime });
110
+ // Group by host
111
+ const grouped = {};
112
+ for (const metric of metrics) {
113
+ if (!grouped[metric.host]) {
114
+ grouped[metric.host] = [];
115
+ }
116
+ grouped[metric.host].push(metric);
117
+ }
118
+ return grouped;
119
+ }
120
+ async getHosts() {
121
+ if (!this.enabled || !this.queryApi) {
122
+ return Array.from(this.fallbackBuffer.keys());
123
+ }
124
+ try {
125
+ const query = `from(bucket: "${this.config.bucket}")
126
+ |> range(start: -24h)
127
+ |> filter(fn: (r) => r._measurement == "infrastructure_metrics")
128
+ |> keep(columns: ["host"])
129
+ |> distinct(column: "host")`;
130
+ const rows = await this.queryApi.collectRows(query);
131
+ return rows.map((row) => row.host).filter(Boolean);
132
+ }
133
+ catch (error) {
134
+ logger_1.logger.error(`Failed to get hosts from InfluxDB: ${error.message}`);
135
+ return Array.from(this.fallbackBuffer.keys());
136
+ }
137
+ }
138
+ async getLatestMetrics() {
139
+ const hosts = await this.getHosts();
140
+ const result = {};
141
+ for (const host of hosts) {
142
+ const metrics = await this.queryMetrics({ host, limit: 1 });
143
+ result[host] = metrics.length > 0 ? metrics[metrics.length - 1] : null;
144
+ }
145
+ // Include fallback buffer hosts
146
+ for (const [host, buffer] of this.fallbackBuffer) {
147
+ if (!result[host] && buffer.length > 0) {
148
+ result[host] = buffer[buffer.length - 1];
149
+ }
150
+ }
151
+ return result;
152
+ }
153
+ async exportMetrics(options, format = 'json') {
154
+ const metrics = await this.queryMetrics(options);
155
+ if (format === 'csv') {
156
+ return this.metricsToCSV(metrics);
157
+ }
158
+ return JSON.stringify(metrics, null, 2);
159
+ }
160
+ async importMetrics(data, format = 'json') {
161
+ let metrics;
162
+ if (format === 'csv') {
163
+ metrics = this.csvToMetrics(data);
164
+ }
165
+ else {
166
+ metrics = JSON.parse(data);
167
+ }
168
+ let count = 0;
169
+ for (const metric of metrics) {
170
+ await this.writeMetrics(metric);
171
+ count++;
172
+ }
173
+ return count;
174
+ }
175
+ async deleteMetrics(options) {
176
+ if (!this.enabled || !this.client) {
177
+ // Clear fallback buffer for host if specified
178
+ if (options.host) {
179
+ this.fallbackBuffer.delete(options.host);
180
+ }
181
+ else {
182
+ this.fallbackBuffer.clear();
183
+ }
184
+ return;
185
+ }
186
+ // Note: InfluxDB 2.x delete requires Delete API
187
+ // For simplicity, we'll just let data expire via retention policy
188
+ logger_1.logger.warn('Delete operation not fully implemented for InfluxDB. Consider using retention policies.');
189
+ }
190
+ getSnapshot() {
191
+ const snapshot = {};
192
+ for (const [host, metrics] of this.fallbackBuffer) {
193
+ snapshot[host] = [...metrics];
194
+ }
195
+ return snapshot;
196
+ }
197
+ addToFallbackBuffer(data) {
198
+ if (!this.fallbackBuffer.has(data.host)) {
199
+ this.fallbackBuffer.set(data.host, []);
200
+ }
201
+ const buffer = this.fallbackBuffer.get(data.host);
202
+ buffer.push(data);
203
+ if (buffer.length > this.MAX_BUFFER_SIZE) {
204
+ buffer.shift();
205
+ }
206
+ }
207
+ queryFallbackBuffer(options) {
208
+ let result = [];
209
+ if (options.host) {
210
+ result = this.fallbackBuffer.get(options.host) || [];
211
+ }
212
+ else {
213
+ for (const buffer of this.fallbackBuffer.values()) {
214
+ result = result.concat(buffer);
215
+ }
216
+ }
217
+ // Filter by time range
218
+ if (options.startTime || options.endTime) {
219
+ result = result.filter(m => {
220
+ const ts = new Date(m.timestamp).getTime();
221
+ if (options.startTime && ts < options.startTime.getTime())
222
+ return false;
223
+ if (options.endTime && ts > options.endTime.getTime())
224
+ return false;
225
+ return true;
226
+ });
227
+ }
228
+ // Sort by timestamp
229
+ result.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
230
+ if (options.limit) {
231
+ result = result.slice(-options.limit);
232
+ }
233
+ return result;
234
+ }
235
+ rowsToMetrics(rows) {
236
+ return rows.map(row => ({
237
+ host: row.host,
238
+ timestamp: row._time,
239
+ interval_seconds: row.interval_seconds || 5,
240
+ metrics: {
241
+ cpu: { usage_percent: row.cpu_usage || 0 },
242
+ memory: {
243
+ usage_percent: row.memory_usage || 0,
244
+ used_mb: row.memory_used_mb || 0,
245
+ total_mb: row.memory_total_mb || 0
246
+ },
247
+ disk: {
248
+ usage_percent: row.disk_usage || 0,
249
+ used_gb: row.disk_used_gb || 0,
250
+ total_gb: row.disk_total_gb || 0,
251
+ path: row.disk_path || '/'
252
+ },
253
+ network: {
254
+ bytes_in: row.network_bytes_in || 0,
255
+ bytes_out: row.network_bytes_out || 0,
256
+ interface: row.network_interface || 'eth0'
257
+ }
258
+ }
259
+ }));
260
+ }
261
+ metricsToCSV(metrics) {
262
+ const headers = [
263
+ 'timestamp', 'host', 'interval_seconds',
264
+ 'cpu_usage', 'memory_usage', 'memory_used_mb', 'memory_total_mb',
265
+ 'disk_usage', 'disk_used_gb', 'disk_total_gb', 'disk_path',
266
+ 'network_bytes_in', 'network_bytes_out', 'network_interface'
267
+ ];
268
+ const rows = metrics.map(m => [
269
+ m.timestamp,
270
+ m.host,
271
+ m.interval_seconds || 5,
272
+ m.metrics?.cpu?.usage_percent || 0,
273
+ m.metrics?.memory?.usage_percent || 0,
274
+ m.metrics?.memory?.used_mb || 0,
275
+ m.metrics?.memory?.total_mb || 0,
276
+ m.metrics?.disk?.usage_percent || 0,
277
+ m.metrics?.disk?.used_gb || 0,
278
+ m.metrics?.disk?.total_gb || 0,
279
+ m.metrics?.disk?.path || '/',
280
+ m.metrics?.network?.bytes_in || 0,
281
+ m.metrics?.network?.bytes_out || 0,
282
+ m.metrics?.network?.interface || 'eth0'
283
+ ].join(','));
284
+ return [headers.join(','), ...rows].join('\n');
285
+ }
286
+ csvToMetrics(csv) {
287
+ const lines = csv.trim().split('\n');
288
+ if (lines.length < 2)
289
+ return [];
290
+ const headers = lines[0].split(',');
291
+ const metrics = [];
292
+ for (let i = 1; i < lines.length; i++) {
293
+ const values = lines[i].split(',');
294
+ const row = {};
295
+ headers.forEach((h, idx) => row[h] = values[idx]);
296
+ metrics.push({
297
+ timestamp: row.timestamp,
298
+ host: row.host,
299
+ interval_seconds: parseInt(row.interval_seconds) || 5,
300
+ metrics: {
301
+ cpu: { usage_percent: parseFloat(row.cpu_usage) || 0 },
302
+ memory: {
303
+ usage_percent: parseFloat(row.memory_usage) || 0,
304
+ used_mb: parseFloat(row.memory_used_mb) || 0,
305
+ total_mb: parseFloat(row.memory_total_mb) || 0
306
+ },
307
+ disk: {
308
+ usage_percent: parseFloat(row.disk_usage) || 0,
309
+ used_gb: parseFloat(row.disk_used_gb) || 0,
310
+ total_gb: parseFloat(row.disk_total_gb) || 0,
311
+ path: row.disk_path || '/'
312
+ },
313
+ network: {
314
+ bytes_in: parseFloat(row.network_bytes_in) || 0,
315
+ bytes_out: parseFloat(row.network_bytes_out) || 0,
316
+ interface: row.network_interface || 'eth0'
317
+ }
318
+ }
319
+ });
320
+ }
321
+ return metrics;
322
+ }
323
+ async close() {
324
+ if (this.writeApi) {
325
+ await this.writeApi.close();
326
+ }
327
+ }
328
+ }
329
+ exports.InfluxDBService = InfluxDBService;
@@ -0,0 +1,12 @@
1
+ import { LiveTest } from '../types';
2
+ export interface MetricsParserOptions {
3
+ /** When InfluxDB is enabled, don't limit response times (data is persisted) */
4
+ influxEnabled?: boolean;
5
+ }
6
+ export declare class MetricsParser {
7
+ private influxEnabled;
8
+ constructor(options?: MetricsParserOptions);
9
+ parseOutputForMetrics(line: string, test: LiveTest): boolean;
10
+ private addHistoryEntry;
11
+ parseNetworkData(line: string): any | null;
12
+ }