Skip to content

Commit

Permalink
feat(cli): set nontemplated url as req name by default in metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
bernardobridge committed Jul 11, 2024
1 parent a231b3e commit bff2895
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 24 deletions.
47 changes: 35 additions & 12 deletions packages/artillery-plugin-metrics-by-endpoint/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* 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/. */

const url = require('url');

module.exports = { Plugin: MetricsByEndpoint };
Expand All @@ -12,6 +11,7 @@ let useOnlyRequestNames;
let stripQueryString;
let ignoreUnnamedRequests;
let metricsPrefix;
let useDefaultName;

// NOTE: Will not work with `parallel` - need request UIDs for that
function MetricsByEndpoint(script, events) {
Expand Down Expand Up @@ -43,20 +43,26 @@ function MetricsByEndpoint(script, events) {
metricsPrefix =
script.config.plugins['metrics-by-endpoint'].metricsNamespace ||
'plugins.metrics-by-endpoint';
useDefaultName =
script.config.plugins['metrics-by-endpoint'].useDefaultName ?? true;

script.config.processor.metricsByEndpoint_afterResponse =
metricsByEndpoint_afterResponse;
script.config.processor.metricsByEndpoint_onError = metricsByEndpoint_onError;
script.config.processor.metricsByEndpoint_beforeRequest =
metricsByEndpoint_beforeRequest;

script.scenarios.forEach(function (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 getReqName(target, originalRequestUrl, requestName) {
function calculateBaseUrl(target, originalRequestUrl) {
const targetUrl = target && url.parse(target);
const requestUrl = url.parse(originalRequestUrl);

Expand All @@ -73,20 +79,33 @@ function getReqName(target, originalRequestUrl, requestName) {
}
baseUrl += stripQueryString ? requestUrl.pathname : requestUrl.path;

let reqName = '';
if (useOnlyRequestNames && requestName) {
reqName += requestName;
} else if (requestName) {
reqName += `${baseUrl} (${requestName})`;
} else if (!ignoreUnnamedRequests) {
reqName += baseUrl;
return decodeURIComponent(baseUrl);
}

function getReqName(target, originalRequestUrl, requestName) {
const baseUrl = calculateBaseUrl(target, originalRequestUrl);

if (!requestName) {
return ignoreUnnamedRequests ? '' : baseUrl;
}

return reqName;
return useOnlyRequestNames ? requestName : `${baseUrl} (${requestName})`;
}

function metricsByEndpoint_beforeRequest(req, userContext, events, done) {
if (useDefaultName) {
req.defaultName = getReqName(userContext.vars.target, req.url, req.name);
}

return done();
}

function metricsByEndpoint_onError(err, req, userContext, events, done) {
const reqName = getReqName(userContext.vars.target, req.url, req.name);
//if useDefaultName 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 = useDefaultName
? req.defaultName
: getReqName(userContext.vars.target, req.url, req.name);

if (reqName === '') {
return done();
Expand All @@ -102,7 +121,11 @@ function metricsByEndpoint_onError(err, req, userContext, events, done) {
}

function metricsByEndpoint_afterResponse(req, res, userContext, events, done) {
const reqName = getReqName(userContext.vars.target, req.url, req.name);
//if useDefaultName 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 = useDefaultName
? req.defaultName
: getReqName(userContext.vars.target, req.url, req.name);

if (reqName === '') {
return done();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
config:
target: http://asciiart.artillery.io:8080
phases:
- duration: 2
arrivalRate: 2
plugins:
metrics-by-endpoint:
stripQueryString: true

scenarios:
- flow:
- get:
url: "/dino/{{ $randomString() }}?potato=1&tomato=2"
name: "GET /dino"
- get:
url: "/armadillo/{{ $randomString() }}"
- get:
url: "/pony"
104 changes: 104 additions & 0 deletions packages/artillery-plugin-metrics-by-endpoint/test/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,107 @@ test("Reports correctly when 'parallel' is used", async (t) => {
);
}
});

test('Reports correctly when `useDefaultName` is set to true (default)', async (t) => {
const reportPath =
os.tmpdir() + '/artillery-plugin-metrics-by-endpoint-use-path-as-name.json';
const output =
await $`../artillery/bin/run run ./test/fixtures/scenario-templated-url.yml -o ${reportPath}`;

const report = require(reportPath);

t.equal(output.exitCode, 0, 'CLI Exit Code should be 0');
t.equal(
report.aggregate.counters[
'plugins.metrics-by-endpoint./armadillo/{{ $randomString() }}.codes.200'
],
4,
'should have counter metrics including templated url and no query strings'
);
t.equal(
report.aggregate.counters[
'plugins.metrics-by-endpoint./dino/{{ $randomString() }} (GET /dino).codes.200'
],
4,
'should have counter metrics including templated url with request name specified'
);
t.equal(
report.aggregate.counters['plugins.metrics-by-endpoint./pony.codes.200'],
4
),
'should display counter metrics for /pony as normal';

t.ok(
Object.keys(report.aggregate.summaries).includes(
'plugins.metrics-by-endpoint.response_time./armadillo/{{ $randomString() }}'
),
'should have summary metrics including templated url'
);
t.ok(
Object.keys(report.aggregate.summaries).includes(
'plugins.metrics-by-endpoint.response_time./dino/{{ $randomString() }} (GET /dino)'
),
'should have summary metrics including templated url with request name specified'
);
t.ok(
Object.keys(report.aggregate.summaries).includes(
'plugins.metrics-by-endpoint.response_time./pony'
),
'should display summary metrics for /pony as normal'
);
});

test('Reports correctly when `useDefaultName` is explicitly set to false', async (t) => {
const reportPath =
os.tmpdir() +
'/artillery-plugin-metrics-by-endpoint-use-path-without-name-test.json';
const overrides = {
config: {
plugins: {
'metrics-by-endpoint': {
useDefaultName: false,
stripQueryString: false
}
}
}
};
const output =
await $`../artillery/bin/run run ./test/fixtures/scenario-templated-url.yml -o ${reportPath} --overrides ${JSON.stringify(
overrides
)}`;

const report = require(reportPath);

t.equal(output.exitCode, 0, 'CLI Exit Code should be 0');

const aggregateCounters = Object.keys(report.aggregate.counters);

const countersWithName = aggregateCounters.filter((counter) => {
return new RegExp(
/plugins\.metrics-by-endpoint\.\/dino\/[a-zA-Z0-9]+\.?\w+\?potato=1&tomato=2 \(GET \/dino\)\.codes\.200/
).test(counter);
});

const countersWithoutName = aggregateCounters.filter((counter) => {
return new RegExp(
/plugins\.metrics-by-endpoint\.\/armadillo\/[a-zA-Z0-9]+\.?\w+\.codes\.200/
).test(counter);
});

const regularPonyCounter = aggregateCounters.filter(
(counter) => counter == 'plugins.metrics-by-endpoint./pony.codes.200'
);

t.ok(
countersWithName.length > 0,
`should have counter metrics without the templated url, got ${countersWithName}`
);
t.ok(
countersWithoutName.length > 0,
`should have counter metrics without the templated url and request name specified, got ${countersWithoutName}`
);
t.ok(
regularPonyCounter.length == 1,
`should display counter metrics for /pony as normal, got ${regularPonyCounter}`
);
});
Loading

0 comments on commit bff2895

Please sign in to comment.