Skip to content

Commit 7b891c9

Browse files
authored
test(publish-metrics): add tests for otel tracing (#2718)
1 parent a8d375e commit 7b891c9

File tree

14 files changed

+1349
-3
lines changed

14 files changed

+1349
-3
lines changed

package-lock.json

+2-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/artillery-plugin-publish-metrics/lib/open-telemetry/exporters.js

+4
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ const traceExporters = {
4343
zipkin(options) {
4444
const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin');
4545
return new ZipkinExporter(options);
46+
},
47+
__test(options) {
48+
const { FileSpanExporter } = require('./file-span-exporter');
49+
return new FileSpanExporter(options);
4650
}
4751
};
4852

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
'use strict';
2+
3+
const fs = require('fs');
4+
const path = require('path');
5+
const { ConsoleSpanExporter } = require('@opentelemetry/sdk-trace-base');
6+
const { ExportResultCode } = require('@opentelemetry/core');
7+
8+
// We extend ConsoleSpanExporter as the logic is almost the same, we just need to write to a file instead of log to console
9+
class FileSpanExporter extends ConsoleSpanExporter {
10+
constructor(opts) {
11+
super();
12+
this.filePath = this.setOutputPath(opts.output);
13+
14+
// We create the file in the main thread and then append to it in the worker threads
15+
if (typeof process.env.LOCAL_WORKER_ID === 'undefined') {
16+
// We write the '[' here to open an array in the file, so we can append spans to it
17+
fs.writeFileSync(this.filePath, '[\n', { flag: 'w' });
18+
}
19+
}
20+
21+
_sendSpans(spans, done) {
22+
const spansToExport = spans.map((span) =>
23+
JSON.stringify(this._exportInfo(span))
24+
);
25+
if (spansToExport.length > 0) {
26+
fs.writeFileSync(this.filePath, spansToExport.join(',\n') + ',', {
27+
flag: 'a'
28+
}); // TODO fix trailing coma
29+
}
30+
if (done) {
31+
return done({ code: ExportResultCode.SUCCESS });
32+
}
33+
}
34+
35+
shutdown() {
36+
this._sendSpans([]);
37+
this.forceFlush();
38+
if (typeof process.env.LOCAL_WORKER_ID === 'undefined') {
39+
try {
40+
// Removing the trailing comma and closing the array
41+
const data =
42+
fs.readFileSync(this.filePath, 'utf8').slice(0, -1) + '\n]';
43+
fs.writeFileSync(this.filePath, data, { flag: 'w' });
44+
console.log('File updated successfully.');
45+
} catch (err) {
46+
console.error('FileSpanExporter: Error updating file:');
47+
throw err;
48+
}
49+
}
50+
}
51+
52+
setOutputPath(output) {
53+
const defaultFileName = `otel-spans-${global.artillery.testRunId}.json`;
54+
const defaultOutputPath = path.resolve(process.cwd(), defaultFileName);
55+
if (!output) {
56+
return defaultOutputPath;
57+
}
58+
59+
const isFile = path.extname(output);
60+
const exists = isFile
61+
? fs.existsSync(path.dirname(output))
62+
: fs.existsSync(output);
63+
64+
if (!exists) {
65+
throw new Error(`FileSpanExporter: Path '${output}' does not exist`);
66+
}
67+
return isFile ? output : path.resolve(output, defaultFileName);
68+
}
69+
}
70+
71+
module.exports = {
72+
FileSpanExporter
73+
};

packages/artillery-plugin-publish-metrics/lib/open-telemetry/tracing/base.js

+4
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ class OTelTraceConfig {
5757
}
5858
}
5959

60+
if (this.config.__outputPath) {
61+
this.exporterOpts.output = this.config.__outputPath;
62+
}
63+
6064
this.exporter = traceExporters[this.config.exporter || 'otlp-http'](
6165
this.exporterOpts
6266
);

packages/artillery-plugin-publish-metrics/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
},
5252
"devDependencies": {
5353
"shelljs": "^0.8.4",
54-
"tap": "^19.0.2"
54+
"tap": "^19.0.2",
55+
"zx": "^4.3.0"
5556
}
5657
}

packages/artillery/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
},
4545
"scripts": {
4646
"test:unit": "tap --timeout=420 test/unit/*.test.js",
47-
"test:acceptance": "tap --timeout=420 test/cli/*.test.js && bash test/lib/run.sh",
47+
"test:acceptance": "tap --timeout=420 test/cli/*.test.js && bash test/lib/run.sh && tap --timeout=420 test/publish-metrics/**/*.test.js",
4848
"test": " npm run test:unit && npm run test:acceptance",
4949
"test:windows": "npm run test:unit && tap --timeout=420 test/cli/*.test.js",
5050
"test:aws": "tap --timeout=4200 test/cloud-e2e/**/*.test.js",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use strict';
2+
3+
async function simpleCheck(page, userContext, events, test) {
4+
await test.step('Go to Artillery', async () => {
5+
const requestPromise = page.waitForRequest('https://artillery.io/');
6+
await page.goto('https://artillery.io/');
7+
const req = await requestPromise;
8+
});
9+
await test.step('Go to docs', async () => {
10+
const docs = await page.getByRole('link', { name: 'Docs' });
11+
await docs.click();
12+
await page.waitForURL('https://www.artillery.io/docs');
13+
});
14+
15+
await test.step('Go to core concepts', async () => {
16+
await page
17+
.getByRole('link', {
18+
name: 'Review core concepts'
19+
})
20+
.click();
21+
22+
await page.waitForURL(
23+
'https://www.artillery.io/docs/get-started/core-concepts'
24+
);
25+
});
26+
}
27+
28+
async function simpleError(page, userContext, events, test) {
29+
await test.step('Go to Artillery', async () => {
30+
const requestPromise = page.waitForRequest('https://artillery.io/');
31+
await page.goto('https://artillery.io/');
32+
const req = await requestPromise;
33+
});
34+
await test.step('Go to docs', async () => {
35+
const docs = await page.getByRole('link', { name: 'Docs' });
36+
await docs.click();
37+
await page.waitForURL('https://www.artillery.io/docs');
38+
});
39+
40+
await test.step('Go to core concepts', async () => {
41+
await page
42+
.getByRole('link', {
43+
name: 'Non-existent link'
44+
})
45+
.click();
46+
await page.waitForURL(
47+
'https://www.artillery.io/docs/get-started/core-concepts'
48+
);
49+
});
50+
}
51+
52+
module.exports = {
53+
simpleCheck,
54+
simpleError
55+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use strict';
2+
3+
function getTestId(outputString) {
4+
const regex = /Test run id: \S+/;
5+
const match = outputString.match(regex);
6+
return match[0].replace('Test run id: ', '');
7+
}
8+
9+
function setDynamicHTTPTraceExpectations(expectedOutcome) {
10+
if (expectedOutcome.errors) {
11+
expectedOutcome.reqSpansWithError = expectedOutcome.reqSpansWithErrorPerVu
12+
? expectedOutcome.reqSpansWithErrorPerVu * expectedOutcome.vus
13+
: 0;
14+
}
15+
expectedOutcome.spansPerVu = 1 + expectedOutcome.reqSpansPerVu;
16+
expectedOutcome.reqSpans =
17+
expectedOutcome.vus * expectedOutcome.reqSpansPerVu;
18+
expectedOutcome.req = expectedOutcome.vus * expectedOutcome.reqPerVu;
19+
expectedOutcome.totalSpans = expectedOutcome.vus * expectedOutcome.spansPerVu;
20+
return expectedOutcome;
21+
}
22+
23+
function setDynamicPlaywrightTraceExpectations(expectedOutcome) {
24+
expectedOutcome.spansPerVu =
25+
1 + expectedOutcome.pageSpansPerVu + (expectedOutcome.stepSpansPerVu || 0); // 1 represents the root scenario/VU span
26+
expectedOutcome.pageSpans =
27+
expectedOutcome.vus * expectedOutcome.pageSpansPerVu;
28+
expectedOutcome.totalSpans = expectedOutcome.vus * expectedOutcome.spansPerVu;
29+
30+
if (expectedOutcome.stepSpansPerVu) {
31+
expectedOutcome.stepSpans =
32+
expectedOutcome.vus * expectedOutcome.stepSpansPerVu;
33+
}
34+
return expectedOutcome;
35+
}
36+
37+
module.exports = {
38+
getTestId,
39+
setDynamicHTTPTraceExpectations,
40+
setDynamicPlaywrightTraceExpectations
41+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
config:
2+
target: "http://asciiart.artillery.io:8080"
3+
phases:
4+
- duration: 2
5+
arrivalRate: 2
6+
plugins:
7+
publish-metrics:
8+
- type: "open-telemetry"
9+
traces:
10+
useRequestNames: true
11+
replaceSpanNameRegex:
12+
- pattern: "/armadillo"
13+
as: "bombolini"
14+
exporter: "__test"
15+
16+
scenarios:
17+
- name: "trace-http-test"
18+
flow:
19+
- get:
20+
url: "/dino"
21+
name: "dino"
22+
- get:
23+
url: "/pony"
24+
- get:
25+
url: "/armadillo"
26+
name: "armadillo"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
config:
2+
target: "https://www.artillery.io"
3+
phases:
4+
- duration: 2
5+
arrivalRate: 2
6+
engines:
7+
playwright:
8+
extendedMetrics: true
9+
processor: "../fixtures/flow.js"
10+
plugins:
11+
publish-metrics:
12+
- type: "open-telemetry"
13+
traces:
14+
replaceSpanNameRegex:
15+
- pattern: https://www.artillery.io/docs/get-started/core-concepts
16+
as: core_concepts
17+
- pattern: https://www.artillery.io/docs
18+
as: docs_main
19+
exporter: "__test"
20+
attributes:
21+
environment: 'test'
22+
tool: 'Artillery'
23+
24+
scenarios:
25+
- engine: playwright
26+
name: "trace-playwright-test"
27+
testFunction: "simpleCheck"

0 commit comments

Comments
 (0)