artillery
Version:
Cloud-scale load testing. https://www.artillery.io
173 lines (145 loc) • 5.5 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 url from 'node:url';
import createDebug from 'debug';
export { MetricsByEndpoint as Plugin };
const debug = createDebug('plugin:metrics-by-endpoint');
let useOnlyRequestNames;
let stripQueryString;
let ignoreUnnamedRequests;
let metricsPrefix;
let groupDynamicURLs;
// NOTE: Will not work with `parallel` - need request UIDs for that
function MetricsByEndpoint(script, _events) {
// if(!global.artillery || !global.artillery.log) {
// console.error('artillery-plugin-metrics-endpoint requires Artillery v2');
// return;
// }
// If running in Artillery v2, the plugin should only load in workers
if (
global.artillery &&
Number(global.artillery.version.slice(0, 1)) > 1 &&
typeof process.env.LOCAL_WORKER_ID === 'undefined'
) {
debug('Not running in a worker, exiting');
return;
}
if (!script.config.processor) {
script.config.processor = {};
}
// In the main thread config.processor may still be an unresolved
// path (a string). Attaching functions to it was a silent no-op
// under sloppy mode; ES modules are strict, so guard explicitly.
// Workers load the processor into an object before plugins run.
const canAttach = typeof script.config.processor === 'object';
useOnlyRequestNames =
script.config.plugins['metrics-by-endpoint'].useOnlyRequestNames || false;
stripQueryString =
script.config.plugins['metrics-by-endpoint'].stripQueryString || false;
ignoreUnnamedRequests =
script.config.plugins['metrics-by-endpoint'].ignoreUnnamedRequests || false;
metricsPrefix =
script.config.plugins['metrics-by-endpoint'].metricsNamespace ||
'plugins.metrics-by-endpoint';
groupDynamicURLs =
script.config.plugins['metrics-by-endpoint'].groupDynamicURLs ?? true;
if (canAttach) {
script.config.processor.metricsByEndpoint_afterResponse =
metricsByEndpoint_afterResponse;
script.config.processor.metricsByEndpoint_onError =
metricsByEndpoint_onError;
script.config.processor.metricsByEndpoint_beforeRequest =
metricsByEndpoint_beforeRequest;
}
script.scenarios.forEach((scenario) => {
scenario.afterResponse = [].concat(scenario.afterResponse || []);
scenario.afterResponse.push('metricsByEndpoint_afterResponse');
scenario.onError = [].concat(scenario.onError || []);
scenario.onError.push('metricsByEndpoint_onError');
scenario.beforeRequest = [].concat(scenario.beforeRequest || []);
scenario.beforeRequest.push('metricsByEndpoint_beforeRequest');
});
}
function calculateBaseUrl(target, originalRequestUrl) {
const targetUrl = target && url.parse(target);
const requestUrl = url.parse(originalRequestUrl);
let baseUrl = '';
if (
targetUrl &&
requestUrl.hostname &&
targetUrl.hostname !== requestUrl.hostname
) {
baseUrl += requestUrl.hostname;
}
if (targetUrl && requestUrl.port && targetUrl.port !== requestUrl.port) {
baseUrl += `:${requestUrl.port}`;
}
baseUrl += stripQueryString ? requestUrl.pathname : requestUrl.path;
return decodeURIComponent(baseUrl);
}
function getReqName(target, originalRequestUrl, requestName) {
const baseUrl = calculateBaseUrl(target, originalRequestUrl);
if (!requestName) {
return ignoreUnnamedRequests ? '' : baseUrl;
}
return useOnlyRequestNames ? requestName : `${baseUrl} (${requestName})`;
}
function metricsByEndpoint_beforeRequest(req, userContext, _events, done) {
if (groupDynamicURLs) {
req.defaultName = getReqName(userContext.vars.target, req.url, req.name);
}
return done();
}
function metricsByEndpoint_onError(err, req, userContext, events, done) {
//if groupDynamicURLs is true, then req.defaultName is set in beforeRequest
//otherwise, we must calculate the reqName here as req.url is the non-templated version
const reqName = groupDynamicURLs
? req.defaultName
: getReqName(userContext.vars.target, req.url, req.name);
if (reqName === '') {
return done();
}
events.emit(
'counter',
`${metricsPrefix}.${reqName}.errors.${err.code || err.name}`,
1
);
done();
}
function metricsByEndpoint_afterResponse(req, res, userContext, events, done) {
//if groupDynamicURLs is true, then req.defaultName is set in beforeRequest
//otherwise, we must calculate the reqName here as req.url is the non-templated version
const reqName = groupDynamicURLs
? req.defaultName
: getReqName(userContext.vars.target, req.url, req.name);
if (reqName === '') {
return done();
}
const histoName = `${metricsPrefix}.response_time.${reqName}`;
if (res.headers['server-timing']) {
const timing = getServerTimingTotal(res.headers['server-timing']);
events.emit(
'histogram',
`${metricsPrefix}.server-timing.${reqName}`,
timing
);
}
events.emit(
'counter',
`${metricsPrefix}.${reqName}.codes.${res.statusCode}`,
1
);
events.emit('histogram', histoName, res.timings.phases.firstByte);
return done();
}
function getServerTimingTotal(s) {
const matches = s.match(/total;dur=[0-9.]+/gi);
if (matches !== null && matches.length > 0) {
// we always grab the first instance of "total" if there's more than one
return Number(matches[0].split('=')[1] || 0);
} else {
// no match
return -1;
}
}