UNPKG

artillery

Version:

Cloud-scale load testing. https://www.artillery.io

173 lines (145 loc) 5.5 kB
/* 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; } }