Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(playwright): add trace support with opentelemetry #2174

23 changes: 21 additions & 2 deletions packages/artillery-engine-playwright/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ class PlaywrightEngine {
constructor(script) {
debug('constructor');
this.target = script.config.target;
this.tracing = (script.config?.plugins?.['publish-metrics'] || []).some(
(config) => {
return config.type === 'open-telemetry' && config.traces;
}
);

this.config = script.config?.engines?.playwright || {};
this.processor = script.config.processor || {};
Expand Down Expand Up @@ -195,9 +200,23 @@ class PlaywrightEngine {
self.processor[spec.testFunction] ||
self.processor[spec.flowFunction];

const test = { step };
if (self.tracing) {
// The following hook function starts the scenario span and returns it
const scenarioSpan = self.processor[spec.beforeScenario](initialContext)
// Returns the step function ready for the user to activate
const traceStep = self.processor[spec.traceStepFunction](scenarioSpan, events)
const test = { step: traceStep }

await fn(page, initialContext, events, test)

const endScenarioSpan = self.processor[spec.afterScenario]
endScenarioSpan(initialContext)
} else {
const test = { step };

await fn(page, initialContext, events, test);
}

await fn(page, initialContext, events, test);

await page.close();

Expand Down
116 changes: 92 additions & 24 deletions packages/artillery-plugin-publish-metrics/lib/open-telemetry/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class OTelReporter {
process.env.DEBUG &&
process.env.DEBUG === 'plugin:publish-metrics:open-telemetry'
) {
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.ERROR);
}
this.metricExporters = {
'otlp-proto'(options) {
Expand Down Expand Up @@ -123,38 +123,73 @@ class OTelReporter {
}

if (config.traces) {
// Set basics needed regardless of the engine
this.traceConfig = config.traces;
this.validateExporter(
this.traceExporters,
this.traceConfig.exporter,
'trace'
);
this.tracing = true;

this.configureTrace(this.traceConfig);
// Create set of all engines used in test -> even though we only support Playwright and HTTP engine for now this is future compatible, same amount of work
this.engines = new Set();
const scenarios = this.script.scenarios || [];
scenarios.forEach((scenario) => {
scenario.engine
? this.engines.add(scenario.engine)
: this.engines.add('http');
});

attachScenarioHooks(script, [
{
type: 'beforeRequest',
name: 'startOTelSpan',
hook: this.startHTTPRequestSpan.bind(this)
},
{
type: 'afterResponse',
name: 'exportOTelSpan',
hook: this.endHTTPRequestSpan.bind(this)
},
{
type: 'beforeScenario',
name: 'startScenarioSpan',
hook: this.startScenarioSpan('http').bind(this)
},
{
type: 'afterScenario',
name: 'endScenarioSpan',
hook: this.endScenarioSpan('http').bind(this)
}
]);
// Set hooks for tracing HTTP engine based scenarios
if (this.engines.has('http')) {
attachScenarioHooks(script, [
{
type: 'beforeRequest',
name: 'startOTelSpan',
hook: this.startHTTPRequestSpan.bind(this)
},
{
type: 'afterResponse',
name: 'exportOTelSpan',
hook: this.endHTTPRequestSpan.bind(this)
},
{
type: 'beforeScenario',
name: 'startScenarioSpan',
hook: this.startScenarioSpan('http').bind(this)
},
{
type: 'afterScenario',
name: 'endScenarioSpan',
hook: this.endScenarioSpan('http').bind(this)
}
]);
}

// Set hooks for tracing Playwright engine based scenarios
if (this.engines.has('playwright')) {
attachScenarioHooks(script, [
{
engine: 'playwright',
type: 'beforeScenario',
name: 'startScenarioSpan',
hook: this.startScenarioSpan('playwright').bind(this)
},
{
engine: 'playwright',
type: 'traceStepFunction',
name: 'step',
hook: this.step.bind(this)
},
{
engine: 'playwright',
type: 'afterScenario',
name: 'endScenarioSpan',
hook: this.endScenarioSpan('playwright').bind(this)
}
]);
}
}
}

Expand Down Expand Up @@ -474,6 +509,39 @@ class OTelReporter {
}
}

// Allows users to wrap a span around a step or set of steps and add attributes to it. It also sends the span into the callback so users have ability to set additional attributes, events etc.
step(parent, events) {
return async function (stepName, callback, attributes) {
const ctx = trace.setSpan(context.active(), parent);
const span = this.playwrightTracer.startSpan(
stepName,
{ kind: SpanKind.CLIENT },
ctx
);
const startTime = Date.now();
if (this.traceConfig.attributes) {
span.setAttributes(this.traceConfig.attributes);
}
try {
if (attributes) {
span.setAttributes(attributes);
}
await callback(span);
} catch (err) {
debug('There has been an error during step execution: ', err);
span.recordException(err);
span.setStatus({
code: SpanKind.ERROR,
message: err.message
});
} finally {
span.end();
const difference = Date.now() - startTime;
events.emit('histogram', `browser.step.${stepName}`, difference);
}
}.bind(this);
}

async shutDown() {
if (this.metrics) {
while (this.pendingRequests > 0) {
Expand Down
10 changes: 6 additions & 4 deletions packages/artillery-plugin-publish-metrics/lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ function attachScenarioHooks(script, specs) {
scenarios.forEach((scenario) => {
specs.forEach((spec) => {
// console.log(spec.engine, scenario.engine);
// if (spec.engine && spec.engine !== scenario.engine) {
// return;
// }

if (
(spec.engine && spec.engine !== scenario.engine) ||
(!spec.engine && scenario.engine && scenario.engine !== 'http')
) {
return;
}
scenario[spec.type] = [].concat(scenario[spec.type] || []);
scenario[spec.type].push(spec.name);
addHelperFunction(script, spec.name, spec.hook);
Expand Down