loadtest 6.3.2 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -384
- package/bin/loadtest.js +2 -1
- package/doc/api.md +366 -0
- package/doc/status-callback.md +105 -0
- package/lib/httpClient.js +68 -36
- package/lib/loadtest.js +26 -40
- package/lib/options.js +1 -0
- package/lib/testserver.js +7 -3
- package/package.json +1 -1
- package/test/integration.js +51 -1
package/README.md
CHANGED
|
@@ -18,17 +18,9 @@ On Ubuntu or Mac OS X systems install using sudo:
|
|
|
18
18
|
|
|
19
19
|
$ sudo npm install -g loadtest
|
|
20
20
|
|
|
21
|
-
For access to the API just
|
|
21
|
+
For access to the API just install it in your `npm` package as a dev dependency:
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
{
|
|
25
|
-
...
|
|
26
|
-
"devDependencies": {
|
|
27
|
-
"loadtest": "*"
|
|
28
|
-
},
|
|
29
|
-
...
|
|
30
|
-
}
|
|
31
|
-
```
|
|
23
|
+
$ npm install --save-dev loadtest
|
|
32
24
|
|
|
33
25
|
### Compatibility
|
|
34
26
|
|
|
@@ -88,24 +80,32 @@ so that you can abort deployment e.g. if 99% of the requests don't finish in 10
|
|
|
88
80
|
|
|
89
81
|
### Usage Don'ts
|
|
90
82
|
|
|
91
|
-
`loadtest`
|
|
92
|
-
|
|
93
|
-
|
|
83
|
+
`loadtest` performance has improved significantly,
|
|
84
|
+
but it is still limited.
|
|
85
|
+
`loadtest` saturates a single CPU pretty quickly,
|
|
86
|
+
so it uses half the available cores in your processor.
|
|
87
|
+
If you see that the Node.js processes are above 100% usage in `top`,
|
|
88
|
+
which happens approx. when your load is above 4000~5000 rps per core,
|
|
89
|
+
please adjust the number of cores.
|
|
90
|
+
So for instance with eight cores you can expect to get a maximum performance of
|
|
91
|
+
8 * 5000 ~ 40 krps.
|
|
94
92
|
(You can measure the practical limits of `loadtest` on your specific test machines by running it against a simple
|
|
95
93
|
[test server](#test-server)
|
|
96
94
|
and seeing when it reaches 100% CPU.)
|
|
97
|
-
In this case try using in multi-process mode using the `--cores` parameter,
|
|
98
|
-
see below.
|
|
99
95
|
|
|
100
|
-
|
|
96
|
+
If you have reached the limits of `loadtest` even after using all cores,
|
|
97
|
+
there are other tools that you can try.
|
|
101
98
|
|
|
99
|
+
* [AutoCannon](https://www.npmjs.com/package/autocannon): also an `npm` package,
|
|
100
|
+
awesome tool with an interface similar to `wrk`.
|
|
102
101
|
* [Apache `ab`](http://httpd.apache.org/docs/2.2/programs/ab.html)
|
|
103
102
|
has great performance, but it is also limited by a single CPU performance.
|
|
104
103
|
Its practical limit is somewhere around ~40 krps.
|
|
105
104
|
* [weighttp](http://redmine.lighttpd.net/projects/weighttp/wiki) is also `ab`-compatible
|
|
106
105
|
and is supposed to be very fast (the author has not personally used it).
|
|
107
|
-
* [wrk](https://github.com/wg/wrk) is multithreaded and
|
|
106
|
+
* [wrk](https://github.com/wg/wrk) is multithreaded and highly performance.
|
|
108
107
|
It may need installing from source though, and its interface is not `ab`-compatible.
|
|
108
|
+
* [wrk2](https://github.com/giltene/wrk2): evolution of `wrk`.
|
|
109
109
|
|
|
110
110
|
### Regular Usage
|
|
111
111
|
|
|
@@ -239,12 +239,12 @@ to provide the body of each request.
|
|
|
239
239
|
This is useful if you want to generate request bodies dynamically and vary them for each request.
|
|
240
240
|
For examples see above for `-p`.
|
|
241
241
|
|
|
242
|
-
##### `-r`
|
|
242
|
+
##### `-r recover`
|
|
243
243
|
|
|
244
244
|
Recover from errors. Always active: loadtest does not stop on errors.
|
|
245
245
|
After the tests are finished, if there were errors a report with all error codes will be shown.
|
|
246
246
|
|
|
247
|
-
#### `-s`
|
|
247
|
+
#### `-s secureProtocol`
|
|
248
248
|
|
|
249
249
|
The TLS/SSL method to use. (e.g. TLSv1_method)
|
|
250
250
|
|
|
@@ -252,7 +252,7 @@ Example:
|
|
|
252
252
|
|
|
253
253
|
$ loadtest -n 1000 -s TLSv1_method https://www.example.com
|
|
254
254
|
|
|
255
|
-
#### `-V`
|
|
255
|
+
#### `-V version`
|
|
256
256
|
|
|
257
257
|
Show version number and exit.
|
|
258
258
|
|
|
@@ -283,15 +283,19 @@ Note: --rps is not supported for websockets.
|
|
|
283
283
|
#### `--cores number`
|
|
284
284
|
|
|
285
285
|
Start `loadtest` in multi-process mode on a number of cores simultaneously.
|
|
286
|
-
Useful when a single CPU is saturated.
|
|
287
286
|
Forks the requested number of processes using the
|
|
288
287
|
[Node.js cluster module](https://nodejs.org/api/cluster.html).
|
|
288
|
+
Default: half the available CPUs on the machine.
|
|
289
289
|
|
|
290
|
-
|
|
291
|
-
The result
|
|
290
|
+
The total number of requests and the rps rate are shared among all processes.
|
|
291
|
+
The result shown is the aggregation of results from all cores.
|
|
292
292
|
|
|
293
293
|
Note: this option is not available in the API,
|
|
294
|
-
|
|
294
|
+
since it runs just within the calling process.
|
|
295
|
+
|
|
296
|
+
**Warning**: the default value for `--cores` has changed in version 7+,
|
|
297
|
+
from 1 to half the available CPUs on the machine.
|
|
298
|
+
Set to 1 to get the previous single-process mode.
|
|
295
299
|
|
|
296
300
|
#### `--timeout milliseconds`
|
|
297
301
|
|
|
@@ -496,352 +500,51 @@ However, it you try to push it beyond that, at 3 krps it will fail miserably.
|
|
|
496
500
|
|
|
497
501
|
`loadtest` is not limited to running from the command line; it can be controlled using an API,
|
|
498
502
|
thus allowing you to load test your application in your own tests.
|
|
503
|
+
A short introduction follows; see [complete docs for API](doc/api.md).
|
|
499
504
|
|
|
500
505
|
### Invoke Load Test
|
|
501
506
|
|
|
502
|
-
To run a load test,
|
|
507
|
+
To run a load test, invoke the exported function `loadTest()` with the desired options:
|
|
503
508
|
|
|
504
509
|
```javascript
|
|
505
510
|
import {loadTest} from 'loadtest'
|
|
506
511
|
|
|
507
512
|
const options = {
|
|
508
|
-
|
|
509
|
-
|
|
513
|
+
url: 'http://localhost:8000',
|
|
514
|
+
maxRequests: 1000,
|
|
510
515
|
}
|
|
511
516
|
const result = await loadTest(options)
|
|
512
517
|
result.show()
|
|
513
518
|
console.log('Tests run successfully')
|
|
514
|
-
})
|
|
515
|
-
```
|
|
516
|
-
|
|
517
|
-
The call returns a `Result` object that contains all info about the load test, also described below.
|
|
518
|
-
Call `result.show()` to display the results in the standard format on the console.
|
|
519
|
-
|
|
520
|
-
As a legacy from before promises existed,
|
|
521
|
-
if an optional callback is passed as second parameter then it will not behave as `async`:
|
|
522
|
-
the callback `function(error, result)` will be invoked when the max number of requests is reached,
|
|
523
|
-
or when the max number of seconds has elapsed.
|
|
524
|
-
|
|
525
|
-
```javascript
|
|
526
|
-
import {loadTest} from 'loadtest'
|
|
527
|
-
|
|
528
|
-
const options = {
|
|
529
|
-
url: 'http://localhost:8000',
|
|
530
|
-
maxRequests: 1000,
|
|
531
|
-
}
|
|
532
|
-
loadTest(options, function(error, result) {
|
|
533
|
-
if (error) {
|
|
534
|
-
return console.error('Got an error: %s', error)
|
|
535
|
-
}
|
|
536
|
-
result.show()
|
|
537
|
-
console.log('Tests run successfully')
|
|
538
|
-
})
|
|
539
|
-
```
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
Beware: if there are no `maxRequests` and no `maxSeconds`, then tests will run forever
|
|
543
|
-
and will not call the callback.
|
|
544
|
-
|
|
545
|
-
### Result
|
|
546
|
-
|
|
547
|
-
The latency result returned at the end of the load test contains a full set of data, including:
|
|
548
|
-
mean latency, number of errors and percentiles.
|
|
549
|
-
A simplified example follows:
|
|
550
|
-
|
|
551
|
-
```javascript
|
|
552
|
-
{
|
|
553
|
-
url: 'http://localhost:80/',
|
|
554
|
-
maxRequests: 1000,
|
|
555
|
-
maxSeconds: 0,
|
|
556
|
-
concurrency: 10,
|
|
557
|
-
agent: 'none',
|
|
558
|
-
requestsPerSecond: undefined,
|
|
559
|
-
totalRequests: 1000,
|
|
560
|
-
percentiles: {
|
|
561
|
-
'50': 7,
|
|
562
|
-
'90': 10,
|
|
563
|
-
'95': 11,
|
|
564
|
-
'99': 15
|
|
565
|
-
},
|
|
566
|
-
effectiveRps: 2824,
|
|
567
|
-
elapsedSeconds: 0.354108,
|
|
568
|
-
meanLatencyMs: 7.72,
|
|
569
|
-
maxLatencyMs: 20,
|
|
570
|
-
totalErrors: 3,
|
|
571
|
-
errorCodes: {
|
|
572
|
-
'0': 1,
|
|
573
|
-
'500': 2
|
|
574
|
-
},
|
|
575
|
-
}
|
|
576
|
-
```
|
|
577
|
-
|
|
578
|
-
The `result` object also has a `result.show()` function
|
|
579
|
-
that displays the results on the console in the standard format.
|
|
580
|
-
|
|
581
|
-
### Options
|
|
582
|
-
|
|
583
|
-
All options but `url` are, as their name implies, optional.
|
|
584
|
-
|
|
585
|
-
#### `url`
|
|
586
|
-
|
|
587
|
-
The URL to invoke. Mandatory.
|
|
588
|
-
|
|
589
|
-
#### `concurrency`
|
|
590
|
-
|
|
591
|
-
How many clients to start in parallel.
|
|
592
|
-
|
|
593
|
-
#### `maxRequests`
|
|
594
|
-
|
|
595
|
-
A max number of requests; after they are reached the test will end.
|
|
596
|
-
|
|
597
|
-
Note: the actual number of requests sent can be bigger if there is a concurrency level;
|
|
598
|
-
loadtest will report just on the max number of requests.
|
|
599
|
-
|
|
600
|
-
#### `maxSeconds`
|
|
601
|
-
|
|
602
|
-
Max number of seconds to run the tests.
|
|
603
|
-
|
|
604
|
-
Note: after the given number of seconds `loadtest` will stop sending requests,
|
|
605
|
-
but may continue receiving tests afterwards.
|
|
606
|
-
|
|
607
|
-
#### `timeout`
|
|
608
|
-
|
|
609
|
-
Timeout for each generated request in milliseconds. Setting this to 0 disables timeout (default).
|
|
610
|
-
|
|
611
|
-
#### `cookies`
|
|
612
|
-
|
|
613
|
-
An array of cookies to send. Each cookie should be a string of the form name=value.
|
|
614
|
-
|
|
615
|
-
#### `headers`
|
|
616
|
-
|
|
617
|
-
A map of headers. Each header should be an entry in the map with the value given as a string.
|
|
618
|
-
If you want to have several values for a header, write a single value separated by semicolons,
|
|
619
|
-
like this:
|
|
620
|
-
|
|
621
|
-
{
|
|
622
|
-
accept: "text/plain;text/html"
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
Note: when using the API, the "host" header is not inferred from the URL but needs to be sent
|
|
626
|
-
explicitly.
|
|
627
|
-
|
|
628
|
-
#### `method`
|
|
629
|
-
|
|
630
|
-
The method to use: POST, PUT. Default: GET.
|
|
631
|
-
|
|
632
|
-
#### `body`
|
|
633
|
-
|
|
634
|
-
The contents to send in the body of the message, for POST or PUT requests.
|
|
635
|
-
Can be a string or an object (which will be converted to JSON).
|
|
636
|
-
|
|
637
|
-
#### `contentType`
|
|
638
|
-
|
|
639
|
-
The MIME type to use for the body. Default content type is `text/plain`.
|
|
640
|
-
|
|
641
|
-
#### `requestsPerSecond`
|
|
642
|
-
|
|
643
|
-
How many requests each client will send per second.
|
|
644
|
-
|
|
645
|
-
#### `requestGenerator`
|
|
646
|
-
|
|
647
|
-
Use a custom request generator function.
|
|
648
|
-
The request needs to be generated synchronously and returned when this function is invoked.
|
|
649
|
-
|
|
650
|
-
Example request generator function could look like this:
|
|
651
|
-
|
|
652
|
-
```javascript
|
|
653
|
-
function(params, options, client, callback) {
|
|
654
|
-
const message = generateMessage();
|
|
655
|
-
const request = client(options, callback);
|
|
656
|
-
options.headers['Content-Length'] = message.length;
|
|
657
|
-
options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
658
|
-
request.write(message);
|
|
659
|
-
request.end();
|
|
660
|
-
return request;
|
|
661
|
-
}
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
See [`sample/request-generator.js`](sample/request-generator.js) for some sample code including a body
|
|
665
|
-
(or [`sample/request-generator.ts`](sample/request-generator.ts) for ES6/TypeScript).
|
|
666
|
-
|
|
667
|
-
#### `agentKeepAlive`
|
|
668
|
-
|
|
669
|
-
Use an agent with 'Connection: Keep-alive'.
|
|
670
|
-
|
|
671
|
-
Note: Uses [agentkeepalive](https://npmjs.org/package/agentkeepalive),
|
|
672
|
-
which performs better than the default node.js agent.
|
|
673
|
-
|
|
674
|
-
#### `quiet` (deprecated)
|
|
675
|
-
|
|
676
|
-
Do not show any messages.
|
|
677
|
-
|
|
678
|
-
Note: deprecated in version 6+, shows a warning.
|
|
679
|
-
|
|
680
|
-
#### `indexParam`
|
|
681
|
-
|
|
682
|
-
The given string will be replaced in the final URL with a unique index.
|
|
683
|
-
E.g.: if URL is `http://test.com/value` and `indexParam=value`, then the URL
|
|
684
|
-
will be:
|
|
685
|
-
|
|
686
|
-
* http://test.com/1
|
|
687
|
-
* http://test.com/2
|
|
688
|
-
* ...
|
|
689
|
-
* body will also be replaced `body:{ userid: id_value }` will be `body:{ userid: id_1 }`
|
|
690
|
-
|
|
691
|
-
#### `indexParamCallback`
|
|
692
|
-
|
|
693
|
-
A function that would be executed to replace the value identified through `indexParam` through a custom value generator.
|
|
694
|
-
|
|
695
|
-
E.g.: if URL is `http://test.com/value` and `indexParam=value` and
|
|
696
|
-
```javascript
|
|
697
|
-
indexParamCallback: function customCallBack() {
|
|
698
|
-
return Math.floor(Math.random() * 10); //returns a random integer from 0 to 9
|
|
699
|
-
}
|
|
700
|
-
```
|
|
701
|
-
then the URL could be:
|
|
702
|
-
|
|
703
|
-
* http://test.com/1 (Randomly generated integer 1)
|
|
704
|
-
* http://test.com/5 (Randomly generated integer 5)
|
|
705
|
-
* http://test.com/6 (Randomly generated integer 6)
|
|
706
|
-
* http://test.com/8 (Randomly generated integer 8)
|
|
707
|
-
* ...
|
|
708
|
-
* body will also be replaced `body:{ userid: id_value }` will be `body:{ userid: id_<value from callback> }`
|
|
709
|
-
|
|
710
|
-
#### `insecure`
|
|
711
|
-
|
|
712
|
-
Allow invalid and self-signed certificates over https.
|
|
713
|
-
|
|
714
|
-
#### `secureProtocol`
|
|
715
|
-
|
|
716
|
-
The TLS/SSL method to use. (e.g. TLSv1_method)
|
|
717
|
-
|
|
718
|
-
Example:
|
|
719
|
-
|
|
720
|
-
```javascript
|
|
721
|
-
import {loadTest} from 'loadtest'
|
|
722
|
-
|
|
723
|
-
const options = {
|
|
724
|
-
url: 'https://www.example.com',
|
|
725
|
-
maxRequests: 100,
|
|
726
|
-
secureProtocol: 'TLSv1_method'
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
loadTest(options, function(error) {
|
|
730
|
-
if (error) {
|
|
731
|
-
return console.error('Got an error: %s', error)
|
|
732
|
-
}
|
|
733
|
-
console.log('Tests run successfully')
|
|
734
|
-
})
|
|
735
|
-
```
|
|
736
|
-
|
|
737
|
-
#### `statusCallback`
|
|
738
|
-
|
|
739
|
-
If present, this function executes after every request operation completes. Provides immediate access to the test result while the
|
|
740
|
-
test batch is still running. This can be used for more detailed custom logging or developing your own spreadsheet or
|
|
741
|
-
statistical analysis of the result.
|
|
742
|
-
|
|
743
|
-
The result and error passed to the callback are in the same format as the result passed to the final callback.
|
|
744
|
-
|
|
745
|
-
In addition, the following three properties are added to the `result` object:
|
|
746
|
-
|
|
747
|
-
- `requestElapsed`: time in milliseconds it took to complete this individual request.
|
|
748
|
-
- `requestIndex`: 0-based index of this particular request in the sequence of all requests to be made.
|
|
749
|
-
- `instanceIndex`: the `loadtest(...)` instance index. This is useful if you call `loadtest()` more than once.
|
|
750
|
-
|
|
751
|
-
You will need to check if `error` is populated in order to determine which object to check for these properties.
|
|
752
|
-
|
|
753
|
-
The second parameter contains info about the current request:
|
|
754
|
-
|
|
755
|
-
```javascript
|
|
756
|
-
{
|
|
757
|
-
host: 'localhost',
|
|
758
|
-
path: '/',
|
|
759
|
-
method: 'GET',
|
|
760
|
-
statusCode: 200,
|
|
761
|
-
body: '<html><body>hi</body></html>',
|
|
762
|
-
headers: [...]
|
|
763
|
-
}
|
|
764
|
-
```
|
|
765
|
-
|
|
766
|
-
Example:
|
|
767
|
-
|
|
768
|
-
```javascript
|
|
769
|
-
import {loadTest} from 'loadtest'
|
|
770
|
-
|
|
771
|
-
function statusCallback(error, result, latency) {
|
|
772
|
-
console.log('Current latency %j, result %j, error %j', latency, result, error)
|
|
773
|
-
console.log('----')
|
|
774
|
-
console.log('Request elapsed milliseconds: ', result.requestElapsed)
|
|
775
|
-
console.log('Request index: ', result.requestIndex)
|
|
776
|
-
console.log('Request loadtest() instance index: ', result.instanceIndex)
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
const options = {
|
|
780
|
-
url: 'http://localhost:8000',
|
|
781
|
-
maxRequests: 1000,
|
|
782
|
-
statusCallback: statusCallback
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
loadTest(options, function(error) {
|
|
786
|
-
if (error) {
|
|
787
|
-
return console.error('Got an error: %s', error)
|
|
788
|
-
}
|
|
789
|
-
console.log('Tests run successfully')
|
|
790
|
-
})
|
|
791
519
|
```
|
|
792
|
-
|
|
793
|
-
In some situations request data needs to be available in the statusCallBack.
|
|
794
|
-
This data can be assigned to `request.labels` in the requestGenerator:
|
|
795
|
-
```javascript
|
|
796
|
-
const options = {
|
|
797
|
-
// ...
|
|
798
|
-
requestGenerator: (params, options, client, callback) => {
|
|
799
|
-
// ...
|
|
800
|
-
const randomInputData = Math.random().toString().substr(2, 8);
|
|
801
|
-
const message = JSON.stringify({ randomInputData })
|
|
802
|
-
const request = client(options, callback);
|
|
803
|
-
request.labels = randomInputData;
|
|
804
|
-
request.write(message);
|
|
805
|
-
return request;
|
|
806
|
-
}
|
|
807
|
-
};
|
|
808
|
-
```
|
|
809
|
-
|
|
810
|
-
Then in statusCallBack the labels can be accessed through `result.labels`:
|
|
811
|
-
```javascript
|
|
812
|
-
function statusCallback(error, result, latency) {
|
|
813
|
-
console.log(result.labels);
|
|
814
|
-
}
|
|
815
|
-
```
|
|
816
|
-
|
|
817
|
-
**Warning**: The format for `statusCallback` has changed in version 2.0.0 onwards.
|
|
818
|
-
It used to be `statusCallback(latency, result, error)`,
|
|
819
|
-
it has been changed to conform to the usual Node.js standard.
|
|
820
|
-
|
|
821
|
-
#### `contentInspector`
|
|
822
|
-
|
|
823
|
-
A function that would be executed after every request before its status be added to the final statistics.
|
|
824
520
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
521
|
+
Beware: if there are no `maxRequests` and no `maxSeconds`, the test will run forever.
|
|
522
|
+
|
|
523
|
+
### `loadTest()` Parameters
|
|
524
|
+
|
|
525
|
+
A simplified list of parameters is shown below;
|
|
526
|
+
see [doc/api.md](doc/api.md) for the full explanations with examples.
|
|
527
|
+
|
|
528
|
+
* `url`: URL to invoke, mandatory.
|
|
529
|
+
* `concurrency`: how many clients to start in parallel.
|
|
530
|
+
* `maxRequests`: max number of requests; after they are reached the test will end.
|
|
531
|
+
* `maxSeconds`: max number of seconds to run the tests.
|
|
532
|
+
* `timeout`: timeout for each generated request in milliseconds, set to 0 to disable (default).
|
|
533
|
+
* `cookies`: array of cookies to send, of the form `name=value`.
|
|
534
|
+
* `headers`: object with headers, each with the value as string. Separate by semicolons to have multiple values.
|
|
535
|
+
* `method`: HTTP method to use, default `GET`.
|
|
536
|
+
* `body`: contents to send in the body of the message.
|
|
537
|
+
* `contentType`: MIME type to use for the body, default `text/plain`.
|
|
538
|
+
* `requestsPerSecond`: how many requests will be sent per second.
|
|
539
|
+
* `requestGenerator`: custom request generator function.
|
|
540
|
+
* `agentKeepAlive`: if true, will use 'Connection: Keep-alive'.
|
|
541
|
+
* `quiet`: if true, do not show any messages.
|
|
542
|
+
* `indexParam`: parameter to replace in URL and body with a unique index.
|
|
543
|
+
* `indexParamCallback`: function to generate unique indexes.
|
|
544
|
+
* `insecure`: allow invalid and self-signed certificates over https.
|
|
545
|
+
* `secureProtocol`: TLS/SSL method to use.
|
|
546
|
+
* `statusCallback(error, result)`: function to call after every request is completed.
|
|
547
|
+
* `contentInspector(result)`: function to call before aggregating statistics.
|
|
845
548
|
|
|
846
549
|
### Start Test Server
|
|
847
550
|
|
|
@@ -854,35 +557,15 @@ const server = await startServer({port: 8000})
|
|
|
854
557
|
await server.close()
|
|
855
558
|
```
|
|
856
559
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
const server = startServer({port: 8000}, error => console.error(error))
|
|
864
|
-
```
|
|
865
|
-
|
|
866
|
-
The following options are available.
|
|
867
|
-
|
|
868
|
-
#### `port`
|
|
869
|
-
|
|
870
|
-
Optional port to use for the server.
|
|
871
|
-
|
|
872
|
-
Note: the default port is 7357, since port 80 requires special privileges.
|
|
873
|
-
|
|
874
|
-
#### `delay`
|
|
875
|
-
|
|
876
|
-
Wait the given number of milliseconds to answer each request.
|
|
877
|
-
|
|
878
|
-
#### `error`
|
|
879
|
-
|
|
880
|
-
Return an HTTP error code.
|
|
881
|
-
|
|
882
|
-
#### `percent`
|
|
560
|
+
The following options are available:
|
|
561
|
+
* `port`: optional port to use for the server, default 7357.
|
|
562
|
+
* `delay`: milliseconds to wait before answering each request.
|
|
563
|
+
* `error`: HTTP status code to return, default 200 (no error).
|
|
564
|
+
* `percent`: return error only for the given % of requests.
|
|
565
|
+
* `logger(request, response)`
|
|
883
566
|
|
|
884
|
-
|
|
885
|
-
|
|
567
|
+
A function to be called as `logger(request, response)` after every request served by the test server.
|
|
568
|
+
Where `request` and `response` are the usual HTTP objects.
|
|
886
569
|
|
|
887
570
|
### Configuration file
|
|
888
571
|
|
package/bin/loadtest.js
CHANGED
|
@@ -5,6 +5,7 @@ import * as stdio from 'stdio'
|
|
|
5
5
|
import {loadTest} from '../lib/loadtest.js'
|
|
6
6
|
import {runTask} from '../lib/cluster.js'
|
|
7
7
|
import {Result} from '../lib/result.js'
|
|
8
|
+
import {getHalfCores} from '../lib/cluster.js'
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
const options = stdio.getopt({
|
|
@@ -34,7 +35,7 @@ const options = stdio.getopt({
|
|
|
34
35
|
key: {args: 1, description: 'The client key to use'},
|
|
35
36
|
cert: {args: 1, description: 'The client certificate to use'},
|
|
36
37
|
quiet: {description: 'Do not log any messages'},
|
|
37
|
-
cores: {args: 1, description: 'Number of cores to use', default:
|
|
38
|
+
cores: {args: 1, description: 'Number of cores to use', default: getHalfCores()},
|
|
38
39
|
agent: {description: 'Use a keep-alive http agent (deprecated)'},
|
|
39
40
|
debug: {description: 'Show debug messages (deprecated)'},
|
|
40
41
|
});
|
package/doc/api.md
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
# loadtest API
|
|
2
|
+
|
|
3
|
+
The `loadtest` package is a powerful tool that can be added to your package.
|
|
4
|
+
The API allows for easy integration in your own tests,
|
|
5
|
+
for instance to run automated load tests.
|
|
6
|
+
You can verify the performance of your server before each deployment,
|
|
7
|
+
and deploy only if a certain target is reached.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
For access to the API just install it in your `npm` package as a dev dependency:
|
|
12
|
+
|
|
13
|
+
$ npm install --save-dev loadtest
|
|
14
|
+
|
|
15
|
+
### Compatibility
|
|
16
|
+
|
|
17
|
+
See [README file](../README.md) for compatibility.
|
|
18
|
+
|
|
19
|
+
## API
|
|
20
|
+
|
|
21
|
+
`loadtest` is not limited to running from the command line; it can be controlled using an API,
|
|
22
|
+
thus allowing you to load test your application in your own tests.
|
|
23
|
+
|
|
24
|
+
### Invoke Load Test
|
|
25
|
+
|
|
26
|
+
To run a load test, just `await` for the exported function `loadTest()` with the desired options, described below:
|
|
27
|
+
|
|
28
|
+
```javascript
|
|
29
|
+
import {loadTest} from 'loadtest'
|
|
30
|
+
|
|
31
|
+
const options = {
|
|
32
|
+
url: 'http://localhost:8000',
|
|
33
|
+
maxRequests: 1000,
|
|
34
|
+
}
|
|
35
|
+
const result = await loadTest(options)
|
|
36
|
+
result.show()
|
|
37
|
+
console.log('Tests run successfully')
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The call returns a `Result` object that contains all info about the load test, also described below.
|
|
41
|
+
Call `result.show()` to display the results in the standard format on the console.
|
|
42
|
+
|
|
43
|
+
As a legacy from before promises existed,
|
|
44
|
+
if an optional callback is passed as second parameter then it will not behave as `async`:
|
|
45
|
+
the callback `function(error, result)` will be invoked when the max number of requests is reached,
|
|
46
|
+
or when the max number of seconds has elapsed.
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
import {loadTest} from 'loadtest'
|
|
50
|
+
|
|
51
|
+
const options = {
|
|
52
|
+
url: 'http://localhost:8000',
|
|
53
|
+
maxRequests: 1000,
|
|
54
|
+
}
|
|
55
|
+
loadTest(options, function(error, result) {
|
|
56
|
+
if (error) {
|
|
57
|
+
return console.error('Got an error: %s', error)
|
|
58
|
+
}
|
|
59
|
+
result.show()
|
|
60
|
+
console.log('Tests run successfully')
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
Beware: if there are no `maxRequests` and no `maxSeconds`, then tests will run forever
|
|
66
|
+
and will not call the callback.
|
|
67
|
+
|
|
68
|
+
### Result
|
|
69
|
+
|
|
70
|
+
The latency result returned at the end of the load test contains a full set of data, including:
|
|
71
|
+
mean latency, number of errors and percentiles.
|
|
72
|
+
A simplified example follows:
|
|
73
|
+
|
|
74
|
+
```javascript
|
|
75
|
+
{
|
|
76
|
+
url: 'http://localhost:80/',
|
|
77
|
+
maxRequests: 1000,
|
|
78
|
+
maxSeconds: 0,
|
|
79
|
+
concurrency: 10,
|
|
80
|
+
agent: 'none',
|
|
81
|
+
requestsPerSecond: undefined,
|
|
82
|
+
totalRequests: 1000,
|
|
83
|
+
percentiles: {
|
|
84
|
+
'50': 7,
|
|
85
|
+
'90': 10,
|
|
86
|
+
'95': 11,
|
|
87
|
+
'99': 15
|
|
88
|
+
},
|
|
89
|
+
effectiveRps: 2824,
|
|
90
|
+
elapsedSeconds: 0.354108,
|
|
91
|
+
meanLatencyMs: 7.72,
|
|
92
|
+
maxLatencyMs: 20,
|
|
93
|
+
totalErrors: 3,
|
|
94
|
+
errorCodes: {
|
|
95
|
+
'0': 1,
|
|
96
|
+
'500': 2
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The `result` object also has a `result.show()` function
|
|
102
|
+
that displays the results on the console in the standard format.
|
|
103
|
+
|
|
104
|
+
### Options
|
|
105
|
+
|
|
106
|
+
All options but `url` are, as their name implies, optional.
|
|
107
|
+
See also the [simplified list](../README.md#loadtest-parameters).
|
|
108
|
+
|
|
109
|
+
#### `url`
|
|
110
|
+
|
|
111
|
+
The URL to invoke. Mandatory.
|
|
112
|
+
|
|
113
|
+
#### `concurrency`
|
|
114
|
+
|
|
115
|
+
How many clients to start in parallel.
|
|
116
|
+
|
|
117
|
+
#### `maxRequests`
|
|
118
|
+
|
|
119
|
+
A max number of requests; after they are reached the test will end.
|
|
120
|
+
|
|
121
|
+
Note: the actual number of requests sent can be bigger if there is a concurrency level;
|
|
122
|
+
loadtest will report just on the max number of requests.
|
|
123
|
+
|
|
124
|
+
#### `maxSeconds`
|
|
125
|
+
|
|
126
|
+
Max number of seconds to run the tests.
|
|
127
|
+
|
|
128
|
+
Note: after the given number of seconds `loadtest` will stop sending requests,
|
|
129
|
+
but may continue receiving tests afterwards.
|
|
130
|
+
|
|
131
|
+
#### `timeout`
|
|
132
|
+
|
|
133
|
+
Timeout for each generated request in milliseconds. Setting this to 0 disables timeout (default).
|
|
134
|
+
|
|
135
|
+
#### `cookies`
|
|
136
|
+
|
|
137
|
+
An array of cookies to send. Each cookie should be a string of the form name=value.
|
|
138
|
+
|
|
139
|
+
#### `headers`
|
|
140
|
+
|
|
141
|
+
A map of headers. Each header should be an entry in the map with the value given as a string.
|
|
142
|
+
If you want to have several values for a header, write a single value separated by semicolons,
|
|
143
|
+
like this:
|
|
144
|
+
|
|
145
|
+
{
|
|
146
|
+
accept: "text/plain;text/html"
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
Note: when using the API, the "host" header is not inferred from the URL but needs to be sent
|
|
150
|
+
explicitly.
|
|
151
|
+
|
|
152
|
+
#### `method`
|
|
153
|
+
|
|
154
|
+
The method to use: POST, PUT. Default: GET.
|
|
155
|
+
|
|
156
|
+
#### `body`
|
|
157
|
+
|
|
158
|
+
The contents to send in the body of the message, for POST or PUT requests.
|
|
159
|
+
Can be a string or an object (which will be converted to JSON).
|
|
160
|
+
|
|
161
|
+
#### `contentType`
|
|
162
|
+
|
|
163
|
+
The MIME type to use for the body. Default content type is `text/plain`.
|
|
164
|
+
|
|
165
|
+
#### `requestsPerSecond`
|
|
166
|
+
|
|
167
|
+
How many requests will be sent per second globally.
|
|
168
|
+
|
|
169
|
+
#### `requestGenerator(params, options, client, callback)`
|
|
170
|
+
|
|
171
|
+
Use a custom request generator function.
|
|
172
|
+
The request needs to be generated synchronously and returned when this function is invoked.
|
|
173
|
+
|
|
174
|
+
Example request generator function could look like this:
|
|
175
|
+
|
|
176
|
+
```javascript
|
|
177
|
+
function(params, options, client, callback) {
|
|
178
|
+
const message = generateMessage();
|
|
179
|
+
const request = client(options, callback);
|
|
180
|
+
options.headers['Content-Length'] = message.length;
|
|
181
|
+
options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
182
|
+
request.write(message);
|
|
183
|
+
request.end();
|
|
184
|
+
return request;
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
See [`sample/request-generator.js`](sample/request-generator.js) for some sample code including a body
|
|
189
|
+
(or [`sample/request-generator.ts`](sample/request-generator.ts) for ES6/TypeScript).
|
|
190
|
+
|
|
191
|
+
#### `agentKeepAlive`
|
|
192
|
+
|
|
193
|
+
Use an agent with 'Connection: Keep-alive'.
|
|
194
|
+
|
|
195
|
+
Note: Uses [agentkeepalive](https://npmjs.org/package/agentkeepalive),
|
|
196
|
+
which performs better than the default node.js agent.
|
|
197
|
+
|
|
198
|
+
#### `quiet`
|
|
199
|
+
|
|
200
|
+
Do not show any messages.
|
|
201
|
+
|
|
202
|
+
#### `indexParam`
|
|
203
|
+
|
|
204
|
+
The given string will be replaced in the final URL with a unique index.
|
|
205
|
+
E.g.: if URL is `http://test.com/value` and `indexParam=value`, then the URL
|
|
206
|
+
will be:
|
|
207
|
+
|
|
208
|
+
* http://test.com/1
|
|
209
|
+
* http://test.com/2
|
|
210
|
+
* ...
|
|
211
|
+
* body will also be replaced `body:{ userid: id_value }` will be `body:{ userid: id_1 }`
|
|
212
|
+
|
|
213
|
+
#### `indexParamCallback`
|
|
214
|
+
|
|
215
|
+
A function that would be executed to replace the value identified through `indexParam` through a custom value generator.
|
|
216
|
+
|
|
217
|
+
E.g.: if URL is `http://test.com/value` and `indexParam=value` and
|
|
218
|
+
```javascript
|
|
219
|
+
indexParamCallback: function customCallBack() {
|
|
220
|
+
return Math.floor(Math.random() * 10); //returns a random integer from 0 to 9
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
then the URL could be:
|
|
224
|
+
|
|
225
|
+
* http://test.com/1 (Randomly generated integer 1)
|
|
226
|
+
* http://test.com/5 (Randomly generated integer 5)
|
|
227
|
+
* http://test.com/6 (Randomly generated integer 6)
|
|
228
|
+
* http://test.com/8 (Randomly generated integer 8)
|
|
229
|
+
* ...
|
|
230
|
+
* body will also be replaced `body:{ userid: id_value }` will be `body:{ userid: id_<value from callback> }`
|
|
231
|
+
|
|
232
|
+
#### `insecure`
|
|
233
|
+
|
|
234
|
+
Allow invalid and self-signed certificates over https.
|
|
235
|
+
|
|
236
|
+
#### `secureProtocol`
|
|
237
|
+
|
|
238
|
+
The TLS/SSL method to use. (e.g. TLSv1_method)
|
|
239
|
+
|
|
240
|
+
Example:
|
|
241
|
+
|
|
242
|
+
```javascript
|
|
243
|
+
import {loadTest} from 'loadtest'
|
|
244
|
+
|
|
245
|
+
const options = {
|
|
246
|
+
url: 'https://www.example.com',
|
|
247
|
+
maxRequests: 100,
|
|
248
|
+
secureProtocol: 'TLSv1_method'
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
loadTest(options, function(error) {
|
|
252
|
+
if (error) {
|
|
253
|
+
return console.error('Got an error: %s', error)
|
|
254
|
+
}
|
|
255
|
+
console.log('Tests run successfully')
|
|
256
|
+
})
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
#### `statusCallback(error, result)`
|
|
260
|
+
|
|
261
|
+
If present, this function executes after every request operation completes. Provides immediate access to the test result while the
|
|
262
|
+
test batch is still running. This can be used for more detailed custom logging or developing your own spreadsheet or
|
|
263
|
+
statistical analysis of the result.
|
|
264
|
+
|
|
265
|
+
The `error` and `result` passed to the callback are in the same format as the result passed to the final callback:
|
|
266
|
+
|
|
267
|
+
* `error` is only populated if the request finished in error,
|
|
268
|
+
* `result` contains info about the current request: `host`, `path`, `method`, `statusCode`, received `body` and `headers`.
|
|
269
|
+
Additionally has the following parameters:
|
|
270
|
+
- `requestElapsed`: time in milliseconds it took to complete this individual request.
|
|
271
|
+
- `requestIndex`: 0-based index of this particular request in the sequence of all requests to be made.
|
|
272
|
+
- `instanceIndex`: the `loadtest(...)` instance index. This is useful if you call `loadtest()` more than once.
|
|
273
|
+
|
|
274
|
+
Example result:
|
|
275
|
+
|
|
276
|
+
```javascript
|
|
277
|
+
{
|
|
278
|
+
host: 'localhost',
|
|
279
|
+
path: '/',
|
|
280
|
+
method: 'GET',
|
|
281
|
+
statusCode: 200,
|
|
282
|
+
body: '<html><body>hi</body></html>',
|
|
283
|
+
headers: [...],
|
|
284
|
+
requestElapsed: 248,
|
|
285
|
+
requestIndex: 8748,
|
|
286
|
+
instanceIndex: 5,
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
See [full example](./status-callback.md).
|
|
291
|
+
|
|
292
|
+
**Warning**: The format for `statusCallback` has changed in version 7+.
|
|
293
|
+
Used to be `statusCallback(error, result, latency)`;
|
|
294
|
+
the third parameter `latency` has been removed due to performance reasons.
|
|
295
|
+
|
|
296
|
+
#### `contentInspector(result)`
|
|
297
|
+
|
|
298
|
+
A function that would be executed after every request before its status be added to the final statistics.
|
|
299
|
+
|
|
300
|
+
The is can be used when you want to mark some result with 200 http status code to be failed or error.
|
|
301
|
+
|
|
302
|
+
The `result` object passed to this callback function has the same fields as the `result` object passed to `statusCallback`.
|
|
303
|
+
|
|
304
|
+
`customError` can be added to mark this result as failed or error. `customErrorCode` will be provided in the final statistics, in addtion to the http status code.
|
|
305
|
+
|
|
306
|
+
Example:
|
|
307
|
+
|
|
308
|
+
```javascript
|
|
309
|
+
function contentInspector(result) {
|
|
310
|
+
if (result.statusCode == 200) {
|
|
311
|
+
const body = JSON.parse(result.body)
|
|
312
|
+
// how to examine the body depends on the content that the service returns
|
|
313
|
+
if (body.status.err_code !== 0) {
|
|
314
|
+
result.customError = body.status.err_code + " " + body.status.msg
|
|
315
|
+
result.customErrorCode = body.status.err_code
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Start Test Server
|
|
322
|
+
|
|
323
|
+
To start the test server use the exported function `startServer()` with a set of options:
|
|
324
|
+
|
|
325
|
+
```javascript
|
|
326
|
+
import {startServer} from 'loadtest'
|
|
327
|
+
const server = await startServer({port: 8000})
|
|
328
|
+
// do your thing
|
|
329
|
+
await server.close()
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
This function returns when the server is up and running,
|
|
333
|
+
with an HTTP server which can be `close()`d when it is no longer useful.
|
|
334
|
+
As a legacy from before promises existed,
|
|
335
|
+
if an optional callback is passed as second parameter then it will not behave as `async`:
|
|
336
|
+
|
|
337
|
+
```javascript
|
|
338
|
+
const server = startServer({port: 8000}, error => console.error(error))
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
The following options are available.
|
|
342
|
+
|
|
343
|
+
#### `port`
|
|
344
|
+
|
|
345
|
+
Optional port to use for the server.
|
|
346
|
+
|
|
347
|
+
Note: the default port is 7357, since port 80 requires special privileges.
|
|
348
|
+
|
|
349
|
+
#### `delay`
|
|
350
|
+
|
|
351
|
+
Wait the given number of milliseconds to answer each request.
|
|
352
|
+
|
|
353
|
+
#### `error`
|
|
354
|
+
|
|
355
|
+
Return an HTTP error code.
|
|
356
|
+
|
|
357
|
+
#### `percent`
|
|
358
|
+
|
|
359
|
+
Return an HTTP error code only for the given % of requests.
|
|
360
|
+
If no error code was specified, default is 500.
|
|
361
|
+
|
|
362
|
+
#### `logger(request, response)`
|
|
363
|
+
|
|
364
|
+
A function to be called after every request served by the test server.
|
|
365
|
+
`request` and `response` are the usual HTTP objects.
|
|
366
|
+
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# `statusCallback`
|
|
2
|
+
|
|
3
|
+
In-depth examples for the `statusCallback` API parameter.
|
|
4
|
+
|
|
5
|
+
## `options.statusCallback(error, result)`
|
|
6
|
+
|
|
7
|
+
This function, if present, is invoked after every request is finished.
|
|
8
|
+
It uses the old-style convention `(error, result)`:
|
|
9
|
+
the `error` is only present if the request fails,
|
|
10
|
+
while the `result` is always present and contains info about the request.
|
|
11
|
+
|
|
12
|
+
**Warning**: The format for `statusCallback` has changed in version 7+.
|
|
13
|
+
The third parameter `latency` has been removed due to performance reasons.
|
|
14
|
+
|
|
15
|
+
**Warning**: The format for `statusCallback` has changed in version 2.0.0 onwards.
|
|
16
|
+
It used to be `statusCallback(latency, result, error)`,
|
|
17
|
+
it has been changed to conform to the usual Node.js standard.
|
|
18
|
+
|
|
19
|
+
### `result` format
|
|
20
|
+
|
|
21
|
+
The `result` parameter has the following attributes, always present:
|
|
22
|
+
|
|
23
|
+
* `host`: the host where the request was sent.
|
|
24
|
+
* `path`: the URL path to send the request.
|
|
25
|
+
* `method: HTTP method used.
|
|
26
|
+
* `statusCode: HTTP status code, 200 is OK.
|
|
27
|
+
* `body: content received from the server.
|
|
28
|
+
* `headers: sent by the server.
|
|
29
|
+
* `requestElapsed`: time in milliseconds it took to complete this individual request.
|
|
30
|
+
* `requestIndex`: 0-based index of this particular request in the sequence of all requests to be made.
|
|
31
|
+
* `instanceIndex`: the `loadtest(...)` instance index. This is useful if you call `loadtest()` more than once.
|
|
32
|
+
|
|
33
|
+
### Example Result
|
|
34
|
+
|
|
35
|
+
A sample result might look like this:
|
|
36
|
+
|
|
37
|
+
```javascript
|
|
38
|
+
{
|
|
39
|
+
host: 'localhost',
|
|
40
|
+
path: '/',
|
|
41
|
+
method: 'GET',
|
|
42
|
+
statusCode: 200,
|
|
43
|
+
body: '<html><body>hi</body></html>',
|
|
44
|
+
headers: [...],
|
|
45
|
+
requestElapsed: 248,
|
|
46
|
+
requestIndex: 8748,
|
|
47
|
+
instanceIndex: 5,
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Full example
|
|
52
|
+
|
|
53
|
+
A full example of how to use the `statusCallback` follows:
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
import {loadTest} from 'loadtest'
|
|
57
|
+
|
|
58
|
+
function statusCallback(error, result, latency) {
|
|
59
|
+
console.log('Current latency %j, result %j, error %j', latency, result, error)
|
|
60
|
+
console.log('----')
|
|
61
|
+
console.log('Request elapsed milliseconds: ', result.requestElapsed)
|
|
62
|
+
console.log('Request index: ', result.requestIndex)
|
|
63
|
+
console.log('Request loadtest() instance index: ', result.instanceIndex)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const options = {
|
|
67
|
+
url: 'http://localhost:8000',
|
|
68
|
+
maxRequests: 1000,
|
|
69
|
+
statusCallback: statusCallback
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
loadTest(options, function(error) {
|
|
73
|
+
if (error) {
|
|
74
|
+
return console.error('Got an error: %s', error)
|
|
75
|
+
}
|
|
76
|
+
console.log('Tests run successfully')
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Adding Request Data
|
|
81
|
+
|
|
82
|
+
In some situations request data needs to be available in the statusCallBack.
|
|
83
|
+
This data can be assigned to `request.labels` in the requestGenerator:
|
|
84
|
+
```javascript
|
|
85
|
+
const options = {
|
|
86
|
+
// ...
|
|
87
|
+
requestGenerator: (params, options, client, callback) => {
|
|
88
|
+
// ...
|
|
89
|
+
const randomInputData = Math.random().toString().substr(2, 8);
|
|
90
|
+
const message = JSON.stringify({ randomInputData })
|
|
91
|
+
const request = client(options, callback);
|
|
92
|
+
request.labels = randomInputData;
|
|
93
|
+
request.write(message);
|
|
94
|
+
return request;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Then in statusCallBack the labels can be accessed through `result.labels`:
|
|
100
|
+
```javascript
|
|
101
|
+
function statusCallback(error, result, latency) {
|
|
102
|
+
console.log(result.labels);
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
package/lib/httpClient.js
CHANGED
|
@@ -9,6 +9,8 @@ import * as agentkeepalive from 'agentkeepalive'
|
|
|
9
9
|
import * as HttpsProxyAgent from 'https-proxy-agent'
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
let uniqueIndex = 1
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* Create a new HTTP client.
|
|
14
16
|
* Seem parameters below.
|
|
@@ -90,6 +92,19 @@ class HttpClient {
|
|
|
90
92
|
if (this.params.secureProtocol) {
|
|
91
93
|
this.options.secureProtocol = this.params.secureProtocol;
|
|
92
94
|
}
|
|
95
|
+
// adding proxy configuration
|
|
96
|
+
if (this.params.proxy) {
|
|
97
|
+
const proxy = this.params.proxy;
|
|
98
|
+
const agent = new HttpsProxyAgent(proxy);
|
|
99
|
+
this.options.agent = agent;
|
|
100
|
+
}
|
|
101
|
+
if (this.params.indexParam) {
|
|
102
|
+
this.options.indexParamFinder = new RegExp(this.params.indexParam, 'g');
|
|
103
|
+
}
|
|
104
|
+
// Disable certificate checking
|
|
105
|
+
if (this.params.insecure === true) {
|
|
106
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
107
|
+
}
|
|
93
108
|
}
|
|
94
109
|
|
|
95
110
|
/**
|
|
@@ -129,43 +144,10 @@ class HttpClient {
|
|
|
129
144
|
this.operation.requests += 1;
|
|
130
145
|
|
|
131
146
|
const id = this.operation.latency.start();
|
|
147
|
+
const options = {...this.options, headers: {...this.options.headers}}
|
|
132
148
|
const requestFinished = this.getRequestFinisher(id);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
lib = https;
|
|
136
|
-
}
|
|
137
|
-
if (this.options.protocol == 'ws:') {
|
|
138
|
-
lib = websocket;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// adding proxy configuration
|
|
142
|
-
if (this.params.proxy) {
|
|
143
|
-
const proxy = this.params.proxy;
|
|
144
|
-
const agent = new HttpsProxyAgent(proxy);
|
|
145
|
-
this.options.agent = agent;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// Disable certificate checking
|
|
150
|
-
if (this.params.insecure === true) {
|
|
151
|
-
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
152
|
-
}
|
|
153
|
-
let request, message;
|
|
154
|
-
if (this.generateMessage) {
|
|
155
|
-
message = this.generateMessage(id);
|
|
156
|
-
if (typeof message === 'object') {
|
|
157
|
-
message = JSON.stringify(message);
|
|
158
|
-
}
|
|
159
|
-
this.options.headers['Content-Length'] = Buffer.byteLength(message);
|
|
160
|
-
} else {
|
|
161
|
-
delete this.options.headers['Content-Length'];
|
|
162
|
-
}
|
|
163
|
-
if (typeof this.params.requestGenerator == 'function') {
|
|
164
|
-
const connect = this.getConnect(id, requestFinished, this.params.contentInspector)
|
|
165
|
-
request = this.params.requestGenerator(this.params, this.options, lib.request, connect);
|
|
166
|
-
} else {
|
|
167
|
-
request = lib.request(this.options, this.getConnect(id, requestFinished, this.params.contentInspector));
|
|
168
|
-
}
|
|
149
|
+
this.customizeIndex(options)
|
|
150
|
+
const request = this.getRequest(id, options, requestFinished)
|
|
169
151
|
if (this.params.timeout) {
|
|
170
152
|
const timeout = parseInt(this.params.timeout);
|
|
171
153
|
if (!timeout) {
|
|
@@ -175,8 +157,10 @@ class HttpClient {
|
|
|
175
157
|
requestFinished('Connection timed out');
|
|
176
158
|
});
|
|
177
159
|
}
|
|
160
|
+
const message = this.getMessage(id, options)
|
|
178
161
|
if (message) {
|
|
179
162
|
request.write(message);
|
|
163
|
+
options.headers['Content-Length'] = Buffer.byteLength(message);
|
|
180
164
|
}
|
|
181
165
|
request.on('error', error => {
|
|
182
166
|
requestFinished('Connection error: ' + error.message);
|
|
@@ -184,6 +168,54 @@ class HttpClient {
|
|
|
184
168
|
request.end();
|
|
185
169
|
}
|
|
186
170
|
|
|
171
|
+
customizeIndex(options) {
|
|
172
|
+
if (!this.options.indexParamFinder) {
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
options.customIndex = this.getCustomIndex()
|
|
176
|
+
options.path = this.options.path.replace(this.options.indexParamFinder, options.customIndex);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
getCustomIndex() {
|
|
180
|
+
if (this.options.indexParamCallback instanceof Function) {
|
|
181
|
+
return this.options.indexParamCallback();
|
|
182
|
+
}
|
|
183
|
+
const customIndex = uniqueIndex
|
|
184
|
+
uniqueIndex += 1
|
|
185
|
+
return customIndex
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
getLib() {
|
|
189
|
+
if (this.options.protocol == 'https:') {
|
|
190
|
+
return https;
|
|
191
|
+
}
|
|
192
|
+
if (this.options.protocol == 'ws:') {
|
|
193
|
+
return websocket;
|
|
194
|
+
}
|
|
195
|
+
return http;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
getMessage(id, options) {
|
|
199
|
+
if (!this.generateMessage) {
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
const candidate = this.generateMessage(id);
|
|
203
|
+
const message = typeof candidate === 'object' ? JSON.stringify(candidate) : candidate
|
|
204
|
+
if (this.options.indexParamFinder) {
|
|
205
|
+
return message.replace(this.options.indexParamFinder, options.customIndex);
|
|
206
|
+
}
|
|
207
|
+
return message
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
getRequest(id, options, requestFinished) {
|
|
211
|
+
const lib = this.getLib()
|
|
212
|
+
if (typeof this.params.requestGenerator == 'function') {
|
|
213
|
+
const connect = this.getConnect(id, requestFinished, this.params.contentInspector)
|
|
214
|
+
return this.params.requestGenerator(this.params, options, lib.request, connect);
|
|
215
|
+
}
|
|
216
|
+
return lib.request(options, this.getConnect(id, requestFinished, this.params.contentInspector));
|
|
217
|
+
}
|
|
218
|
+
|
|
187
219
|
/**
|
|
188
220
|
* Get a function that finishes one request and goes for the next.
|
|
189
221
|
*/
|
package/lib/loadtest.js
CHANGED
|
@@ -16,23 +16,25 @@ https.globalAgent.maxSockets = 1000;
|
|
|
16
16
|
* Run a load test.
|
|
17
17
|
* Parameters:
|
|
18
18
|
* - `options`: an object which may have:
|
|
19
|
-
* - url
|
|
20
|
-
* - concurrency
|
|
21
|
-
* - maxRequests
|
|
22
|
-
* - maxSeconds
|
|
23
|
-
* - cookies
|
|
24
|
-
* - headers
|
|
25
|
-
* - method
|
|
26
|
-
* -
|
|
27
|
-
* - contentType
|
|
28
|
-
* - requestsPerSecond
|
|
19
|
+
* - url: URL to access (mandatory).
|
|
20
|
+
* - concurrency: how many concurrent clients to use.
|
|
21
|
+
* - maxRequests: how many requests to send
|
|
22
|
+
* - maxSeconds: how long to run the tests.
|
|
23
|
+
* - cookies: a string or an array of strings, each with name:value.
|
|
24
|
+
* - headers: a map with headers: {key1: value1, key2: value2}.
|
|
25
|
+
* - method: the method to use: POST, PUT. Default: GET, what else.
|
|
26
|
+
* - data: the contents to send along a POST or PUT request.
|
|
27
|
+
* - contentType: the MIME type to use for the body, default text/plain.
|
|
28
|
+
* - requestsPerSecond: how many requests per second to send.
|
|
29
29
|
* - agentKeepAlive: if true, then use connection keep-alive.
|
|
30
|
-
* - indexParam
|
|
30
|
+
* - indexParam: string to replace with a unique index.
|
|
31
31
|
* - insecure: allow https using self-signed certs.
|
|
32
|
-
* - secureProtocol
|
|
33
|
-
* - proxy
|
|
32
|
+
* - secureProtocol: TLS/SSL secure protocol method to use.
|
|
33
|
+
* - proxy: use a proxy for requests e.g. http://localhost:8080.
|
|
34
34
|
* - quiet: do not log any messages.
|
|
35
35
|
* - debug: show debug messages (deprecated).
|
|
36
|
+
* - requestGenerator: use a custom function to generate requests.
|
|
37
|
+
* - statusCallback: function called after every request.
|
|
36
38
|
* - `callback`: optional `function(result, error)` called if/when the test finishes;
|
|
37
39
|
* if not present a promise is returned.
|
|
38
40
|
*/
|
|
@@ -109,7 +111,7 @@ class Operation {
|
|
|
109
111
|
next();
|
|
110
112
|
}
|
|
111
113
|
if (this.options.statusCallback) {
|
|
112
|
-
this.options.statusCallback(error, result
|
|
114
|
+
this.options.statusCallback(error, result);
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
|
|
@@ -117,33 +119,9 @@ class Operation {
|
|
|
117
119
|
* Start a number of measuring clients.
|
|
118
120
|
*/
|
|
119
121
|
startClients() {
|
|
120
|
-
const url = this.options.url;
|
|
121
|
-
const strBody = JSON.stringify(this.options.body);
|
|
122
122
|
for (let index = 0; index < this.options.concurrency; index++) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if(this.options.indexParamCallback instanceof Function) {
|
|
126
|
-
let customIndex = this.options.indexParamCallback();
|
|
127
|
-
this.options.url = url.replace(oldToken, customIndex);
|
|
128
|
-
if(this.options.body) {
|
|
129
|
-
let body = strBody.replace(oldToken, customIndex);
|
|
130
|
-
this.options.body = JSON.parse(body);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
this.options.url = url.replace(oldToken, index);
|
|
135
|
-
if(this.options.body) {
|
|
136
|
-
let body = strBody.replace(oldToken, index);
|
|
137
|
-
this.options.body = JSON.parse(body);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
let constructor = httpClient.create;
|
|
142
|
-
// TODO: || this.options.url.startsWith('wss:'))
|
|
143
|
-
if (this.options.url.startsWith('ws:')) {
|
|
144
|
-
constructor = websocket.create;
|
|
145
|
-
}
|
|
146
|
-
const client = constructor(this, this.options);
|
|
123
|
+
const createClient = this.getClientCreator()
|
|
124
|
+
const client = createClient(this, this.options);
|
|
147
125
|
this.clients[index] = client;
|
|
148
126
|
if (!this.options.requestsPerSecond) {
|
|
149
127
|
client.start();
|
|
@@ -155,6 +133,14 @@ class Operation {
|
|
|
155
133
|
}
|
|
156
134
|
}
|
|
157
135
|
|
|
136
|
+
getClientCreator() {
|
|
137
|
+
// TODO: || this.options.url.startsWith('wss:'))
|
|
138
|
+
if (this.options.url.startsWith('ws:')) {
|
|
139
|
+
return websocket.create;
|
|
140
|
+
}
|
|
141
|
+
return httpClient.create;
|
|
142
|
+
}
|
|
143
|
+
|
|
158
144
|
/**
|
|
159
145
|
* Stop clients.
|
|
160
146
|
*/
|
package/lib/options.js
CHANGED
package/lib/testserver.js
CHANGED
|
@@ -16,6 +16,7 @@ const LOG_HEADERS_INTERVAL_MS = 5000;
|
|
|
16
16
|
* - quiet: do not log any messages.
|
|
17
17
|
* - percent: give an error (default 500) on some % of requests.
|
|
18
18
|
* - error: set an HTTP error code, default is 500.
|
|
19
|
+
* - logger: function to log all incoming requests.
|
|
19
20
|
* - `callback`: optional callback, called after the server has started.
|
|
20
21
|
* If not present will return a promise.
|
|
21
22
|
*/
|
|
@@ -118,10 +119,10 @@ class TestServer {
|
|
|
118
119
|
this.debug(request, elapsedMs);
|
|
119
120
|
}
|
|
120
121
|
if (!this.options.delay) {
|
|
121
|
-
return this.end(response, id);
|
|
122
|
+
return this.end(request, response, id);
|
|
122
123
|
}
|
|
123
124
|
setTimeout(() => {
|
|
124
|
-
this.end(response, id);
|
|
125
|
+
this.end(request, response, id);
|
|
125
126
|
}, this.options.delay).unref();
|
|
126
127
|
});
|
|
127
128
|
}
|
|
@@ -167,7 +168,7 @@ class TestServer {
|
|
|
167
168
|
/**
|
|
168
169
|
* End the response now.
|
|
169
170
|
*/
|
|
170
|
-
end(response, id) {
|
|
171
|
+
end(request, response, id) {
|
|
171
172
|
if (this.shouldError()) {
|
|
172
173
|
const code = this.options.error || 500;
|
|
173
174
|
response.writeHead(code);
|
|
@@ -176,6 +177,9 @@ class TestServer {
|
|
|
176
177
|
response.end('OK');
|
|
177
178
|
}
|
|
178
179
|
this.latency.end(id);
|
|
180
|
+
if (this.options.logger) {
|
|
181
|
+
this.options.logger(request, response)
|
|
182
|
+
}
|
|
179
183
|
}
|
|
180
184
|
|
|
181
185
|
shouldError() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loadtest",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Run load tests for your web application. Mostly ab-compatible interface, with an option to force requests per second. Includes an API for automated load testing.",
|
|
6
6
|
"homepage": "https://github.com/alexfernandez/loadtest",
|
package/test/integration.js
CHANGED
|
@@ -159,12 +159,62 @@ async function testPromise() {
|
|
|
159
159
|
return 'Test result: ' + JSON.stringify(result)
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
async function testIndexParam() {
|
|
163
|
+
const urls = new Map()
|
|
164
|
+
const bodies = new Map()
|
|
165
|
+
function logger(request) {
|
|
166
|
+
if (urls.has(request.url)) {
|
|
167
|
+
throw new Error(`Duplicated url ${request.url}`)
|
|
168
|
+
}
|
|
169
|
+
urls.set(request.url, true)
|
|
170
|
+
if (bodies.has(request.body)) {
|
|
171
|
+
throw new Error(`Duplicated body ${request.body}`)
|
|
172
|
+
}
|
|
173
|
+
bodies.set(request.body, true)
|
|
174
|
+
}
|
|
175
|
+
const server = await startServer({logger, ...serverOptions})
|
|
176
|
+
const options = {
|
|
177
|
+
url: `http://localhost:${PORT}/?param=index`,
|
|
178
|
+
maxRequests: 100,
|
|
179
|
+
concurrency: 10,
|
|
180
|
+
postBody: {
|
|
181
|
+
hi: 'my_index',
|
|
182
|
+
},
|
|
183
|
+
indexParam: 'index',
|
|
184
|
+
quiet: true,
|
|
185
|
+
};
|
|
186
|
+
await loadTest(options)
|
|
187
|
+
await server.close()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function testStatusCallback() {
|
|
191
|
+
let calls = 0
|
|
192
|
+
const server = await startServer(serverOptions)
|
|
193
|
+
const options = {
|
|
194
|
+
url: `http://localhost:${PORT}/`,
|
|
195
|
+
maxRequests: 100,
|
|
196
|
+
concurrency: 10,
|
|
197
|
+
postBody: {
|
|
198
|
+
hi: 'hey',
|
|
199
|
+
},
|
|
200
|
+
quiet: true,
|
|
201
|
+
statusCallback: (error, result) => {
|
|
202
|
+
testing.assertEquals(result.statusCode, 200, 'Should receive status 200')
|
|
203
|
+
calls += 1
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
await loadTest(options)
|
|
207
|
+
testing.assertEquals(calls, 100, 'Should have 100 calls')
|
|
208
|
+
await server.close()
|
|
209
|
+
}
|
|
210
|
+
|
|
162
211
|
/**
|
|
163
212
|
* Run all tests.
|
|
164
213
|
*/
|
|
165
214
|
export function test(callback) {
|
|
166
215
|
testing.run([
|
|
167
|
-
testIntegration, testIntegrationFile, testDelay, testWSIntegration,
|
|
216
|
+
testIntegration, testIntegrationFile, testDelay, testWSIntegration,
|
|
217
|
+
testPromise, testIndexParam, testStatusCallback,
|
|
168
218
|
], 4000, callback);
|
|
169
219
|
}
|
|
170
220
|
|