Node.js client module for easy load testing / benchmarking REST (HTTP/HTTPS) API's using a simple structure/DSL can create REST flows with setup and teardown and returns (measured) metrics.
## Programmatic Usage
Simple flow performing 100 iterations with 10 concurrent connections
var benchrest = require('bench-rest');
var flow = 'http://localhost:8000/';
var flow = {
main: [
{ put: 'http://localhost:8000/foo_#{INDEX}', json: 'mydata_#{INDEX}' },
{ get: 'http://localhost:8000/foo_#{INDEX}' }
]
};
module.exports = flow;
var runOptions = {
limit: 10,
iterations: 100
};
benchrest(flow, runOptions)
.on('error', function (err, ctxName) { console.error('Failed in %s with err: ', ctxName, err); })
.on('end', function (stats, errorCount) {
console.log('error count: ', errorCount);
console.log('stats', stats);
});
## Command-line usage
bench-rest
node_modules/.bin/bench-rest
Outputs
Usage: bench-rest [options] <flow-js-path-or-GET-URL>
Options:
-h, --help output usage information
-V, --version output the version number
-n --iterations <integer> Number of iterations to run, defaults to 1
-a --prealloc <integer> Max iterations to preallocate, defaults 100000
-c --concurrency <integer> Concurrent operations, defaults to 10
-d --progress <integer> Display progress bar (> 0), update every N ms, defaults 1000
-u --user <username> User for basic authentication, default no auth
-p --password <password> Password for basic authentication
-e --evaluate <flow-string> Evaluate flow from string, not file
Examples:
bench-rest -n 100 -c 100 ./examples/simple.js
bench-rest -n 100 -c 100 -u "joe" -p "secret" /foo/flow.js
bench-rest -n 10 -c 2 http://localhost:8000/
bench-rest -n 10 -c 2 -e "{ head: 'http://localhost:8000/' }"
Running this
bench-rest -n 1000 -c 50 ./examples/simple.js
would output
Benchmarking 1000 iteration(s) using up to 50 concurrent connections
Using flow from: /Users/barczewskij/projects/bench-rest/examples/simple.js
{ main: [ { get: 'http://localhost:8000/' } ] }
Progress [=======================================] 100% 0.0s conc:49 1341/s
errors: 0
stats: { totalElapsed: 894,
main:
{ meter:
{ mean: 1240.6947890818858,
count: 1000,
currentRate: 1240.6947890818858,
'1MinuteRate': 0,
'5MinuteRate': 0,
'15MinuteRate': 0 },
histogram:
{ min: 4,
max: 89,
sum: 41603,
variance: 242.0954864864864,
mean: 41.603,
stddev: 15.55941793533699,
count: 1000,
median: 42,
p75: 50,
p95: 70.94999999999993,
p99: 81.99000000000001,
p999: 88.99900000000002 } } }
It has one expected required parameter which is the path to a node.js
file which exports a REST flow. For example:
var flow = {
main: [{ get: 'http://localhost:8000/' }]
};
module.exports = flow;
Check for example flows in the examples
directory.
## Detailed Usage
Advanced flow with setup/teardown and multiple steps to benchmark in each iteration
var benchrest = require('bench-rest');
var flow = {
before: [],
beforeMain: [],
main: [
{ put: 'http://localhost:8000/foo_#{INDEX}', json: 'mydata_#{INDEX}' },
{ get: 'http://localhost:8000/foo_#{INDEX}' }
],
afterMain: [{ del: 'http://localhost:8000/foo_#{INDEX}' }],
after: []
};
module.exports = flow;
var runOptions = {
limit: 10,
iterations: 1000,
prealloc: 100
};
var errors = [];
benchrest(flow, runOptions)
.on('error', function (err, ctxName) { console.error('Failed in %s with err: ', ctxName, err); })
.on('progress', function (stats, percent, concurrent, ips) {
console.log('Progress: %s complete', percent);
})
.on('end', function (stats, errorCount) {
console.log('error count: ', errorCount);
console.log('stats', stats);
});
### REST Operations in the flow
The REST operations that need to be performed in either as part of the main flow or for setup and teardown are configured using the following flow properties.
Each array of opertions will be performed in series one after another unless an error is hit. The afterMain and after operations will be performed regardless of any errors encountered in the flow.
var flow = {
before: [],
beforeMain: [],
main: [],
afterMain: [],
after: []
};
Each operation can have the following properties:
### Token substitution for iteration operations
To make REST flows that are independent of each other, one often wants unique URLs and unique data, so one way to make this easy is to include special tokens in the uri
, json
, or data
.
Currently the token(s) replaced in the uri
, json
, or body
are:
#{INDEX}
- replaced with the zero based counter/index of the iteration
Note: for the json
property the json
object is JSON.stringified, tokens substituted, then JSON.parsed back to an object so that tokens will be substituted anywhere in the structure. If subsitution is not needed (no #{INDEX}
in the structure, then no copy (stringify/parse) will be performed.
### Pre/post operation processing
If an array of hooks is specified in an operation as beforeHooks
and/or afterHooks
then these synchronous operations will be done before/after the REST operation.
Built-in processing filters can be referred to by name using a string, while custom filters can be provided as a function, ex:
{ head: 'http://localhost:8000', beforeHooks: ['useEtag'], afterHooks: ['ignoreStatus'] }
The list of current built-in beforeHooks:
useEtag
- if an etag had been previously saved for this URI with saveEtag
afterHook, then set the appropriate header (for GET/HEAD, If-None-Match
, otherwise If-Match
). If was not previously saved or empty then no header is set.
The list of current built-in afterHooks:
saveEtag
- afterHook which causes an etag to be saved into an object cache specific to this iteration. Stored by URI. If the etag was the result of a POST operation and a Location
header was provided, then the URI at the Location
will be used.ignoreStatus
- afterHookif an operation could possibly return an error code that you want to ignore and always continue anyway. Failing status codes are those that are greater than or equal to 400. Normal operation would be to terminate an iteration if there is a failure status code in any before
, beforeMain
, or main
operation.verify2XX
- afterHook which fails if an operation's status code was not in 200-299 range. If you don't want a redirect followed, be sure to add the request option followRedirect: false
. Note: by default errors are verified (greater than or equal to 400), so this would just be used when you want to make sure it is not a 3xx either.startStepTimer
- used in beforeHooks to start a timer for this step named step_OPIDX where OPIDX is the zero based index of the step in the flow. Be sure to call endStepTimer
in afterHooks to end it. Provides detailed stats for an individual step in a flow.endStepTimer
- used in afterHooks to end a timer previously started with startStepTimer
and included in the stats displayed at the end of the run.
To create custom beforeHook or afterHook the synchronous function needs to accept an all
object and return the same or possibly modified object. To exit the flow, an exception can be thrown which will be caught and emitted. Using these beforeHooks you can modify the next request, and using the afterHooks can verify the response and/or store data for future actions.
One way to keep state for each iteration (without using external variables) is to use the all.iterCtx object which is an empty object provided for each iteration. See examples/hook.js
and test/hooks-iter-ctx.mocha.js
So a verification function could be written as such
function verifyData(all) {
if (all.err) return all;
assert.equal(all.response.statusCode, 200);
assert(all.body, 'foobarbaz');
return all;
}
Postprocess function example:
function postProcess(all) {
all.iterCtx.location = all.response.headers.location;
all.iterCtx.body = all.body;
return all;
}
Preprocess function example:
function preProcess(all) {
all.requestOptions.uri = 'http://localhost:8000' + all.iterCtx.location;
return all;
}
The properties available on the all
object are:
- all.env.index - the zero based counter for this iteration, same as what is used for #{INDEX}
- all.env.jar - the cookie jar
- all.env.user - basic auth user if provided
- all.env.password - basic auth password if provided
- all.env.etags - object of etags saved by URI
- all.env.stats - measured stats collection containing
totalElapsed
and main
. If startStepTimer
and endStepTimer
hooks are added to individual steps then additional timers step_OPINDEX will be created for steps that have the hooks. - all.iterCtx - empty object created for each iteration, can be used for your private storage from beforeHooks and afterHooks
- all.opIndex - zero based index for the operation in the array of operations, ie: first operation in the main flow will have opIndex of 0
- all.requestOptions - the options that will be used for the request (see mikeal/request)
- all.requestOptions.uri - the URL that will be used for the request
- all.requestOptions.method - the method that will be used for the request
- all.response - the response obj (only for afterHooks)
- all.body - the response body (only for afterHooks)
- all.err - not empty if an error has occurred
- all.cb - the cb that will be called when done
## Why create this project?
It is important to understand how well your architecture performs and with each change to the system how performance is impacted. The best way to know this is to benchmark your system with each major change.
Benchmarking also lets you:
- understand how your system will act under load
- how and whether multiple servers or processes will help you scale
- whether a feature added improved or hurt performance
- predict the need add instances or throttle load before your server reaches overload
After attempting to use the variety of load testing clients and modules for benchmarking, none really met all of my desired goals. Most clients are only able to benchmark a single operation, not a whole flow and not one with setup and teardown.
Building your own is certainly an option but it gets tedious to make all the necessary setup and error handling to achieve a simple flow and thus this project was born.