/* 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 };