From 0ea08b7a230818ef69631160c672af91389c0c2d Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 10 Mar 2021 12:05:35 -0500 Subject: [PATCH] Adding individual status code statistics table (#347) * Adding individual status code statistics table * Adding unit test for status code statistics table * Adding unit test for status code statistics table * Adding unit test for status code statistics table --- autocannon.js | 5 +- help.txt | 2 + lib/aggregateResult.js | 9 ++++ lib/printResult.js | 18 +++++++ lib/run.js | 10 ++++ test/fixtures/example-result.json | 10 +++- test/printResult-process.js | 10 +++- test/printResult-renderStatusCodes.test.js | 61 ++++++++++++++++++++++ 8 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 test/printResult-renderStatusCodes.test.js diff --git a/autocannon.js b/autocannon.js index 5b348cd2..44be8f41 100755 --- a/autocannon.js +++ b/autocannon.js @@ -33,7 +33,7 @@ module.exports.parseArguments = parseArguments function parseArguments (argvs) { const argv = minimist(argvs, { - boolean: ['json', 'n', 'help', 'renderLatencyTable', 'renderProgressBar', 'forever', 'idReplacement', 'excludeErrorStats', 'onPort', 'debug', 'ignoreCoordinatedOmission'], + boolean: ['json', 'n', 'help', 'renderLatencyTable', 'renderProgressBar', 'renderStatusCodes', 'forever', 'idReplacement', 'excludeErrorStats', 'onPort', 'debug', 'ignoreCoordinatedOmission'], alias: { connections: 'c', pipelining: 'p', @@ -57,6 +57,7 @@ function parseArguments (argvs) { ignoreCoordinatedOmission: 'C', reconnectRate: 'D', renderProgressBar: 'progress', + renderStatusCodes: 'statusCodes', title: 'T', version: 'v', forever: 'f', @@ -75,6 +76,7 @@ function parseArguments (argvs) { reconnectRate: 0, renderLatencyTable: false, renderProgressBar: true, + renderStatusCodes: false, json: false, forever: false, method: 'GET', @@ -96,6 +98,7 @@ function parseArguments (argvs) { if (argv.n) { argv.renderProgressBar = false argv.renderResultsTable = false + argv.renderStatusCodes = false } if (argv.version) { diff --git a/help.txt b/help.txt index 1b474e0f..e4fab218 100644 --- a/help.txt +++ b/help.txt @@ -82,6 +82,8 @@ Available options: -E/--expectBody EXPECTED Ensure the body matches this value. If enabled, mismatches count towards bailout. Enabling this option will slow down the load testing. + --renderStatusCodes + Print status codes and their respective statistics. --debug Print connection errors to stderr. -v/--version diff --git a/lib/aggregateResult.js b/lib/aggregateResult.js index aafda989..b6976f05 100644 --- a/lib/aggregateResult.js +++ b/lib/aggregateResult.js @@ -28,6 +28,14 @@ function aggregateResult (results, opts, histograms) { acc['4xx'] += r['4xx'] acc['5xx'] += r['5xx'] + Object.keys(r.statusCodeStats).forEach(statusCode => { + if (!acc.statusCodeStats[statusCode]) { + acc.statusCodeStats[statusCode] = r.statusCodeStats[statusCode] + } else { + acc.statusCodeStats[statusCode].count += r.statusCodeStats[statusCode].count + } + }) + return acc }) @@ -52,6 +60,7 @@ function aggregateResult (results, opts, histograms) { '3xx': aggregated['3xx'], '4xx': aggregated['4xx'], '5xx': aggregated['5xx'], + statusCodeStats: aggregated.statusCodeStats, latency: addPercentiles(aggregated.latencies, histAsObj(aggregated.latencies)), requests: addPercentiles(histograms.requests, histAsObj(histograms.requests, aggregated.totalCompletedRequests)), diff --git a/lib/printResult.js b/lib/printResult.js index 79cf07c6..50e03ade 100644 --- a/lib/printResult.js +++ b/lib/printResult.js @@ -33,6 +33,19 @@ const printResult = (result, opts) => { requests.push(asHighRow(chalk.bold('Req/Sec'), result.requests)) requests.push(asHighRow(chalk.bold('Bytes/Sec'), asBytes(result.throughput))) logToLocalStr(requests.toString()) + + if (opts.renderStatusCodes === true) { + const statusCodeStats = new Table({ + head: asColor(chalk.cyan, ['Code', 'Count']) + }) + Object.keys(result.statusCodeStats).forEach(statusCode => { + const stats = result.statusCodeStats[statusCode] + const colorize = colorizeByStatusCode(chalk, statusCode) + statusCodeStats.push([colorize(statusCode), stats.count]) + }) + logToLocalStr(statusCodeStats.toString()) + } + logToLocalStr('') logToLocalStr('Req/Bytes counts sampled once per second.\n') @@ -131,4 +144,9 @@ function asBytes (stat) { return result } +function colorizeByStatusCode (chalk, statusCode) { + const codeClass = Math.floor(parseInt(statusCode) / 100) - 1 + return [chalk.cyan, chalk.cyan, chalk.cyan, chalk.redBright, chalk.redBright][codeClass] +} + module.exports = printResult diff --git a/lib/run.js b/lib/run.js index 97cde3e0..780fd2e2 100644 --- a/lib/run.js +++ b/lib/run.js @@ -28,6 +28,8 @@ function run (opts, tracker, cb) { 0 // 5xx ] + const statusCodeStats = {} + if (opts.overallRate && (opts.overallRate < opts.connections)) opts.connections = opts.overallRate let counter = 0 @@ -116,6 +118,7 @@ function run (opts, tracker, cb) { timeouts: timeouts, mismatches: mismatches, non2xx: statusCodes[0] + statusCodes[2] + statusCodes[3] + statusCodes[4], + statusCodeStats, resets: resets, duration: Math.round((Date.now() - startTime) / 10) / 100, start: new Date(startTime), @@ -216,6 +219,13 @@ function run (opts, tracker, cb) { tracker.emit('response', this, statusCode, resBytes, responseTime) const codeIndex = Math.floor(parseInt(statusCode) / 100) - 1 statusCodes[codeIndex] += 1 + + if (!statusCodeStats[statusCode]) { + statusCodeStats[statusCode] = { count: 1 } + } else { + statusCodeStats[statusCode].count++ + } + // only recordValue 2xx latencies if (codeIndex === 1 || includeErrorStats) { if (rate && !opts.ignoreCoordinatedOmission) { diff --git a/test/fixtures/example-result.json b/test/fixtures/example-result.json index 28a0e01a..17c87fda 100644 --- a/test/fixtures/example-result.json +++ b/test/fixtures/example-result.json @@ -85,6 +85,12 @@ "2xx": 500, "3xx": 0, "4xx": 0, - "5xx": 0 + "5xx": 0, + "statusCodeStats": { + "200": { "count": "500" }, + "302": { "count": "0" }, + "401": { "count": "0" }, + "403": { "count": "0" } + } } - \ No newline at end of file + diff --git a/test/printResult-process.js b/test/printResult-process.js index cac16d3d..67fd56a4 100644 --- a/test/printResult-process.js +++ b/test/printResult-process.js @@ -2,6 +2,14 @@ const autocannon = require('../autocannon') const exampleResult = require('./fixtures/example-result.json') +const crossArgv = require('cross-argv') -const resultStr = autocannon.printResult(exampleResult) +let opts = null + +if (process.argv.length > 2) { + const args = crossArgv(process.argv.slice(2)) + opts = autocannon.parseArguments(args) +} + +const resultStr = autocannon.printResult(exampleResult, opts) process.stderr.write(resultStr) diff --git a/test/printResult-renderStatusCodes.test.js b/test/printResult-renderStatusCodes.test.js new file mode 100644 index 00000000..b8303292 --- /dev/null +++ b/test/printResult-renderStatusCodes.test.js @@ -0,0 +1,61 @@ +'use strict' + +const test = require('tap').test +const split = require('split2') +const path = require('path') +const childProcess = require('child_process') + +test('should stdout (print) the result', (t) => { + const lines = [ + /.*/, + /Stat.*2\.5%.*50%.*97\.5%.*99%.*Avg.*Stdev.*Max.*$/, + /.*/, + /Latency.*$/, + /$/, + /.*/, + /Stat.*1%.*2\.5%.*50%.*97\.5%.*Avg.*Stdev.*Min.*$/, + /.*/, + /Req\/Sec.*$/, + /.*/, + /Bytes\/Sec.*$/, + /.*/, + /.*/, + /Code.*Count.*$/, + /.*/, + /200.*500.*$/, + /.*/, + /302.*0.*$/, + /.*/, + /401.*0.*$/, + /.*/, + /403.*0.*$/, + /.*/, + /$/, + /Req\/Bytes counts sampled once per second.*$/, + /$/, + /.* requests in ([0-9]|\.)+s, .* read/ + ] + + t.plan(lines.length * 2) + + const child = childProcess.spawn(process.execPath, [path.join(__dirname, 'printResult-process.js'), '--renderStatusCodes', 'http://127.0.0.1'], { + cwd: __dirname, + env: process.env, + stdio: ['ignore', 'pipe', 'pipe'], + detached: false + }) + + t.tearDown(() => { + child.kill() + }) + + child + .stderr + .pipe(split()) + .on('data', (line) => { + const regexp = lines.shift() + t.ok(regexp, 'we are expecting this line') + t.ok(regexp.test(line), 'line matches ' + regexp) + }) + .on('end', t.end) +})