artillery
Version:
Cloud-scale load testing. https://www.artillery.io
256 lines (215 loc) • 6.92 kB
text/typescript
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import chalkModule from 'chalk';
const chalk: any = chalkModule;
import createDebug from 'debug';
import { compileExpression as filtrex } from 'filtrex';
import {
getHashedVarToValueMap,
hashString,
replaceMetricsWithHashes
} from './utils.ts';
const debug = createDebug('plugin:ensure');
class EnsurePlugin {
// Untyped JS class - properties assigned dynamically
[key: string]: any;
constructor(script, events) {
// If running in Artillery v1, do nothing
// If running in Artillery v2, we only want to run on the main thread
if (!global.artillery) {
debug('Running in an unsupported Artillery version, nothing to do');
return;
}
if (
global.artillery &&
Number(global.artillery.version.slice(0, 1)) === 1
) {
debug('Running in Artillery v1, nothing to do');
return;
}
if (
global.artillery &&
Number(global.artillery.version.slice(0, 1)) > 1 &&
typeof process.env.LOCAL_WORKER_ID !== 'undefined'
) {
debug('Running in a worker, nothing to do');
return;
}
debug('plugin loaded');
this.script = script;
this.events = events;
const checks =
this.script.config?.ensure || this.script.config?.plugins?.ensure;
global.artillery.ext({
ext: 'beforeExit',
method: async (data) => {
if (
!checks ||
typeof process.env.ARTILLERY_DISABLE_ENSURE !== 'undefined'
) {
return;
}
debug(JSON.stringify(data));
const vars = Object.assign(
{},
global.artillery.apdexPlugin || {},
EnsurePlugin.statsToVars(data)
);
debug({ vars });
const checkTests = EnsurePlugin.runChecks(checks, vars);
global.artillery.globalEvents.emit('checks', checkTests);
checkTests
.sort((a, b) => (a.result < b.result ? 1 : -1))
.forEach((check) => {
if (check.result !== 1) {
global.artillery.log(
`${chalk.red('fail')}: ${check.original}${
check.strict ? '' : ' (optional)'
}`
);
if (check.strict) {
global.artillery.suggestedExitCode = 1;
}
} else {
global.artillery.log(`${chalk.green('ok')}: ${check.original}`);
}
});
}
});
}
// Combine counters/rates/summaries into a flat key->value object for filtrex
static statsToVars(data) {
const vars = Object.assign({}, data.report.counters, data.report.rates);
// Function to hash and assign keys from a given object to the vars object
const hashAndAssign = (obj) => {
for (const [key, value] of Object.entries(obj)) {
const hashedKey = hashString(key);
vars[key] = {
value,
hash: hashedKey
};
}
};
hashAndAssign(data.report.counters);
hashAndAssign(data.report.rates);
for (const [name, values] of Object.entries(data.report.summaries || {})) {
for (const [aggregation, value] of Object.entries(values)) {
vars[`${name}.${aggregation}`] = {
value,
hash: hashString(`${name}.${aggregation}`)
};
}
}
return vars;
}
static runChecks(checks, vars) {
const LEGACY_CONDITIONS = ['min', 'max', 'median', 'p95', 'p99'];
const checkTests = [];
if (Array.isArray(checks.thresholds)) {
checks.thresholds.forEach((o) => {
if (typeof o === 'object') {
const metricName = Object.keys(o)[0]; // only one metric check per array entry
const maxValue = o[metricName];
const expr = `${metricName} < ${maxValue}`;
const hashedExpression = replaceMetricsWithHashes(
Object.keys(vars),
expr
);
let f: any = () => {};
try {
f = filtrex(hashedExpression);
} catch (err) {
global.artillery.log(err);
}
// all threshold checks are strict:
checkTests.push({
f,
strict: true,
original: expr,
hashed: hashedExpression
});
}
});
}
if (Array.isArray(checks.conditions)) {
checks.conditions.forEach((o) => {
if (typeof o === 'object') {
const expression = o.expression;
const strict = typeof o.strict === 'boolean' ? o.strict : true;
const hashedExpression = replaceMetricsWithHashes(
Object.keys(vars),
expression
);
let f: any = () => {};
try {
f = filtrex(hashedExpression);
} catch (err) {
global.artillery.log(err);
}
checkTests.push({
f,
strict,
original: expression,
hashed: hashedExpression
});
}
});
}
Object.keys(checks)
.filter((k) => LEGACY_CONDITIONS.indexOf(k) > -1)
.forEach((k) => {
const metricName = `http.response_time.${k}`;
const maxValue = parseInt(checks[k], 10);
const expression = `${metricName} < ${maxValue}`;
const hashedExpression = replaceMetricsWithHashes(
Object.keys(vars),
expression
);
let f: any = () => {};
try {
f = filtrex(hashedExpression);
} catch (err) {
global.artillery.log(err);
}
// all legacy threshold checks are strict:
checkTests.push({
f,
strict: true,
original: `${k} < ${maxValue}`,
hash: hashedExpression
});
});
if (typeof checks.maxErrorRate !== 'undefined') {
const maxValue = Number(checks.maxErrorRate);
const expression = `((vusers.created - vusers.completed)/vusers.created * 100) <= ${maxValue}`;
const hashedExpression = replaceMetricsWithHashes(
Object.keys(vars),
expression
);
let f: any = () => {};
try {
f = filtrex(hashedExpression);
} catch (err) {
global.artillery.log(err);
}
checkTests.push({
f,
strict: true,
original: `maxErrorRate < ${maxValue}`,
hash: hashedExpression
});
}
if (checkTests.length > 0) {
global.artillery.log('\nChecks:');
}
const hashedVarsMap = getHashedVarToValueMap(vars);
checkTests.forEach((check) => {
const result = check.f(hashedVarsMap);
check.result = result;
debug(`check ${check.original} -> ${result}`);
});
return checkTests;
}
}
export { EnsurePlugin as Plugin };