@atom8n/n8n-benchmark 2.0.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.
Files changed (158) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/Dockerfile +63 -0
  3. package/README.md +122 -0
  4. package/bin/n8n-benchmark +13 -0
  5. package/biome.jsonc +7 -0
  6. package/dist/build.tsbuildinfo +1 -0
  7. package/dist/commands/list.d.ts +8 -0
  8. package/dist/commands/list.js +23 -0
  9. package/dist/commands/list.js.map +1 -0
  10. package/dist/commands/run.d.ts +24 -0
  11. package/dist/commands/run.js +128 -0
  12. package/dist/commands/run.js.map +1 -0
  13. package/dist/config/common-flags.d.ts +1 -0
  14. package/dist/config/common-flags.js +9 -0
  15. package/dist/config/common-flags.js.map +1 -0
  16. package/dist/n8n-api-client/authenticated-n8n-api-client.d.ts +15 -0
  17. package/dist/n8n-api-client/authenticated-n8n-api-client.js +67 -0
  18. package/dist/n8n-api-client/authenticated-n8n-api-client.js.map +1 -0
  19. package/dist/n8n-api-client/credentials-api-client.d.ts +9 -0
  20. package/dist/n8n-api-client/credentials-api-client.js +24 -0
  21. package/dist/n8n-api-client/credentials-api-client.js.map +1 -0
  22. package/dist/n8n-api-client/data-table-api-client.d.ts +9 -0
  23. package/dist/n8n-api-client/data-table-api-client.js +23 -0
  24. package/dist/n8n-api-client/data-table-api-client.js.map +1 -0
  25. package/dist/n8n-api-client/n8n-api-client.d.ts +13 -0
  26. package/dist/n8n-api-client/n8n-api-client.js +82 -0
  27. package/dist/n8n-api-client/n8n-api-client.js.map +1 -0
  28. package/dist/n8n-api-client/n8n-api-client.types.d.ts +21 -0
  29. package/dist/n8n-api-client/n8n-api-client.types.js +3 -0
  30. package/dist/n8n-api-client/n8n-api-client.types.js.map +1 -0
  31. package/dist/n8n-api-client/project-api-client.d.ts +6 -0
  32. package/dist/n8n-api-client/project-api-client.js +14 -0
  33. package/dist/n8n-api-client/project-api-client.js.map +1 -0
  34. package/dist/n8n-api-client/workflows-api-client.d.ts +11 -0
  35. package/dist/n8n-api-client/workflows-api-client.js +30 -0
  36. package/dist/n8n-api-client/workflows-api-client.js.map +1 -0
  37. package/dist/scenario/scenario-data-loader.d.ts +13 -0
  38. package/dist/scenario/scenario-data-loader.js +84 -0
  39. package/dist/scenario/scenario-data-loader.js.map +1 -0
  40. package/dist/scenario/scenario-loader.d.ts +7 -0
  41. package/dist/scenario/scenario-loader.js +101 -0
  42. package/dist/scenario/scenario-loader.js.map +1 -0
  43. package/dist/test-execution/app-metrics-poller.d.ts +13 -0
  44. package/dist/test-execution/app-metrics-poller.js +54 -0
  45. package/dist/test-execution/app-metrics-poller.js.map +1 -0
  46. package/dist/test-execution/k6-executor.d.ts +33 -0
  47. package/dist/test-execution/k6-executor.js +120 -0
  48. package/dist/test-execution/k6-executor.js.map +1 -0
  49. package/dist/test-execution/k6-summary.d.ts +82 -0
  50. package/dist/test-execution/k6-summary.js +2 -0
  51. package/dist/test-execution/k6-summary.js.map +1 -0
  52. package/dist/test-execution/prometheus-metrics-parser.d.ts +9 -0
  53. package/dist/test-execution/prometheus-metrics-parser.js +44 -0
  54. package/dist/test-execution/prometheus-metrics-parser.js.map +1 -0
  55. package/dist/test-execution/scenario-data-importer.d.ts +21 -0
  56. package/dist/test-execution/scenario-data-importer.js +108 -0
  57. package/dist/test-execution/scenario-data-importer.js.map +1 -0
  58. package/dist/test-execution/scenario-runner.d.ts +18 -0
  59. package/dist/test-execution/scenario-runner.js +46 -0
  60. package/dist/test-execution/scenario-runner.js.map +1 -0
  61. package/dist/test-execution/test-report.d.ts +56 -0
  62. package/dist/test-execution/test-report.js +65 -0
  63. package/dist/test-execution/test-report.js.map +1 -0
  64. package/dist/types/scenario.d.ts +16 -0
  65. package/dist/types/scenario.js +3 -0
  66. package/dist/types/scenario.js.map +1 -0
  67. package/eslint.config.mjs +22 -0
  68. package/infra/.terraform.lock.hcl +60 -0
  69. package/infra/benchmark-env.tf +54 -0
  70. package/infra/modules/benchmark-vm/output.tf +11 -0
  71. package/infra/modules/benchmark-vm/vars.tf +29 -0
  72. package/infra/modules/benchmark-vm/vm.tf +126 -0
  73. package/infra/output.tf +16 -0
  74. package/infra/providers.tf +23 -0
  75. package/infra/vars.tf +34 -0
  76. package/package.json +55 -0
  77. package/scenarios/binary-data/binary-data.json +67 -0
  78. package/scenarios/binary-data/binary-data.manifest.json +7 -0
  79. package/scenarios/binary-data/binary-data.script.js +29 -0
  80. package/scenarios/credential-http-node/credential-bearer.json +8 -0
  81. package/scenarios/credential-http-node/credential-http-node.json +241 -0
  82. package/scenarios/credential-http-node/credential-http-node.manifest.json +10 -0
  83. package/scenarios/credential-http-node/credential-http-node.script.js +30 -0
  84. package/scenarios/data-table-node/data-table-node.json +168 -0
  85. package/scenarios/data-table-node/data-table-node.manifest.json +10 -0
  86. package/scenarios/data-table-node/data-table-node.script.js +38 -0
  87. package/scenarios/data-table-node/data-table.json +25 -0
  88. package/scenarios/http-node/http-node.json +213 -0
  89. package/scenarios/http-node/http-node.manifest.json +7 -0
  90. package/scenarios/http-node/http-node.script.js +30 -0
  91. package/scenarios/js-code-node/js-code-node.json +96 -0
  92. package/scenarios/js-code-node/js-code-node.manifest.json +7 -0
  93. package/scenarios/js-code-node/js-code-node.script.js +29 -0
  94. package/scenarios/multiple-webhooks/multiple-webhooks.manifest.json +20 -0
  95. package/scenarios/multiple-webhooks/multiple-webhooks.script.js +19 -0
  96. package/scenarios/multiple-webhooks/multiple-webhooks1.json +25 -0
  97. package/scenarios/multiple-webhooks/multiple-webhooks10.json +25 -0
  98. package/scenarios/multiple-webhooks/multiple-webhooks2.json +25 -0
  99. package/scenarios/multiple-webhooks/multiple-webhooks3.json +25 -0
  100. package/scenarios/multiple-webhooks/multiple-webhooks4.json +25 -0
  101. package/scenarios/multiple-webhooks/multiple-webhooks5.json +25 -0
  102. package/scenarios/multiple-webhooks/multiple-webhooks6.json +25 -0
  103. package/scenarios/multiple-webhooks/multiple-webhooks7.json +25 -0
  104. package/scenarios/multiple-webhooks/multiple-webhooks8.json +25 -0
  105. package/scenarios/multiple-webhooks/multiple-webhooks9.json +25 -0
  106. package/scenarios/py-code-node/py-code-node.json +98 -0
  107. package/scenarios/py-code-node/py-code-node.manifest.json +7 -0
  108. package/scenarios/py-code-node/py-code-node.script.js +29 -0
  109. package/scenarios/scenario.schema.json +51 -0
  110. package/scenarios/set-node-expressions/set-node-expressions.json +91 -0
  111. package/scenarios/set-node-expressions/set-node-expressions.manifest.json +7 -0
  112. package/scenarios/set-node-expressions/set-node-expressions.script.js +18 -0
  113. package/scenarios/single-webhook/single-webhook.json +25 -0
  114. package/scenarios/single-webhook/single-webhook.manifest.json +7 -0
  115. package/scenarios/single-webhook/single-webhook.script.js +18 -0
  116. package/scripts/bootstrap.sh +63 -0
  117. package/scripts/clients/docker-compose-client.mjs +45 -0
  118. package/scripts/clients/ssh-client.mjs +37 -0
  119. package/scripts/clients/terraform-client.mjs +71 -0
  120. package/scripts/destroy-cloud-env.mjs +86 -0
  121. package/scripts/mock-api/mappings/mockApiData.json +92110 -0
  122. package/scripts/n8n-setups/postgres/docker-compose.yml +76 -0
  123. package/scripts/n8n-setups/postgres/setup.mjs +15 -0
  124. package/scripts/n8n-setups/scaling-multi-main/docker-compose.yml +230 -0
  125. package/scripts/n8n-setups/scaling-multi-main/nginx.conf +24 -0
  126. package/scripts/n8n-setups/scaling-multi-main/setup.mjs +15 -0
  127. package/scripts/n8n-setups/scaling-single-main/docker-compose.yml +174 -0
  128. package/scripts/n8n-setups/scaling-single-main/setup.mjs +15 -0
  129. package/scripts/n8n-setups/sqlite/docker-compose.yml +55 -0
  130. package/scripts/n8n-setups/sqlite/setup.mjs +15 -0
  131. package/scripts/provision-cloud-env.mjs +36 -0
  132. package/scripts/run-for-n8n-setup.mjs +175 -0
  133. package/scripts/run-in-cloud.mjs +167 -0
  134. package/scripts/run-locally.mjs +73 -0
  135. package/scripts/run.mjs +192 -0
  136. package/scripts/utils/flags.mjs +20 -0
  137. package/src/commands/list.ts +26 -0
  138. package/src/commands/run.ts +140 -0
  139. package/src/config/common-flags.ts +6 -0
  140. package/src/n8n-api-client/authenticated-n8n-api-client.ts +88 -0
  141. package/src/n8n-api-client/credentials-api-client.ts +28 -0
  142. package/src/n8n-api-client/data-table-api-client.ts +30 -0
  143. package/src/n8n-api-client/n8n-api-client.ts +85 -0
  144. package/src/n8n-api-client/n8n-api-client.types.ts +27 -0
  145. package/src/n8n-api-client/project-api-client.ts +11 -0
  146. package/src/n8n-api-client/workflows-api-client.ts +38 -0
  147. package/src/scenario/scenario-data-loader.ts +75 -0
  148. package/src/scenario/scenario-loader.ts +90 -0
  149. package/src/test-execution/app-metrics-poller.ts +81 -0
  150. package/src/test-execution/k6-executor.ts +192 -0
  151. package/src/test-execution/k6-summary.ts +255 -0
  152. package/src/test-execution/prometheus-metrics-parser.ts +63 -0
  153. package/src/test-execution/scenario-data-importer.ts +165 -0
  154. package/src/test-execution/scenario-runner.ts +76 -0
  155. package/src/test-execution/test-report.ts +152 -0
  156. package/src/types/scenario.ts +33 -0
  157. package/tsconfig.build.json +9 -0
  158. package/tsconfig.json +14 -0
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Polls the /metrics endpoint from an n8n instance to collect application metrics
3
+ * during benchmark test runs.
4
+ *
5
+ * A Poller can be started and stopped once. After it's stopped, it cannot be restarted.
6
+ * Instead, a new poller instance should be created.
7
+ */
8
+ export class AppMetricsPoller {
9
+ private intervalId: NodeJS.Timeout | undefined = undefined;
10
+ private metricsData: string[] = [];
11
+ private isRunning = false;
12
+ private isStopped = false;
13
+
14
+ constructor(
15
+ private readonly metricsUrl: string,
16
+ private readonly pollIntervalMs: number = 5000,
17
+ ) {}
18
+
19
+ /**
20
+ * Starts polling the metrics endpoint
21
+ */
22
+ start() {
23
+ if (this.isRunning) {
24
+ throw new Error('Metrics poller is already running');
25
+ }
26
+ if (this.isStopped) {
27
+ throw new Error('Metrics poller has been stopped and cannot be restarted');
28
+ }
29
+
30
+ this.isRunning = true;
31
+ this.metricsData = [];
32
+
33
+ // Immediately poll once to get initial metrics
34
+ void this.pollMetrics();
35
+
36
+ // Set up interval polling
37
+ this.intervalId = setInterval(() => {
38
+ void this.pollMetrics();
39
+ }, this.pollIntervalMs);
40
+ }
41
+
42
+ /**
43
+ * Stops polling the metrics endpoint
44
+ */
45
+ stop() {
46
+ if (this.intervalId) {
47
+ clearInterval(this.intervalId);
48
+ this.intervalId = undefined;
49
+ }
50
+ this.isRunning = false;
51
+ this.isStopped = true;
52
+ }
53
+
54
+ /**
55
+ * Gets all collected metrics data
56
+ */
57
+ getMetricsData(): string[] {
58
+ return this.metricsData;
59
+ }
60
+
61
+ /**
62
+ * Polls the metrics endpoint once
63
+ */
64
+ private async pollMetrics() {
65
+ try {
66
+ const response = await fetch(this.metricsUrl);
67
+
68
+ if (!response.ok) {
69
+ console.warn(`Failed to poll metrics: ${response.status} ${response.statusText}`);
70
+ return;
71
+ }
72
+
73
+ const metricsText = await response.text();
74
+ this.metricsData.push(metricsText);
75
+ } catch (error) {
76
+ console.warn(
77
+ `Error polling metrics: ${error instanceof Error ? error.message : String(error)}`,
78
+ );
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,192 @@
1
+ import fs from 'fs';
2
+ import assert from 'node:assert/strict';
3
+ import path from 'path';
4
+ import { $, which, tmpfile } from 'zx';
5
+
6
+ import { AppMetricsPoller } from '@/test-execution/app-metrics-poller';
7
+ import { buildTestReport, type K6Tag } from '@/test-execution/test-report';
8
+ import type { Scenario } from '@/types/scenario';
9
+ export type { K6Tag };
10
+
11
+ export type K6ExecutorOpts = {
12
+ k6ExecutablePath: string;
13
+ /** How many concurrent requests to make */
14
+ vus: number;
15
+ /** Test duration, e.g. 1m or 30s */
16
+ duration: string;
17
+ k6Out?: string;
18
+ k6ApiToken?: string;
19
+ n8nApiBaseUrl: string;
20
+ tags?: K6Tag[];
21
+ resultsWebhook?: {
22
+ url: string;
23
+ authHeader: string;
24
+ };
25
+ /** Configuration for polling app metrics during test runs */
26
+ appMetricsPolling?: {
27
+ enabled: boolean;
28
+ /** Poll interval in milliseconds. Defaults to 5000ms (5 seconds) */
29
+ intervalMs?: number;
30
+ };
31
+ };
32
+
33
+ export type K6RunOpts = {
34
+ /** Name of the scenario run. Used e.g. when the run is reported to k6 cloud */
35
+ scenarioRunName: string;
36
+ };
37
+
38
+ /**
39
+ * Flag for the k6 CLI.
40
+ * @example ['--duration', '1m']
41
+ * @example ['--quiet']
42
+ */
43
+ type K6CliFlag = [string | number] | [string, string | number];
44
+
45
+ /**
46
+ * Executes test scenarios using k6
47
+ */
48
+ export class K6Executor {
49
+ /**
50
+ * This script is dynamically injected into the k6 test script to generate
51
+ * a summary report of the test execution.
52
+ */
53
+ private readonly handleSummaryScript = `
54
+ import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.2/index.js';
55
+ export function handleSummary(data) {
56
+ return {
57
+ stdout: textSummary(data),
58
+ '{{scenarioName}}.summary.json': JSON.stringify(data),
59
+ };
60
+ }
61
+ `;
62
+
63
+ constructor(private readonly opts: K6ExecutorOpts) {}
64
+
65
+ async executeTestScenario(scenario: Scenario, { scenarioRunName }: K6RunOpts) {
66
+ const augmentedTestScriptPath = this.augmentSummaryScript(scenario, scenarioRunName);
67
+ const runDirPath = path.dirname(augmentedTestScriptPath);
68
+
69
+ const flags: K6CliFlag[] = [
70
+ ['--quiet'],
71
+ ['--duration', this.opts.duration],
72
+ ['--vus', this.opts.vus],
73
+ ];
74
+
75
+ if (this.opts.k6Out) {
76
+ flags.push(['--out', this.opts.k6Out]);
77
+ } else if (!this.opts.resultsWebhook && this.opts.k6ApiToken) {
78
+ flags.push(['--out', 'cloud']);
79
+ }
80
+
81
+ const flattedFlags = flags.flat(2);
82
+
83
+ const k6ExecutablePath = await this.resolveK6ExecutablePath();
84
+
85
+ // Start app metrics polling if enabled
86
+ let metricsPoller: AppMetricsPoller | undefined;
87
+ if (this.opts.appMetricsPolling?.enabled) {
88
+ const metricsUrl = `${this.opts.n8nApiBaseUrl}/metrics`;
89
+ const intervalMs = this.opts.appMetricsPolling.intervalMs ?? 5000;
90
+ metricsPoller = new AppMetricsPoller(metricsUrl, intervalMs);
91
+ metricsPoller.start();
92
+ console.log(`Started polling app metrics from ${metricsUrl} every ${intervalMs}ms`);
93
+ }
94
+
95
+ try {
96
+ await $({
97
+ cwd: runDirPath,
98
+ env: {
99
+ API_BASE_URL: this.opts.n8nApiBaseUrl,
100
+ DATA_TABLE_ID: scenario.dataTableId,
101
+ K6_CLOUD_TOKEN: this.opts.k6ApiToken,
102
+ },
103
+ stdio: 'inherit',
104
+ })`${k6ExecutablePath} run ${flattedFlags} ${augmentedTestScriptPath}`;
105
+ } finally {
106
+ // Stop metrics polling regardless of test outcome
107
+ if (metricsPoller) {
108
+ metricsPoller.stop();
109
+ console.log('Stopped polling app metrics');
110
+ }
111
+ }
112
+
113
+ console.log('\n');
114
+
115
+ if (this.opts.resultsWebhook) {
116
+ const endOfTestSummary = this.loadEndOfTestSummary(runDirPath, scenarioRunName);
117
+ const appMetricsData = metricsPoller?.getMetricsData();
118
+
119
+ const testReport = buildTestReport(
120
+ scenario,
121
+ endOfTestSummary,
122
+ [
123
+ ...(this.opts.tags ?? []),
124
+ { name: 'Vus', value: this.opts.vus.toString() },
125
+ { name: 'Duration', value: this.opts.duration.toString() },
126
+ ],
127
+ appMetricsData,
128
+ );
129
+
130
+ await this.sendTestReport(testReport);
131
+ }
132
+ }
133
+
134
+ async sendTestReport(testReport: unknown) {
135
+ assert(this.opts.resultsWebhook);
136
+
137
+ const response = await fetch(this.opts.resultsWebhook.url, {
138
+ method: 'POST',
139
+ body: JSON.stringify(testReport),
140
+ headers: {
141
+ Authorization: this.opts.resultsWebhook.authHeader,
142
+ 'Content-Type': 'application/json',
143
+ },
144
+ });
145
+
146
+ if (!response.ok) {
147
+ console.warn(`Failed to send test summary: ${response.status} ${await response.text()}`);
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Augments the test script with a summary script
153
+ *
154
+ * @returns Absolute path to the augmented test script
155
+ */
156
+ private augmentSummaryScript(scenario: Scenario, scenarioRunName: string) {
157
+ const fullTestScriptPath = path.join(scenario.scenarioDirPath, scenario.scriptPath);
158
+ const testScript = fs.readFileSync(fullTestScriptPath, 'utf8');
159
+ const summaryScript = this.handleSummaryScript.replace('{{scenarioName}}', scenarioRunName);
160
+
161
+ const augmentedTestScript = `${testScript}\n\n${summaryScript}`;
162
+
163
+ const tempFilePath = tmpfile(`${scenarioRunName}.js`, augmentedTestScript);
164
+
165
+ return tempFilePath;
166
+ }
167
+
168
+ private loadEndOfTestSummary(dir: string, scenarioRunName: string): K6EndOfTestSummary {
169
+ const summaryReportPath = path.join(dir, `${scenarioRunName}.summary.json`);
170
+ const summaryReport = fs.readFileSync(summaryReportPath, 'utf8');
171
+
172
+ try {
173
+ return JSON.parse(summaryReport) as K6EndOfTestSummary;
174
+ } catch (error) {
175
+ throw new Error(`Failed to parse the summary report at ${summaryReportPath}`);
176
+ }
177
+ }
178
+
179
+ /**
180
+ * @returns Resolved path to the k6 executable
181
+ */
182
+ private async resolveK6ExecutablePath(): Promise<string> {
183
+ const k6ExecutablePath = await which(this.opts.k6ExecutablePath, { nothrow: true });
184
+ if (!k6ExecutablePath) {
185
+ throw new Error(
186
+ 'Could not find k6 executable based on your `PATH`. Please ensure k6 is available in your system and add it to your `PATH` or specify the path to the k6 executable using the `K6_PATH` environment variable.',
187
+ );
188
+ }
189
+
190
+ return k6ExecutablePath;
191
+ }
192
+ }
@@ -0,0 +1,255 @@
1
+ /**
2
+ Example JSON:
3
+
4
+ {
5
+ "options": {
6
+ "summaryTrendStats": ["avg", "min", "med", "max", "p(90)", "p(95)"],
7
+ "summaryTimeUnit": "",
8
+ "noColor": false
9
+ },
10
+ "state": { "isStdOutTTY": false, "isStdErrTTY": false, "testRunDurationMs": 23.374 },
11
+ "metrics": {
12
+ "http_req_tls_handshaking": {
13
+ "type": "trend",
14
+ "contains": "time",
15
+ "values": { "avg": 0, "min": 0, "med": 0, "max": 0, "p(90)": 0, "p(95)": 0 }
16
+ },
17
+ "checks": {
18
+ "type": "rate",
19
+ "contains": "default",
20
+ "values": { "rate": 1, "passes": 1, "fails": 0 }
21
+ },
22
+ "http_req_sending": {
23
+ "type": "trend",
24
+ "contains": "time",
25
+ "values": {
26
+ "p(90)": 0.512,
27
+ "p(95)": 0.512,
28
+ "avg": 0.512,
29
+ "min": 0.512,
30
+ "med": 0.512,
31
+ "max": 0.512
32
+ }
33
+ },
34
+ "http_reqs": {
35
+ "contains": "default",
36
+ "values": { "count": 1, "rate": 42.78257893385813 },
37
+ "type": "counter"
38
+ },
39
+ "http_req_blocked": {
40
+ "contains": "time",
41
+ "values": {
42
+ "avg": 1.496,
43
+ "min": 1.496,
44
+ "med": 1.496,
45
+ "max": 1.496,
46
+ "p(90)": 1.496,
47
+ "p(95)": 1.496
48
+ },
49
+ "type": "trend"
50
+ },
51
+ "data_received": {
52
+ "type": "counter",
53
+ "contains": "data",
54
+ "values": { "count": 269, "rate": 11508.513733207838 }
55
+ },
56
+ "iterations": {
57
+ "type": "counter",
58
+ "contains": "default",
59
+ "values": { "count": 1, "rate": 42.78257893385813 }
60
+ },
61
+ "http_req_waiting": {
62
+ "type": "trend",
63
+ "contains": "time",
64
+ "values": {
65
+ "p(95)": 18.443,
66
+ "avg": 18.443,
67
+ "min": 18.443,
68
+ "med": 18.443,
69
+ "max": 18.443,
70
+ "p(90)": 18.443
71
+ }
72
+ },
73
+ "http_req_receiving": {
74
+ "type": "trend",
75
+ "contains": "time",
76
+ "values": {
77
+ "avg": 0.186,
78
+ "min": 0.186,
79
+ "med": 0.186,
80
+ "max": 0.186,
81
+ "p(90)": 0.186,
82
+ "p(95)": 0.186
83
+ }
84
+ },
85
+ "http_req_duration{expected_response:true}": {
86
+ "type": "trend",
87
+ "contains": "time",
88
+ "values": {
89
+ "max": 19.141,
90
+ "p(90)": 19.141,
91
+ "p(95)": 19.141,
92
+ "avg": 19.141,
93
+ "min": 19.141,
94
+ "med": 19.141
95
+ }
96
+ },
97
+ "iteration_duration": {
98
+ "type": "trend",
99
+ "contains": "time",
100
+ "values": {
101
+ "avg": 22.577833,
102
+ "min": 22.577833,
103
+ "med": 22.577833,
104
+ "max": 22.577833,
105
+ "p(90)": 22.577833,
106
+ "p(95)": 22.577833
107
+ }
108
+ },
109
+ "http_req_connecting": {
110
+ "type": "trend",
111
+ "contains": "time",
112
+ "values": {
113
+ "avg": 0.673,
114
+ "min": 0.673,
115
+ "med": 0.673,
116
+ "max": 0.673,
117
+ "p(90)": 0.673,
118
+ "p(95)": 0.673
119
+ }
120
+ },
121
+ "http_req_failed": {
122
+ "type": "rate",
123
+ "contains": "default",
124
+ "values": { "rate": 0, "passes": 0, "fails": 1 }
125
+ },
126
+ "http_req_duration": {
127
+ "type": "trend",
128
+ "contains": "time",
129
+ "values": {
130
+ "p(90)": 19.141,
131
+ "p(95)": 19.141,
132
+ "avg": 19.141,
133
+ "min": 19.141,
134
+ "med": 19.141,
135
+ "max": 19.141
136
+ }
137
+ },
138
+ "data_sent": {
139
+ "type": "counter",
140
+ "contains": "data",
141
+ "values": { "count": 102, "rate": 4363.82305125353 }
142
+ }
143
+ },
144
+ "root_group": {
145
+ "name": "",
146
+ "path": "",
147
+ "id": "d41d8cd98f00b204e9800998ecf8427e",
148
+ "groups": [],
149
+ "checks": [
150
+ {
151
+ "name": "is status 200",
152
+ "path": "::is status 200",
153
+ "id": "548d37ca5f33793206f7832e7cea54fb",
154
+ "passes": 1,
155
+ "fails": 0
156
+ }
157
+ ]
158
+ }
159
+ }
160
+ */
161
+
162
+ type TrendStat = 'avg' | 'min' | 'med' | 'max' | 'p(90)' | 'p(95)';
163
+ type MetricType = 'trend' | 'rate' | 'counter';
164
+ type MetricContains = 'time' | 'default' | 'data';
165
+
166
+ interface TrendValues {
167
+ avg: number;
168
+ min: number;
169
+ med: number;
170
+ max: number;
171
+ 'p(90)': number;
172
+ 'p(95)': number;
173
+ }
174
+
175
+ interface RateValues {
176
+ rate: number;
177
+ passes: number;
178
+ fails: number;
179
+ }
180
+
181
+ interface CounterValues {
182
+ count: number;
183
+ rate: number;
184
+ }
185
+
186
+ interface K6TrendMetric {
187
+ type: 'trend';
188
+ contains: 'time';
189
+ values: TrendValues;
190
+ }
191
+
192
+ interface RateMetric {
193
+ type: 'rate';
194
+ contains: 'default';
195
+ values: RateValues;
196
+ }
197
+
198
+ interface K6CounterMetric {
199
+ type: 'counter';
200
+ contains: MetricContains;
201
+ values: CounterValues;
202
+ }
203
+
204
+ interface Options {
205
+ summaryTrendStats: TrendStat[];
206
+ summaryTimeUnit: string;
207
+ noColor: boolean;
208
+ }
209
+
210
+ interface State {
211
+ isStdOutTTY: boolean;
212
+ isStdErrTTY: boolean;
213
+ testRunDurationMs: number;
214
+ }
215
+
216
+ interface Metrics {
217
+ http_req_tls_handshaking: K6TrendMetric;
218
+ checks: RateMetric;
219
+ http_req_sending: K6TrendMetric;
220
+ http_reqs: K6CounterMetric;
221
+ http_req_blocked: K6TrendMetric;
222
+ data_received: K6CounterMetric;
223
+ iterations: K6CounterMetric;
224
+ http_req_waiting: K6TrendMetric;
225
+ http_req_receiving: K6TrendMetric;
226
+ 'http_req_duration{expected_response:true}': K6TrendMetric;
227
+ iteration_duration: K6TrendMetric;
228
+ http_req_connecting: K6TrendMetric;
229
+ http_req_failed: RateMetric;
230
+ http_req_duration: K6TrendMetric;
231
+ data_sent: K6CounterMetric;
232
+ }
233
+
234
+ interface K6Check {
235
+ name: string;
236
+ path: string;
237
+ id: string;
238
+ passes: number;
239
+ fails: number;
240
+ }
241
+
242
+ interface RootGroup {
243
+ name: string;
244
+ path: string;
245
+ id: string;
246
+ groups: unknown[];
247
+ checks: K6Check[];
248
+ }
249
+
250
+ interface K6EndOfTestSummary {
251
+ options: Options;
252
+ state: State;
253
+ metrics: Metrics;
254
+ root_group: RootGroup;
255
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Parses Prometheus metrics text format and extracts time series data
3
+ */
4
+ export class PrometheusMetricsParser {
5
+ /**
6
+ * Extracts all values for a specific metric from collected metrics data
7
+ */
8
+ static extractMetricValues(metricsData: string[], metricName: string): number[] {
9
+ const values: number[] = [];
10
+
11
+ for (const metricsText of metricsData) {
12
+ const lines = metricsText.split('\n');
13
+
14
+ for (const line of lines) {
15
+ // Skip comments and empty lines
16
+ if (line.startsWith('#') || line.trim() === '') {
17
+ continue;
18
+ }
19
+
20
+ // Parse metric line: metric_name{labels} value
21
+ // For example: n8n_nodejs_heap_size_total_bytes 149159936
22
+ // For simplicity, we'll just check if the line starts with the metric name
23
+ if (line.startsWith(metricName)) {
24
+ const parts = line.split(/\s+/);
25
+ if (parts.length >= 2) {
26
+ const value = parseFloat(parts[parts.length - 1]);
27
+ if (!isNaN(value)) {
28
+ values.push(value);
29
+ }
30
+ }
31
+ }
32
+ }
33
+ }
34
+
35
+ return values;
36
+ }
37
+
38
+ /**
39
+ * Calculates statistics for a metric from collected data
40
+ */
41
+ static calculateMetricStats(
42
+ metricsData: string[],
43
+ metricName: string,
44
+ ): { max: number; avg: number; min: number; count: number } | null {
45
+ const values = this.extractMetricValues(metricsData, metricName);
46
+
47
+ if (values.length === 0) {
48
+ return null;
49
+ }
50
+
51
+ const max = Math.max(...values);
52
+ const min = Math.min(...values);
53
+ const sum = values.reduce((a, b) => a + b, 0);
54
+ const avg = sum / values.length;
55
+
56
+ return {
57
+ max,
58
+ min,
59
+ avg,
60
+ count: values.length,
61
+ };
62
+ }
63
+ }