@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.
- package/.turbo/turbo-build.log +4 -0
- package/Dockerfile +63 -0
- package/README.md +122 -0
- package/bin/n8n-benchmark +13 -0
- package/biome.jsonc +7 -0
- package/dist/build.tsbuildinfo +1 -0
- package/dist/commands/list.d.ts +8 -0
- package/dist/commands/list.js +23 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/run.d.ts +24 -0
- package/dist/commands/run.js +128 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/config/common-flags.d.ts +1 -0
- package/dist/config/common-flags.js +9 -0
- package/dist/config/common-flags.js.map +1 -0
- package/dist/n8n-api-client/authenticated-n8n-api-client.d.ts +15 -0
- package/dist/n8n-api-client/authenticated-n8n-api-client.js +67 -0
- package/dist/n8n-api-client/authenticated-n8n-api-client.js.map +1 -0
- package/dist/n8n-api-client/credentials-api-client.d.ts +9 -0
- package/dist/n8n-api-client/credentials-api-client.js +24 -0
- package/dist/n8n-api-client/credentials-api-client.js.map +1 -0
- package/dist/n8n-api-client/data-table-api-client.d.ts +9 -0
- package/dist/n8n-api-client/data-table-api-client.js +23 -0
- package/dist/n8n-api-client/data-table-api-client.js.map +1 -0
- package/dist/n8n-api-client/n8n-api-client.d.ts +13 -0
- package/dist/n8n-api-client/n8n-api-client.js +82 -0
- package/dist/n8n-api-client/n8n-api-client.js.map +1 -0
- package/dist/n8n-api-client/n8n-api-client.types.d.ts +21 -0
- package/dist/n8n-api-client/n8n-api-client.types.js +3 -0
- package/dist/n8n-api-client/n8n-api-client.types.js.map +1 -0
- package/dist/n8n-api-client/project-api-client.d.ts +6 -0
- package/dist/n8n-api-client/project-api-client.js +14 -0
- package/dist/n8n-api-client/project-api-client.js.map +1 -0
- package/dist/n8n-api-client/workflows-api-client.d.ts +11 -0
- package/dist/n8n-api-client/workflows-api-client.js +30 -0
- package/dist/n8n-api-client/workflows-api-client.js.map +1 -0
- package/dist/scenario/scenario-data-loader.d.ts +13 -0
- package/dist/scenario/scenario-data-loader.js +84 -0
- package/dist/scenario/scenario-data-loader.js.map +1 -0
- package/dist/scenario/scenario-loader.d.ts +7 -0
- package/dist/scenario/scenario-loader.js +101 -0
- package/dist/scenario/scenario-loader.js.map +1 -0
- package/dist/test-execution/app-metrics-poller.d.ts +13 -0
- package/dist/test-execution/app-metrics-poller.js +54 -0
- package/dist/test-execution/app-metrics-poller.js.map +1 -0
- package/dist/test-execution/k6-executor.d.ts +33 -0
- package/dist/test-execution/k6-executor.js +120 -0
- package/dist/test-execution/k6-executor.js.map +1 -0
- package/dist/test-execution/k6-summary.d.ts +82 -0
- package/dist/test-execution/k6-summary.js +2 -0
- package/dist/test-execution/k6-summary.js.map +1 -0
- package/dist/test-execution/prometheus-metrics-parser.d.ts +9 -0
- package/dist/test-execution/prometheus-metrics-parser.js +44 -0
- package/dist/test-execution/prometheus-metrics-parser.js.map +1 -0
- package/dist/test-execution/scenario-data-importer.d.ts +21 -0
- package/dist/test-execution/scenario-data-importer.js +108 -0
- package/dist/test-execution/scenario-data-importer.js.map +1 -0
- package/dist/test-execution/scenario-runner.d.ts +18 -0
- package/dist/test-execution/scenario-runner.js +46 -0
- package/dist/test-execution/scenario-runner.js.map +1 -0
- package/dist/test-execution/test-report.d.ts +56 -0
- package/dist/test-execution/test-report.js +65 -0
- package/dist/test-execution/test-report.js.map +1 -0
- package/dist/types/scenario.d.ts +16 -0
- package/dist/types/scenario.js +3 -0
- package/dist/types/scenario.js.map +1 -0
- package/eslint.config.mjs +22 -0
- package/infra/.terraform.lock.hcl +60 -0
- package/infra/benchmark-env.tf +54 -0
- package/infra/modules/benchmark-vm/output.tf +11 -0
- package/infra/modules/benchmark-vm/vars.tf +29 -0
- package/infra/modules/benchmark-vm/vm.tf +126 -0
- package/infra/output.tf +16 -0
- package/infra/providers.tf +23 -0
- package/infra/vars.tf +34 -0
- package/package.json +55 -0
- package/scenarios/binary-data/binary-data.json +67 -0
- package/scenarios/binary-data/binary-data.manifest.json +7 -0
- package/scenarios/binary-data/binary-data.script.js +29 -0
- package/scenarios/credential-http-node/credential-bearer.json +8 -0
- package/scenarios/credential-http-node/credential-http-node.json +241 -0
- package/scenarios/credential-http-node/credential-http-node.manifest.json +10 -0
- package/scenarios/credential-http-node/credential-http-node.script.js +30 -0
- package/scenarios/data-table-node/data-table-node.json +168 -0
- package/scenarios/data-table-node/data-table-node.manifest.json +10 -0
- package/scenarios/data-table-node/data-table-node.script.js +38 -0
- package/scenarios/data-table-node/data-table.json +25 -0
- package/scenarios/http-node/http-node.json +213 -0
- package/scenarios/http-node/http-node.manifest.json +7 -0
- package/scenarios/http-node/http-node.script.js +30 -0
- package/scenarios/js-code-node/js-code-node.json +96 -0
- package/scenarios/js-code-node/js-code-node.manifest.json +7 -0
- package/scenarios/js-code-node/js-code-node.script.js +29 -0
- package/scenarios/multiple-webhooks/multiple-webhooks.manifest.json +20 -0
- package/scenarios/multiple-webhooks/multiple-webhooks.script.js +19 -0
- package/scenarios/multiple-webhooks/multiple-webhooks1.json +25 -0
- package/scenarios/multiple-webhooks/multiple-webhooks10.json +25 -0
- package/scenarios/multiple-webhooks/multiple-webhooks2.json +25 -0
- package/scenarios/multiple-webhooks/multiple-webhooks3.json +25 -0
- package/scenarios/multiple-webhooks/multiple-webhooks4.json +25 -0
- package/scenarios/multiple-webhooks/multiple-webhooks5.json +25 -0
- package/scenarios/multiple-webhooks/multiple-webhooks6.json +25 -0
- package/scenarios/multiple-webhooks/multiple-webhooks7.json +25 -0
- package/scenarios/multiple-webhooks/multiple-webhooks8.json +25 -0
- package/scenarios/multiple-webhooks/multiple-webhooks9.json +25 -0
- package/scenarios/py-code-node/py-code-node.json +98 -0
- package/scenarios/py-code-node/py-code-node.manifest.json +7 -0
- package/scenarios/py-code-node/py-code-node.script.js +29 -0
- package/scenarios/scenario.schema.json +51 -0
- package/scenarios/set-node-expressions/set-node-expressions.json +91 -0
- package/scenarios/set-node-expressions/set-node-expressions.manifest.json +7 -0
- package/scenarios/set-node-expressions/set-node-expressions.script.js +18 -0
- package/scenarios/single-webhook/single-webhook.json +25 -0
- package/scenarios/single-webhook/single-webhook.manifest.json +7 -0
- package/scenarios/single-webhook/single-webhook.script.js +18 -0
- package/scripts/bootstrap.sh +63 -0
- package/scripts/clients/docker-compose-client.mjs +45 -0
- package/scripts/clients/ssh-client.mjs +37 -0
- package/scripts/clients/terraform-client.mjs +71 -0
- package/scripts/destroy-cloud-env.mjs +86 -0
- package/scripts/mock-api/mappings/mockApiData.json +92110 -0
- package/scripts/n8n-setups/postgres/docker-compose.yml +76 -0
- package/scripts/n8n-setups/postgres/setup.mjs +15 -0
- package/scripts/n8n-setups/scaling-multi-main/docker-compose.yml +230 -0
- package/scripts/n8n-setups/scaling-multi-main/nginx.conf +24 -0
- package/scripts/n8n-setups/scaling-multi-main/setup.mjs +15 -0
- package/scripts/n8n-setups/scaling-single-main/docker-compose.yml +174 -0
- package/scripts/n8n-setups/scaling-single-main/setup.mjs +15 -0
- package/scripts/n8n-setups/sqlite/docker-compose.yml +55 -0
- package/scripts/n8n-setups/sqlite/setup.mjs +15 -0
- package/scripts/provision-cloud-env.mjs +36 -0
- package/scripts/run-for-n8n-setup.mjs +175 -0
- package/scripts/run-in-cloud.mjs +167 -0
- package/scripts/run-locally.mjs +73 -0
- package/scripts/run.mjs +192 -0
- package/scripts/utils/flags.mjs +20 -0
- package/src/commands/list.ts +26 -0
- package/src/commands/run.ts +140 -0
- package/src/config/common-flags.ts +6 -0
- package/src/n8n-api-client/authenticated-n8n-api-client.ts +88 -0
- package/src/n8n-api-client/credentials-api-client.ts +28 -0
- package/src/n8n-api-client/data-table-api-client.ts +30 -0
- package/src/n8n-api-client/n8n-api-client.ts +85 -0
- package/src/n8n-api-client/n8n-api-client.types.ts +27 -0
- package/src/n8n-api-client/project-api-client.ts +11 -0
- package/src/n8n-api-client/workflows-api-client.ts +38 -0
- package/src/scenario/scenario-data-loader.ts +75 -0
- package/src/scenario/scenario-loader.ts +90 -0
- package/src/test-execution/app-metrics-poller.ts +81 -0
- package/src/test-execution/k6-executor.ts +192 -0
- package/src/test-execution/k6-summary.ts +255 -0
- package/src/test-execution/prometheus-metrics-parser.ts +63 -0
- package/src/test-execution/scenario-data-importer.ts +165 -0
- package/src/test-execution/scenario-runner.ts +76 -0
- package/src/test-execution/test-report.ts +152 -0
- package/src/types/scenario.ts +33 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { AuthenticatedN8nApiClient } from '@/n8n-api-client/authenticated-n8n-api-client';
|
|
2
|
+
import { CredentialApiClient } from '@/n8n-api-client/credentials-api-client';
|
|
3
|
+
import { DataTableApiClient } from '@/n8n-api-client/data-table-api-client';
|
|
4
|
+
import type { Workflow, Credential, DataTable } from '@/n8n-api-client/n8n-api-client.types';
|
|
5
|
+
import { ProjectApiClient } from '@/n8n-api-client/project-api-client';
|
|
6
|
+
import { WorkflowApiClient } from '@/n8n-api-client/workflows-api-client';
|
|
7
|
+
import type { LoadableScenarioData } from '@/scenario/scenario-data-loader';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Imports scenario data into an n8n instance
|
|
11
|
+
*/
|
|
12
|
+
export class ScenarioDataImporter {
|
|
13
|
+
private readonly workflowApiClient: WorkflowApiClient;
|
|
14
|
+
private readonly credentialApiClient: CredentialApiClient;
|
|
15
|
+
private readonly dataTableApiClient: DataTableApiClient;
|
|
16
|
+
private readonly projectApiClient: ProjectApiClient;
|
|
17
|
+
|
|
18
|
+
constructor(n8nApiClient: AuthenticatedN8nApiClient) {
|
|
19
|
+
this.workflowApiClient = new WorkflowApiClient(n8nApiClient);
|
|
20
|
+
this.credentialApiClient = new CredentialApiClient(n8nApiClient);
|
|
21
|
+
this.dataTableApiClient = new DataTableApiClient(n8nApiClient);
|
|
22
|
+
this.projectApiClient = new ProjectApiClient(n8nApiClient);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private replaceValuesInObject(obj: unknown, searchText: string, targetText: string) {
|
|
26
|
+
if (Array.isArray(obj)) {
|
|
27
|
+
obj.map((item) => this.replaceValuesInObject(item, searchText, targetText));
|
|
28
|
+
} else if (typeof obj === 'object' && obj !== null) {
|
|
29
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
30
|
+
if (typeof value === 'string' && value === searchText) {
|
|
31
|
+
(obj as Record<string, unknown>)[key] = targetText;
|
|
32
|
+
} else {
|
|
33
|
+
this.replaceValuesInObject(value, searchText, targetText);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async importTestScenarioData(data: LoadableScenarioData) {
|
|
40
|
+
const existingWorkflows = await this.workflowApiClient.getAllWorkflows();
|
|
41
|
+
const existingCredentials = await this.credentialApiClient.getAllCredentials();
|
|
42
|
+
const existingDataTables = await this.dataTableApiClient.getAllDataTables();
|
|
43
|
+
|
|
44
|
+
for (const credential of data.credentials) {
|
|
45
|
+
const createdCredential = await this.importCredentials({ existingCredentials, credential });
|
|
46
|
+
|
|
47
|
+
// We need to update the id and name of the credential in the workflows
|
|
48
|
+
for (const workflow of data.workflows) {
|
|
49
|
+
this.replaceValuesInObject(workflow, credential.id, createdCredential.id);
|
|
50
|
+
this.replaceValuesInObject(workflow, credential.name, createdCredential.name);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const workflow of data.workflows) {
|
|
55
|
+
await this.importWorkflow({ existingWorkflows, workflow });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let dataTableId: string | undefined;
|
|
59
|
+
if (data.dataTable) {
|
|
60
|
+
dataTableId = await this.importDataTable({ existingDataTables, dataTable: data.dataTable });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { dataTableId };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Imports a single credential into n8n removing any existing credentials with the same name
|
|
68
|
+
* @param opts
|
|
69
|
+
* @returns
|
|
70
|
+
*/
|
|
71
|
+
private async importCredentials(opts: {
|
|
72
|
+
existingCredentials: Credential[];
|
|
73
|
+
credential: Credential;
|
|
74
|
+
}) {
|
|
75
|
+
const existingCredentials = this.findExistingCredentials(
|
|
76
|
+
opts.existingCredentials,
|
|
77
|
+
opts.credential,
|
|
78
|
+
);
|
|
79
|
+
if (existingCredentials.length > 0) {
|
|
80
|
+
for (const toDelete of existingCredentials) {
|
|
81
|
+
await this.credentialApiClient.deleteCredential(toDelete.id);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return await this.credentialApiClient.createCredential({
|
|
86
|
+
...opts.credential,
|
|
87
|
+
name: this.getBenchmarkCredentialName(opts.credential),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private async importDataTable(opts: { existingDataTables: DataTable[]; dataTable: DataTable }) {
|
|
92
|
+
const { existingDataTables, dataTable } = opts;
|
|
93
|
+
|
|
94
|
+
const projectId = await this.projectApiClient.getPersonalProject();
|
|
95
|
+
|
|
96
|
+
const existingTable = existingDataTables.find(
|
|
97
|
+
(dt) => dt.name === this.getBenchmarkDataTableName(dataTable),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (existingTable) {
|
|
101
|
+
await this.dataTableApiClient.deleteDataTable(projectId, existingTable.id);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const { id } = await this.dataTableApiClient.createDataTable(projectId, {
|
|
105
|
+
...dataTable,
|
|
106
|
+
name: this.getBenchmarkDataTableName(dataTable),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return id;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Imports a single workflow into n8n removing any existing workflows with the same name
|
|
114
|
+
*/
|
|
115
|
+
private async importWorkflow(opts: { existingWorkflows: Workflow[]; workflow: Workflow }) {
|
|
116
|
+
const existingWorkflows = this.findExistingWorkflows(opts.existingWorkflows, opts.workflow);
|
|
117
|
+
if (existingWorkflows.length > 0) {
|
|
118
|
+
for (const toDelete of existingWorkflows) {
|
|
119
|
+
await this.workflowApiClient.archiveWorkflow(toDelete.id);
|
|
120
|
+
await this.workflowApiClient.deleteWorkflow(toDelete.id);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const createdWorkflow = await this.workflowApiClient.createWorkflow({
|
|
125
|
+
...opts.workflow,
|
|
126
|
+
name: this.getBenchmarkWorkflowName(opts.workflow),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return await this.workflowApiClient.activateWorkflow(createdWorkflow);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private findExistingCredentials(
|
|
133
|
+
existingCredentials: Credential[],
|
|
134
|
+
credentialToImport: Credential,
|
|
135
|
+
): Credential[] {
|
|
136
|
+
const benchmarkCredentialName = this.getBenchmarkCredentialName(credentialToImport);
|
|
137
|
+
|
|
138
|
+
return existingCredentials.filter(
|
|
139
|
+
(existingCredential) => existingCredential.name === benchmarkCredentialName,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private findExistingWorkflows(
|
|
144
|
+
existingWorkflows: Workflow[],
|
|
145
|
+
workflowToImport: Workflow,
|
|
146
|
+
): Workflow[] {
|
|
147
|
+
const benchmarkWorkflowName = this.getBenchmarkWorkflowName(workflowToImport);
|
|
148
|
+
|
|
149
|
+
return existingWorkflows.filter(
|
|
150
|
+
(existingWorkflow) => existingWorkflow.name === benchmarkWorkflowName,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private getBenchmarkCredentialName(credential: Credential) {
|
|
155
|
+
return `[BENCHMARK] ${credential.name}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private getBenchmarkWorkflowName(workflow: Workflow) {
|
|
159
|
+
return `[BENCHMARK] ${workflow.name}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private getBenchmarkDataTableName(dataTable: DataTable) {
|
|
163
|
+
return `[BENCHMARK] ${dataTable.name}`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { sleep } from 'zx';
|
|
2
|
+
|
|
3
|
+
import { AuthenticatedN8nApiClient } from '@/n8n-api-client/authenticated-n8n-api-client';
|
|
4
|
+
import type { N8nApiClient } from '@/n8n-api-client/n8n-api-client';
|
|
5
|
+
import type { ScenarioDataFileLoader } from '@/scenario/scenario-data-loader';
|
|
6
|
+
import { ScenarioDataImporter } from '@/test-execution/scenario-data-importer';
|
|
7
|
+
import type { Scenario } from '@/types/scenario';
|
|
8
|
+
|
|
9
|
+
import type { K6Executor } from './k6-executor';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Runs scenarios
|
|
13
|
+
*/
|
|
14
|
+
export class ScenarioRunner {
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly n8nClient: N8nApiClient,
|
|
17
|
+
private readonly dataLoader: ScenarioDataFileLoader,
|
|
18
|
+
private readonly k6Executor: K6Executor,
|
|
19
|
+
private readonly ownerConfig: {
|
|
20
|
+
email: string;
|
|
21
|
+
password: string;
|
|
22
|
+
},
|
|
23
|
+
private readonly scenarioPrefix: string,
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
async runManyScenarios(scenarios: Scenario[]) {
|
|
27
|
+
console.log(`Waiting for n8n ${this.n8nClient.apiBaseUrl} to become online`);
|
|
28
|
+
await this.n8nClient.waitForInstanceToBecomeOnline();
|
|
29
|
+
|
|
30
|
+
console.log('Setting up owner');
|
|
31
|
+
await this.n8nClient.setupOwnerIfNeeded(this.ownerConfig);
|
|
32
|
+
|
|
33
|
+
const authenticatedN8nClient = await AuthenticatedN8nApiClient.createUsingUsernameAndPassword(
|
|
34
|
+
this.n8nClient,
|
|
35
|
+
this.ownerConfig,
|
|
36
|
+
);
|
|
37
|
+
const testDataImporter = new ScenarioDataImporter(authenticatedN8nClient);
|
|
38
|
+
|
|
39
|
+
for (const scenario of scenarios) {
|
|
40
|
+
await this.runSingleTestScenario(testDataImporter, scenario);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private async runSingleTestScenario(testDataImporter: ScenarioDataImporter, scenario: Scenario) {
|
|
45
|
+
const scenarioRunName = this.formTestScenarioRunName(scenario);
|
|
46
|
+
console.log('Running scenario:', scenarioRunName);
|
|
47
|
+
|
|
48
|
+
console.log('Loading and importing data');
|
|
49
|
+
const testData = await this.dataLoader.loadDataForScenario(scenario);
|
|
50
|
+
const { dataTableId } = await testDataImporter.importTestScenarioData(testData);
|
|
51
|
+
|
|
52
|
+
// Wait for 1s before executing the scenario to ensure that the workflows are activated.
|
|
53
|
+
// In multi-main mode it can take some time before the workflow becomes active.
|
|
54
|
+
await sleep(1000);
|
|
55
|
+
|
|
56
|
+
console.log('Executing scenario script');
|
|
57
|
+
await this.k6Executor.executeTestScenario(
|
|
58
|
+
{
|
|
59
|
+
...scenario,
|
|
60
|
+
dataTableId,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
scenarioRunName,
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Forms a name for the scenario by combining prefix and scenario name.
|
|
70
|
+
* The benchmarks are ran against different n8n setups, so we use the
|
|
71
|
+
* prefix to differentiate between them.
|
|
72
|
+
*/
|
|
73
|
+
private formTestScenarioRunName(scenario: Scenario) {
|
|
74
|
+
return `${this.scenarioPrefix}-${scenario.name}`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid';
|
|
2
|
+
|
|
3
|
+
import { PrometheusMetricsParser } from '@/test-execution/prometheus-metrics-parser';
|
|
4
|
+
import type { Scenario } from '@/types/scenario';
|
|
5
|
+
|
|
6
|
+
export type K6Tag = {
|
|
7
|
+
name: string;
|
|
8
|
+
value: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type Check = {
|
|
12
|
+
name: string;
|
|
13
|
+
passes: number;
|
|
14
|
+
fails: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type CounterMetric = {
|
|
18
|
+
type: 'counter';
|
|
19
|
+
count: number;
|
|
20
|
+
rate: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type TrendMetric = {
|
|
24
|
+
type: 'trend';
|
|
25
|
+
'p(95)': number;
|
|
26
|
+
avg: number;
|
|
27
|
+
min: number;
|
|
28
|
+
med: number;
|
|
29
|
+
max: number;
|
|
30
|
+
'p(90)': number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type AppMetricStats = {
|
|
34
|
+
max: number;
|
|
35
|
+
avg: number;
|
|
36
|
+
min: number;
|
|
37
|
+
count: number;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type AppMetricsReport = {
|
|
41
|
+
heapSizeTotal?: AppMetricStats;
|
|
42
|
+
heapSizeUsed?: AppMetricStats;
|
|
43
|
+
externalMemory?: AppMetricStats;
|
|
44
|
+
eventLoopLag?: AppMetricStats;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type TestReport = {
|
|
48
|
+
runId: string;
|
|
49
|
+
ts: string; // ISO8601
|
|
50
|
+
scenarioName: string;
|
|
51
|
+
tags: K6Tag[];
|
|
52
|
+
metrics: {
|
|
53
|
+
iterations: CounterMetric;
|
|
54
|
+
dataReceived: CounterMetric;
|
|
55
|
+
dataSent: CounterMetric;
|
|
56
|
+
httpRequests: CounterMetric;
|
|
57
|
+
httpRequestDuration: TrendMetric;
|
|
58
|
+
httpRequestSending: TrendMetric;
|
|
59
|
+
httpRequestReceiving: TrendMetric;
|
|
60
|
+
httpRequestWaiting: TrendMetric;
|
|
61
|
+
};
|
|
62
|
+
checks: Check[];
|
|
63
|
+
appMetrics?: AppMetricsReport;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function k6CheckToCheck(check: K6Check): Check {
|
|
67
|
+
return {
|
|
68
|
+
name: check.name,
|
|
69
|
+
passes: check.passes,
|
|
70
|
+
fails: check.fails,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function k6CounterToCounter(counter: K6CounterMetric): CounterMetric {
|
|
75
|
+
return {
|
|
76
|
+
type: 'counter',
|
|
77
|
+
count: counter.values.count,
|
|
78
|
+
rate: counter.values.rate,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function k6TrendToTrend(trend: K6TrendMetric): TrendMetric {
|
|
83
|
+
return {
|
|
84
|
+
type: 'trend',
|
|
85
|
+
'p(90)': trend.values['p(90)'],
|
|
86
|
+
avg: trend.values.avg,
|
|
87
|
+
min: trend.values.min,
|
|
88
|
+
med: trend.values.med,
|
|
89
|
+
max: trend.values.max,
|
|
90
|
+
'p(95)': trend.values['p(95)'],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Builds an app metrics report from collected Prometheus metrics data
|
|
96
|
+
*/
|
|
97
|
+
export function buildAppMetricsReport(metricsData: string[]): AppMetricsReport {
|
|
98
|
+
const heapSizeTotal = PrometheusMetricsParser.calculateMetricStats(
|
|
99
|
+
metricsData,
|
|
100
|
+
'n8n_nodejs_heap_size_total_bytes',
|
|
101
|
+
);
|
|
102
|
+
const heapSizeUsed = PrometheusMetricsParser.calculateMetricStats(
|
|
103
|
+
metricsData,
|
|
104
|
+
'n8n_nodejs_heap_size_used_bytes',
|
|
105
|
+
);
|
|
106
|
+
const externalMemory = PrometheusMetricsParser.calculateMetricStats(
|
|
107
|
+
metricsData,
|
|
108
|
+
'n8n_nodejs_external_memory_bytes',
|
|
109
|
+
);
|
|
110
|
+
const eventLoopLag = PrometheusMetricsParser.calculateMetricStats(
|
|
111
|
+
metricsData,
|
|
112
|
+
'n8n_nodejs_eventloop_lag_seconds',
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
...(heapSizeTotal && { heapSizeTotal }),
|
|
117
|
+
...(heapSizeUsed && { heapSizeUsed }),
|
|
118
|
+
...(externalMemory && { externalMemory }),
|
|
119
|
+
...(eventLoopLag && { eventLoopLag }),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Converts the k6 test summary to a test report
|
|
125
|
+
*/
|
|
126
|
+
export function buildTestReport(
|
|
127
|
+
scenario: Scenario,
|
|
128
|
+
endOfTestSummary: K6EndOfTestSummary,
|
|
129
|
+
tags: K6Tag[],
|
|
130
|
+
appMetricsData?: string[],
|
|
131
|
+
): TestReport {
|
|
132
|
+
const appMetrics = appMetricsData ? buildAppMetricsReport(appMetricsData) : undefined;
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
runId: nanoid(),
|
|
136
|
+
ts: new Date().toISOString(),
|
|
137
|
+
scenarioName: scenario.name,
|
|
138
|
+
tags,
|
|
139
|
+
checks: endOfTestSummary.root_group.checks.map(k6CheckToCheck),
|
|
140
|
+
metrics: {
|
|
141
|
+
dataReceived: k6CounterToCounter(endOfTestSummary.metrics.data_received),
|
|
142
|
+
dataSent: k6CounterToCounter(endOfTestSummary.metrics.data_sent),
|
|
143
|
+
httpRequests: k6CounterToCounter(endOfTestSummary.metrics.http_reqs),
|
|
144
|
+
httpRequestDuration: k6TrendToTrend(endOfTestSummary.metrics.http_req_duration),
|
|
145
|
+
httpRequestSending: k6TrendToTrend(endOfTestSummary.metrics.http_req_sending),
|
|
146
|
+
httpRequestReceiving: k6TrendToTrend(endOfTestSummary.metrics.http_req_receiving),
|
|
147
|
+
httpRequestWaiting: k6TrendToTrend(endOfTestSummary.metrics.http_req_waiting),
|
|
148
|
+
iterations: k6CounterToCounter(endOfTestSummary.metrics.iterations),
|
|
149
|
+
},
|
|
150
|
+
...(appMetrics && { appMetrics }),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type ScenarioData = {
|
|
2
|
+
/** Relative paths to the workflow files */
|
|
3
|
+
workflowFiles?: string[];
|
|
4
|
+
/** Relative paths to the credential files */
|
|
5
|
+
credentialFiles?: string[];
|
|
6
|
+
/** Relative paths to the data table files */
|
|
7
|
+
dataTableFile?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Configuration that defines the benchmark scenario
|
|
12
|
+
*/
|
|
13
|
+
export type ScenarioManifest = {
|
|
14
|
+
/** The name of the scenario */
|
|
15
|
+
name: string;
|
|
16
|
+
/** A longer description of the scenario */
|
|
17
|
+
description: string;
|
|
18
|
+
/** Relative path to the k6 script */
|
|
19
|
+
scriptPath: string;
|
|
20
|
+
/** Data to import before running the scenario */
|
|
21
|
+
scenarioData: ScenarioData;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Scenario with additional metadata
|
|
26
|
+
*/
|
|
27
|
+
export type Scenario = ScenarioManifest & {
|
|
28
|
+
id: string;
|
|
29
|
+
/** Path to the directory containing the scenario */
|
|
30
|
+
scenarioDirPath: string;
|
|
31
|
+
/** ID of the data table created for the scenario, if any */
|
|
32
|
+
dataTableId?: string;
|
|
33
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": [
|
|
3
|
+
"@n8n/typescript-config/tsconfig.common.json",
|
|
4
|
+
"@n8n/typescript-config/tsconfig.backend.json"
|
|
5
|
+
],
|
|
6
|
+
"compilerOptions": {
|
|
7
|
+
"rootDir": ".",
|
|
8
|
+
"baseUrl": "src",
|
|
9
|
+
"paths": {
|
|
10
|
+
"@/*": ["./*"]
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*.ts"]
|
|
14
|
+
}
|