├── .npmignore ├── LICENSE.md ├── README.md ├── package.json └── wperf.js /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2019 Joseph Huckaby 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
Table of Contents 2 | 3 | 4 | - [Overview](#overview) 5 | * [Progress Display](#progress-display) 6 | * [Completed Output](#completed-output) 7 | - [Usage](#usage) 8 | * [Configuration Options](#configuration-options) 9 | + [url](#url) 10 | + [params](#params) 11 | + [max](#max) 12 | + [threads](#threads) 13 | + [keepalive](#keepalive) 14 | + [throttle](#throttle) 15 | + [timeout](#timeout) 16 | + [warn](#warn) 17 | + [warnings](#warnings) 18 | + [log](#log) 19 | + [fatal](#fatal) 20 | + [verbose](#verbose) 21 | + [cache_dns](#cache_dns) 22 | + [auth](#auth) 23 | + [compress](#compress) 24 | + [useragent](#useragent) 25 | + [follow](#follow) 26 | + [retries](#retries) 27 | + [insecure](#insecure) 28 | + [headers](#headers) 29 | + [method](#method) 30 | + [data](#data) 31 | + [multipart](#multipart) 32 | + [files](#files) 33 | + [success_match](#success_match) 34 | + [error_match](#error_match) 35 | + [histo](#histo) 36 | + [histo_ranges](#histo_ranges) 37 | + [stats](#stats) 38 | + [quiet](#quiet) 39 | + [color](#color) 40 | + [wrapper (Advanced)](#wrapper-advanced) 41 | - [Wrapper Constructor](#wrapper-constructor) 42 | - [Wrapper Method Hook](#wrapper-method-hook) 43 | - [Related](#related) 44 | - [License (MIT)](#license-mit) 45 | 46 |
47 | 48 | # Overview 49 | 50 | **WebPerf** (`wperf`) is a simple command-line HTTP load tester utility, which can send repeated HTTP requests to a target URL or set of URLs. It provides detailed performance metrics, including a breakdown of all the HTTP request phases, and a histogram showing the elapsed time spread. 51 | 52 | This is similar to the popular [ApacheBench](http://httpd.apache.org/docs/2.4/programs/ab.html) (`ab`) tool, but provides additional features like dynamic URL substitution, progress display with time remaining and live req/sec, a more detailed performance breakdown, and the ability to save configurations in JSON files. 53 | 54 | During a run, the script will display a graphical progress bar with estimated time remaining, and the current requests per second. You can also hit **Ctrl+Z** to output an in-progress report, and of course **Ctrl+C** to abort a run, which will render one final report before exiting. 55 | 56 | ## Progress Display 57 | 58 | ![Screenshot](https://pixlcore.com/software/wperf/progress.png) 59 | 60 | ## Completed Output 61 | 62 | ![Screenshot](https://pixlcore.com/software/wperf/terminal.png) 63 | 64 | **Notes:** 65 | 66 | - The "Samples" numbers may differ between metrics for things like Keep-Alives, which will reuse sockets and therefore require fewer DNS lookups and TCP connects. 67 | - The "Decompress" metric is only applicable for encoded (compressed) server responses (i.e. Gzip, Deflate). 68 | - The "Peak Performance" is the best performing second, which is only shown if the total time covered a full second. 69 | - A "warning" is a request that took longer than a specified duration (default is 1 second). 70 | - An "error is a bad HTTP response (outside of the 2xx or 3xx range) or DNS lookup / TCP connect failure. 71 | 72 | # Usage 73 | 74 | Use [npm](https://www.npmjs.com/) to install the module as a command-line executable: 75 | 76 | ```sh 77 | npm install -g wperf 78 | ``` 79 | 80 | Then call it using `wperf` and specify your URL and options on the command-line: 81 | 82 | ```sh 83 | wperf URL [--OPTION1 --OPTION2 ...] 84 | ``` 85 | 86 | Example command: 87 | 88 | ```sh 89 | wperf https://myserver.com/some/path --max 100 --threads 2 --keepalive 90 | ``` 91 | 92 | This would send an HTTP GET request to the specified URL 100 times using 2 threads, and utilizing [HTTP Keep-Alives](https://en.wikipedia.org/wiki/HTTP_persistent_connection) (i.e. reuse sockets for subsequent requests, if the target server supports it). 93 | 94 | Alternatively, you can store all your configuration settings in a JSON file, and specify it as the first argument (see below for details on how to format the JSON file): 95 | 96 | ```sh 97 | wperf my-load-test.json 98 | ``` 99 | 100 | You can also include command-line arguments after the configuration file which acts as overrides: 101 | 102 | ```sh 103 | wperf my-load-test.json --verbose 104 | ``` 105 | 106 | ## Configuration Options 107 | 108 | You can specify most configuration options on the command line (using the syntax `--key value`) or in a JSON configuration file as a top-level property. There are a few exceptions which are noted below. 109 | 110 | ### url 111 | 112 | Use the command-line `--url` or JSON `url` property to specify the URL to be requested. As a shortcut, the URL can be specified as the first argument to the command-line script, without the `--url` prefix. Example command-line: 113 | 114 | ```sh 115 | wperf https://myserver.com/some/path 116 | ``` 117 | 118 | Example JSON configuration: 119 | 120 | ```json 121 | { 122 | "url": "https://myserver.com/some/path" 123 | } 124 | ``` 125 | 126 | ### params 127 | 128 | If you use a JSON configuration file, you can insert `[placeholder]` variables into your URL. These are expanded by looking in a `params` object in the JSON file, if provided. Further, each parameter may be an array of values, of which one is picked randomly per request. Example: 129 | 130 | ```json 131 | { 132 | "url": "https://myserver.com/some/path?food=[food]", 133 | "params": { 134 | "food": ["apple", "orange", "banana"] 135 | } 136 | } 137 | ``` 138 | 139 | This would produce three different URLs, picked randomly for each request: 140 | 141 | ``` 142 | https://myserver.com/some/path?food=apple 143 | https://myserver.com/some/path?food=orange 144 | https://myserver.com/some/path?food=banana 145 | ``` 146 | 147 | You can nest parameters, meaning the values can themselves contain `[placeholder]` variables, which are further expanded until all are satisfied (up to 32 levels deep). Example of this: 148 | 149 | ```json 150 | { 151 | "url": "https://myserver.com[uri]", 152 | "params": { 153 | "uri": [ 154 | "/some/path?&action=eat&food=[food]", 155 | "/some/other/path?&action=drink&beverage=[beverage]" 156 | ], 157 | "food": ["apple", "orange", "banana"], 158 | "beverage": ["coke", "pepsi"] 159 | } 160 | } 161 | ``` 162 | 163 | Here we have the full URI path substituted out as `[uri]`, which may pick one of two values, one with a `[food]` and one with a `[beverage]` variable. This particular configuration would result in 5 total unique URLs. 164 | 165 | If you just want to pick from a random list of URLs, simply set the `url` property to a string containing a single macro like `[url]`, then list all your URLs like this: 166 | 167 | ```json 168 | { 169 | "url": "[url]", 170 | "params": { 171 | "url": [ 172 | "http://server1.com/path/one", 173 | "http://server2.com/path/two", 174 | "http://server3.com/path/three" 175 | ] 176 | } 177 | } 178 | ``` 179 | 180 | If you simply want a random number on your URLs, you can use the `[#-#]` shortcut, which will pick a random integer within the specified range (inclusive). Example: 181 | 182 | ```json 183 | { 184 | "url": "https://myserver.com/some/path?&random=[0-99999]" 185 | } 186 | ``` 187 | 188 | You can optionally store your parameters in their own JSON file. To use this feature, specify the file path using the `--params` command-line argument, or as a `params` string property in your configuration file. Example of the latter: 189 | 190 | ```json 191 | { 192 | "url": "https://myserver.com/some/path?food=[food]", 193 | "params": "my_params_file.json" 194 | } 195 | ``` 196 | 197 | And then the contents of `my_params_file.json` would be: 198 | 199 | ```json 200 | { 201 | "food": ["apple", "orange", "banana"] 202 | } 203 | ``` 204 | 205 | Here is an example of specifying the parameters file using the command line: 206 | 207 | ```sh 208 | wperf https://myserver.com/some/path --params my_params_file.json 209 | ``` 210 | 211 | You can override individual parameters on the command line like this: 212 | 213 | ```sh 214 | wperf https://myserver.com/some/path --food orange 215 | ``` 216 | 217 | Another use of this is to make the hostname portion of the URL configurable, but have the rest of the URL be hard-coded or based on parameters in the config file. Example: 218 | 219 | ```json 220 | { 221 | "url": "https://[host][uri]", 222 | "params": { 223 | "host": [ 224 | "myserver.com" 225 | ], 226 | "uri": [ 227 | "/some/path?&action=eat&food=[food]", 228 | "/some/other/path?&action=drink&beverage=[beverage]" 229 | ], 230 | "food": ["apple", "orange", "banana"], 231 | "beverage": ["coke", "pepsi"] 232 | } 233 | } 234 | ``` 235 | 236 | Then you can customize just the hostname per test run like this: 237 | 238 | ```sh 239 | wperf my-config-file.json --host myOTHERserver.com 240 | ``` 241 | 242 | ### max 243 | 244 | The `max` parameter specifies the total number of HTTP requests to send (regardless of threads). You can specify this on the command-line or in a configuration file. The default is `1`. Example: 245 | 246 | ```sh 247 | wperf https://myserver.com/some/path --max 100 248 | ``` 249 | 250 | Example JSON configuration: 251 | 252 | ```json 253 | { 254 | "url": "https://myserver.com/some/path", 255 | "max": 100 256 | } 257 | ``` 258 | 259 | ### threads 260 | 261 | The `threads` parameter specifies the number of "threads" (i.e. concurrent HTTP requests) to send. You can specify this on the command-line or in a configuration file. The default is `1`. Example: 262 | 263 | ```sh 264 | wperf https://myserver.com/some/path --max 100 --threads 4 265 | ``` 266 | 267 | Example JSON configuration: 268 | 269 | ```json 270 | { 271 | "url": "https://myserver.com/some/path", 272 | "max": 100, 273 | "threads": 4 274 | } 275 | ``` 276 | 277 | ### keepalive 278 | 279 | The `keepalive` parameter, when present on the command-line or set to `true` in your JSON configuration, enables [HTTP Keep-Alives](https://en.wikipedia.org/wiki/HTTP_persistent_connection) for all requests. This means that sockets will be reused whenever possible (if the target server supports it and doesn't close the socket itself). The default behavior is to disable Keep-Alives, and open a new socket for every request. Example use: 280 | 281 | ```sh 282 | wperf https://myserver.com/some/path --max 100 --keepalive 283 | ``` 284 | 285 | Example JSON configuration: 286 | 287 | ```json 288 | { 289 | "url": "https://myserver.com/some/path", 290 | "max": 100, 291 | "keepalive": true 292 | } 293 | ``` 294 | 295 | Of course, Keep-Alives only take effect if you send more than one request. 296 | 297 | ### throttle 298 | 299 | The `throttle` parameter allows you to set a maximum requests per second limit, which the script will always stay under, regardless of the number of threads. You can specify this on the command-line or in a configuration file. The default is *unlimited*. Example use: 300 | 301 | ```sh 302 | wperf https://myserver.com/some/path --max 100 --throttle 10 303 | ``` 304 | 305 | Example JSON configuration: 306 | 307 | ```json 308 | { 309 | "url": "https://myserver.com/some/path", 310 | "max": 100, 311 | "throttle": 10 312 | } 313 | ``` 314 | 315 | ### timeout 316 | 317 | The `timeout` parameter allows you to specify a maximum time for requests in seconds, before they are aborted and considered an error. This is measured as the [time to first byte](https://en.wikipedia.org/wiki/Time_to_first_byte), and is specified as seconds. You can set this on the command-line or in a configuration file. The default is `5` seconds. The value can be a floating point decimal (fractional seconds). Example use: 318 | 319 | ```sh 320 | wperf https://myserver.com/some/path --timeout 2.5 321 | ``` 322 | 323 | Example JSON configuration: 324 | 325 | ```json 326 | { 327 | "url": "https://myserver.com/some/path", 328 | "timeout": 2.5 329 | } 330 | ``` 331 | 332 | ### warn 333 | 334 | The `warn` parameter allows you to specify a maximum time for requests in seconds, before they are logged as a warning. You can set this on the command-line or in a configuration file. The default is `1` second. The value can be a floating point decimal (fractional seconds). Example use: 335 | 336 | ```sh 337 | wperf https://myserver.com/some/path --warn 0.5 338 | ``` 339 | 340 | Example JSON configuration: 341 | 342 | ```json 343 | { 344 | "url": "https://myserver.com/some/path", 345 | "warn": 0.5 346 | } 347 | ``` 348 | 349 | Warnings are printed to STDERR, and contain a date/time stamp (local time), the request sequence number, the HTTP response code, and a JSON object containing the raw performance metrics (measured in milliseconds), along with the bytes sent & received, and the URL that was requested. Example: 350 | 351 | ``` 352 | [2019/08/31 16:46:20] Perf Warning: Req #1: HTTP 200 OK -- {"scale":1000,"perf":{"total":20.584,"dns":2.647,"send":0,"connect":0.477,"wait":13.68,"receive":1.102,"decompress":2.323},"counters":{"bytes_sent":151,"bytes_received":266},"url":"http://localhost:3012/rsleep?veg=celery"} 353 | ``` 354 | 355 | ### warnings 356 | 357 | If you would prefer warnings in a more machine-readable format, you can have them logged to a file using [newline delimited JSON](http://ndjson.org/) format. To enable this, include the `--warnings` command-line argument followed by a log file path, or use the `warnings` configuration property. Example use: 358 | 359 | ```sh 360 | wperf https://myserver.com/some/path --warn 0.5 --warnings /var/log/my-warning-log.ndjson 361 | ``` 362 | 363 | Example JSON configuration: 364 | 365 | ```json 366 | { 367 | "url": "https://myserver.com/some/path", 368 | "warn": 0.5, 369 | "warnings": "/var/log/my-warning-log.ndjson" 370 | } 371 | ``` 372 | 373 | Here is an example warning log entry (the JSON has been pretty-printed for display purposes): 374 | 375 | ```json 376 | { 377 | "perf": { 378 | "total": 34.992, 379 | "dns": 2.324, 380 | "send": 0, 381 | "connect": 0.423, 382 | "wait": 29.014, 383 | "receive": 0.801, 384 | "decompress": 2.168 385 | }, 386 | "counters": { 387 | "bytes_sent": 151, 388 | "bytes_received": 266 389 | }, 390 | "url": "http://localhost:3012/rsleep?veg=celery", 391 | "code": 200, 392 | "status": "OK", 393 | "req_num": 1, 394 | "now": 1567295312.123, 395 | "date_time": "[2019/08/31 16:48:32]" 396 | } 397 | ``` 398 | 399 | Here are descriptions of the properties: 400 | 401 | | Property | Description | 402 | |----------|-------------| 403 | | `perf` | This object contains the raw performance metrics for the request, which are all measured in milliseconds. See [Performance Metrics](https://github.com/jhuckaby/pixl-request#performance-metrics) for details. | 404 | | `counters` | This object contains raw counters, including `bytes_sent` and `bytes_received`, which count the number of bytes sent and received for the request, respectively. | 405 | | `url` | This is the URL that was requested. It is included because it may have been dynamically constructed with placeholder substitutions. | 406 | | `code` | This is the HTTP response code sent back from the server. | 407 | | `status` | This is the HTTP status line sent back from the server. | 408 | | `req_num` | This is the request sequence number (from one to [max](#max)). | 409 | | `now` | This is the current date/time expressed as [Epoch](https://en.wikipedia.org/wiki/Unix_time) seconds. | 410 | | `date_time` | This is the current date/time expressed as a string in the local timezone. | 411 | 412 | ### log 413 | 414 | Building upon [warnings](#warnings) discussed above, you can also tell wperf to log *every request* regardless if it is a warning or not. To do this, include the `--log` command-line argument followed by a log file path, or use the `log` configuration property. Example use: 415 | 416 | ```sh 417 | wperf https://myserver.com/some/path --warn 0.5 --log /var/log/my-req-log.ndjson 418 | ``` 419 | 420 | Example JSON configuration: 421 | 422 | ```json 423 | { 424 | "url": "https://myserver.com/some/path", 425 | "warn": 0.5, 426 | "log": "/var/log/my-req-log.ndjson" 427 | } 428 | ``` 429 | 430 | See [warnings](#warnings) above for details on the [NDJSON](http://ndjson.org/) logging format. 431 | 432 | ### fatal 433 | 434 | The `fatal` parameter, when present on the command-line or set to `true` in your JSON configuration, will cause the first HTTP error response to abort the entire run. By default this is disabled, and the script continues after encountering errors. Example use: 435 | 436 | ```sh 437 | wperf https://myserver.com/some/path --fatal 438 | ``` 439 | 440 | Example JSON configuration: 441 | 442 | ```json 443 | { 444 | "url": "https://myserver.com/some/path", 445 | "fatal": true 446 | } 447 | ``` 448 | 449 | ### verbose 450 | 451 | The `verbose` parameter, when present on the command-line or set to `true` in your JSON configuration, outputs information about every single request just as it completes. Example use: 452 | 453 | ```sh 454 | wperf https://myserver.com/some/path --verbose 455 | ``` 456 | 457 | Example JSON configuration: 458 | 459 | ```json 460 | { 461 | "url": "https://myserver.com/some/path", 462 | "verbose": true 463 | } 464 | ``` 465 | 466 | Verbose output looks like this: 467 | 468 | ``` 469 | [2019/08/31 16:42:54] Req #1: HTTP 200 OK -- {"scale":1000,"perf":{"total":24.139,"dns":2.044,"send":0,"connect":0.43,"wait":17.426,"receive":1.104,"decompress":2.802},"counters":{"bytes_sent":151,"bytes_received":266},"url":"http://localhost:3012/rsleep?veg=celery"} 470 | [2019/08/31 16:42:54] Req #2: HTTP 200 OK -- {"scale":1000,"perf":{"total":25.454,"dns":1.872,"send":0,"connect":0.402,"wait":20.285,"receive":0.171,"decompress":2.683},"counters":{"bytes_sent":152,"bytes_received":266},"url":"http://localhost:3012/rsleep?color=green"} 471 | [2019/08/31 16:42:54] Req #3: HTTP 200 OK -- {"scale":1000,"perf":{"total":35.035,"dns":2.317,"send":0,"connect":0.378,"wait":31.838,"receive":0.093,"decompress":0.393},"counters":{"bytes_sent":151,"bytes_received":266},"url":"http://localhost:3012/rsleep?veg=celery"} 472 | [2019/08/31 16:42:54] Req #4: HTTP 200 OK -- {"scale":1000,"perf":{"total":40.736,"dns":6.997,"send":0,"connect":0.476,"wait":32.247,"receive":0.074,"decompress":0.943},"counters":{"bytes_sent":150,"bytes_received":266},"url":"http://localhost:3012/rsleep?color=red"} 473 | [2019/08/31 16:42:54] Req #5: HTTP 200 OK -- {"scale":1000,"perf":{"total":10.893,"send":0.39,"wait":9.735,"receive":0.103,"decompress":0.641},"counters":{"bytes_sent":151,"bytes_received":264},"url":"http://localhost:3012/rsleep?veg=celery"} 474 | ``` 475 | 476 | Similar to the [warn](#warn) output, these lines contain a date/time stamp (local time), the request sequence number, the HTTP response code, and a JSON object containing the raw performance metrics (measured in milliseconds), along with the bytes sent & received, and the URL that was requested. 477 | 478 | ### cache_dns 479 | 480 | The `cache_dns` parameter, when present on the command-line or set to `true` in your JSON configuration, will cache the IP addresses from DNS lookups, so they only need to be requested once per unique domain name. Example use: 481 | 482 | ```sh 483 | wperf https://myserver.com/some/path --cache_dns 484 | ``` 485 | 486 | Example JSON configuration: 487 | 488 | ```json 489 | { 490 | "url": "https://myserver.com/some/path", 491 | "cache_dns": true 492 | } 493 | ``` 494 | 495 | ### auth 496 | 497 | The `auth` parameter allows you to include [HTTP Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) credentials (i.e. username and password). These should be delimited by a colon (`:`) character. You can set this on the command-line or in a configuration file. Example: 498 | 499 | ```sh 500 | wperf https://myserver.com/some/path --auth "jsmith:12345" 501 | ``` 502 | 503 | Example JSON configuration: 504 | 505 | ```json 506 | { 507 | "url": "https://myserver.com/some/path", 508 | "auth": "jsmith:12345" 509 | } 510 | ``` 511 | 512 | ### compress 513 | 514 | By default, the request library supports compressed server responses (i.e. Gzip or Deflate content encoding), and announces support for these via the `Accept-Encoding` header. The `compress` property allows you to *disable* compression support in the request library. Specifically, disabling compression means that an `Accept-Encoding: none` header is sent with every request, informing the target server that the client doesn't support a compressed response. You can set this property to `false` on the command-line or in a configuration file. Example: 515 | 516 | ```sh 517 | wperf https://myserver.com/some/path --compress false 518 | ``` 519 | 520 | Example JSON configuration: 521 | 522 | ```json 523 | { 524 | "url": "https://myserver.com/some/path", 525 | "compress": false 526 | } 527 | ``` 528 | 529 | **Note:** It is really up to the target server whether this header is followed or not. 530 | 531 | ### useragent 532 | 533 | The `useragent` parameter allows you to specify a custom `User-Agent` request header. By default, this is set to `Mozilla/5.0; wperf/1.0.0`. You can set this property on the command-line or in a configuration file. Example: 534 | 535 | ```sh 536 | wperf https://myserver.com/some/path --useragent "My Custom Agent v1.2.3" 537 | ``` 538 | 539 | Example JSON configuration: 540 | 541 | ```json 542 | { 543 | "url": "https://myserver.com/some/path", 544 | "useragent": "My Custom Agent v1.2.3" 545 | } 546 | ``` 547 | 548 | ### follow 549 | 550 | The `follow` parameter, when present on the command-line or set to `true` in your JSON configuration, will cause the HTTP request library to automatically follow redirects. That is, HTTP response codes in the `3xx` range, with an accompanying `Location` response header. You can set this property on the command-line or in a configuration file. Example: 551 | 552 | ```sh 553 | wperf https://myserver.com/some/path --follow 554 | ``` 555 | 556 | Example JSON configuration: 557 | 558 | ```json 559 | { 560 | "url": "https://myserver.com/some/path", 561 | "follow": true 562 | } 563 | ``` 564 | 565 | Alternatively, you can set this parameter to a number value, which represents the total amount of redirects to follow for a given request. This can help prevent infinite redirect loops. 566 | 567 | **Note:** The request library only tracks the performance metrics of the *final* request in the redirect chain. 568 | 569 | ### retries 570 | 571 | The `retries` parameter allows you to set a number of retries before an error is logged, and possibly [fatal](#fatal). The default is `0` retries. You can set this property on the command-line or in a configuration file. Example: 572 | 573 | ```sh 574 | wperf https://myserver.com/some/path --retries 5 575 | ``` 576 | 577 | Example JSON configuration: 578 | 579 | ```json 580 | { 581 | "url": "https://myserver.com/some/path", 582 | "retries": 5 583 | } 584 | ``` 585 | 586 | **Note:** If retries occur, the request library only tracks the performance metrics of the *final* request of each group. 587 | 588 | ### insecure 589 | 590 | By default, when HTTPS requests are made, the SSL certificate is verified using a Certificate Authority (CA). The `insecure` parameter, when present on the command-line or set to `true` in your JSON configuration, will cause the request library to bypass all SSL certificate verification. One potential use of this is for self-signed certificates. Example: 591 | 592 | ```sh 593 | wperf https://myserver.com/some/path --insecure 594 | ``` 595 | 596 | Example JSON configuration: 597 | 598 | ```json 599 | { 600 | "url": "https://myserver.com/some/path", 601 | "insecure": true 602 | } 603 | ``` 604 | 605 | ### headers 606 | 607 | The `headers` parameter allows you to add custom HTTP headers to every request. These can be specified on the command-line using a `h_` prefix followed by the header name, then the header value. You can repeat this for adding multiple headers. Example: 608 | 609 | ```sh 610 | wperf https://myserver.com/some/path --h_Cookie "sessionid=1234567890;" 611 | ``` 612 | 613 | If using a JSON configuration file, you can instead use a `headers` object, with the header names specified as the keys within. Example: 614 | 615 | ```json 616 | { 617 | "url": "https://myserver.com/some/path", 618 | "headers": { 619 | "Cookie": "sessionid=1234567890;" 620 | } 621 | } 622 | ``` 623 | 624 | You can also use placeholder macros in your header values, which will be expanded with matching [params](#params). Example: 625 | 626 | ```json 627 | { 628 | "url": "https://myserver.com/some/path", 629 | "headers": { 630 | "Cookie": "sessionid=[session_id];" 631 | }, 632 | "params": { 633 | "session_id": ["1234567890", "abcdefghij", "qrstuvwxyz"] 634 | } 635 | } 636 | ``` 637 | 638 | The underlying request library also adds a few basic headers of its own, including `Host`, `Connection`, `Accept-Encoding` and `User-Agent`. 639 | 640 | ### method 641 | 642 | The `method` parameter allows you to set the HTTP method for all requests sent. By default this is `GET`, but you can set it to any of the standard values, i.e. `GET`, `HEAD`, `POST`, `PUT` and `DELETE`. You can set this property on the command-line or in a configuration file. Example: 643 | 644 | ```sh 645 | wperf https://myserver.com/some/path --method HEAD 646 | ``` 647 | 648 | Example JSON configuration: 649 | 650 | ```json 651 | { 652 | "url": "https://myserver.com/some/path", 653 | "method": "HEAD" 654 | } 655 | ``` 656 | 657 | ### data 658 | 659 | If you select any of the HTTP methods that support a request body, e.g. `POST`, `PUT` or `DELETE`, you can specify the data in a number of different ways. By default, it is sent as `application/x-www-form-urlencoded`, and you can specify named parameters on the command-line using a `d_` prefix like this: 660 | 661 | ```sh 662 | wperf https://myserver.com/some/path --method POST --d_username jsmith --d_email jsmith@aol.com 663 | ``` 664 | 665 | This would serialize the two post parameters using standard form encoding, resulting in `username=jsmith&email=jsmith%40aol.com`. You can also specify these parameters in your JSON configuration file using the `data` property: 666 | 667 | ```json 668 | { 669 | "url": "https://myserver.com/some/path", 670 | "method": "POST", 671 | "data": { 672 | "username": "jsmith", 673 | "email": "jsmith@aol.com" 674 | } 675 | } 676 | ``` 677 | 678 | Alternatively, you can specify the request body in "raw" format using the `--data` command-line argument (or `data` configuration property) by setting it to a string. This can be used to send a pure JSON post, for example: 679 | 680 | ```sh 681 | wperf https://myserver.com/some/path --method POST --h_Content-Type application/json --data '{"username":"jsmith","email":"jsmith@aol.com"}' 682 | ``` 683 | 684 | Or similarly via configuration file: 685 | 686 | ```json 687 | { 688 | "url": "https://myserver.com/some/path", 689 | "method": "POST", 690 | "headers": { 691 | "Content-Type": "application/json" 692 | }, 693 | "data": "{\"username\":\"jsmith\",\"email\":\"jsmith@aol.com\"}" 694 | } 695 | ``` 696 | 697 | You can also use placeholder macros in your data, which will be expanded with matching [params](#params). Example: 698 | 699 | ```json 700 | { 701 | "url": "https://myserver.com/some/path", 702 | "method": "POST", 703 | "headers": { 704 | "Content-Type": "application/json" 705 | }, 706 | "data": "{\"username\":\"jsmith\",\"sessionid\":\"[session_id]\"}", 707 | "params": { 708 | "session_id": ["1234567890", "abcdefghij", "qrstuvwxyz"] 709 | } 710 | } 711 | ``` 712 | 713 | ### multipart 714 | 715 | To send a "multipart" HTTP POST, which is designed more for larger parameters (also see [files](#files)), include the `--multipart` parameter on the command-line, or set the `multipart` JSON configuration property to `true`. Example of this: 716 | 717 | ```sh 718 | wperf https://myserver.com/some/path --method POST --multipart --d_username jsmith --d_email jsmith@aol.com 719 | ``` 720 | 721 | And in JSON configuration format: 722 | 723 | ```json 724 | { 725 | "url": "https://myserver.com/some/path", 726 | "method": "POST", 727 | "multipart": true, 728 | "data": { 729 | "username": "jsmith", 730 | "email": "jsmith@aol.com" 731 | } 732 | } 733 | ``` 734 | 735 | ### files 736 | 737 | If you are performing a multipart HTTP POST, you can upload actual files from the local filesystem. These can be specified on the command-line using the `f_` prefix, or in a configuration file using a `files` object. Here are examples of each: 738 | 739 | ```sh 740 | wperf https://myserver.com/some/path --method POST --multipart --f_file1 /path/to/my/file.jpg 741 | ``` 742 | 743 | Example JSON configuration: 744 | 745 | ```json 746 | { 747 | "url": "https://myserver.com/some/path", 748 | "method": "POST", 749 | "multipart": true, 750 | "files": { 751 | "file1": "/path/to/my/file.jpg" 752 | } 753 | } 754 | ``` 755 | 756 | In this case the file is identified by the parameter name `file1`. The filename and content type are set automatically by the request library, based on the specified file path. 757 | 758 | ### success_match 759 | 760 | The `success_match` parameter allows you to specify a [regular expression](https://en.wikipedia.org/wiki/Regular_expression) that must match against the server response content body in order for the request to be considered a success. You can set this property on the command-line or in a configuration file. Example: 761 | 762 | ```sh 763 | wperf https://myserver.com/some/path --success_match "Operation was successful" 764 | ``` 765 | 766 | Example JSON configuration: 767 | 768 | ```json 769 | { 770 | "url": "https://myserver.com/some/path", 771 | "success_match": "Operation was successful" 772 | } 773 | ``` 774 | 775 | ### error_match 776 | 777 | The `error_match` parameter allows you to specify a [regular expression](https://en.wikipedia.org/wiki/Regular_expression) that generates an error when it matches the server response content body. You can set this property on the command-line or in a configuration file. Example: 778 | 779 | ```sh 780 | wperf https://myserver.com/some/path --error_match "Database failure" 781 | ``` 782 | 783 | Example JSON configuration: 784 | 785 | ```json 786 | { 787 | "url": "https://myserver.com/some/path", 788 | "error_match": "Database failure" 789 | } 790 | ``` 791 | 792 | ### histo 793 | 794 | Normally `wperf` outputs a histogram of the "Total Time" metric, which is the full HTTP request from beginning to end. However, you can customize this, and set it to generate multiple histograms of different HTTP metrics, including: 795 | 796 | | Metric | Description | 797 | |--------|-------------| 798 | | `dns` | Time to resolve the hostname to an IP address via DNS. This is omitted if cached, or you specify an IP on the URL. | 799 | | `connect` | Time to connect to the remote socket (omitted if using Keep-Alives and reusing a host). | 800 | | `send` | Time to send the request data (typically for POST / PUT). Also includes SSL handshake time (if HTTPS). | 801 | | `wait` | Time spent waiting for the server response (after request is sent). | 802 | | `receive` | Time spent downloading data from the server (after headers received). | 803 | | `decompress` | Time taken to decompress the response (if encoded with Gzip or Deflate). | 804 | | `total` | Total time of the entire HTTP transaction. | 805 | 806 | To specify additional histograms, include the `--histo` argument on the command-line, set to a comma-separate list of any of the metrics defined above. Or you can use the special keyword `all` to generate histograms for *all* of them. Example: 807 | 808 | ```sh 809 | wperf https://myserver.com/some/path --histo "dns,connect,receive" 810 | ``` 811 | 812 | If you are using a JSON configuration file, you can set the `histo` property to an array instead of a CSV string: 813 | 814 | ```json 815 | { 816 | "url": "https://myserver.com/some/path", 817 | "histo": ["dns", "connect", "receive"] 818 | } 819 | ``` 820 | 821 | See this [screenshot](https://pixlcore.com/software/wperf/terminal.png) for an example histogram. 822 | 823 | ### histo_ranges 824 | 825 | By default, the histograms generated by `wperf` include metrics in the following time ranges: 826 | 827 | - 0-1 ms 828 | - 1-2 ms 829 | - 2-3 ms 830 | - 3-4 ms 831 | - 4-5 ms 832 | - 5-10 ms 833 | - 10-20 ms 834 | - 20-30 ms 835 | - 30-40 ms 836 | - 40-50 ms 837 | - 50-100 ms 838 | - 100-200 ms 839 | - 200-300 ms 840 | - 300-400 ms 841 | - 400-500 ms 842 | - 500-1000 ms 843 | - 1-2 sec 844 | - 2-3 sec 845 | - 3-4 sec 846 | - 4-5 sec 847 | - 5+ sec 848 | 849 | You can customize these lanes by specifying a `histo_ranges` key in your JSON configuration file. This should be set to an array of strings, and the format of the strings needs to match `#-# ms`, `#+ ms`, `#-# sec` or `#+ sec`. Example: 850 | 851 | ```json 852 | { 853 | "url": "https://myserver.com/some/path", 854 | "histo_ranges": [ 855 | "0-1 ms", 856 | "1-2 ms", 857 | "2-3 ms", 858 | "3-4 ms", 859 | "4-5 ms", 860 | "5-10 ms", 861 | "10-20 ms", 862 | "20-30 ms", 863 | "30-40 ms", 864 | "40-50 ms", 865 | "50-100 ms", 866 | "100-200 ms", 867 | "200-300 ms", 868 | "300-400 ms", 869 | "400-500 ms", 870 | "500-1000 ms", 871 | "1-2 sec", 872 | "2-3 sec", 873 | "3-4 sec", 874 | "4-5 sec", 875 | "5+ sec" 876 | ] 877 | } 878 | ``` 879 | 880 | ### stats 881 | 882 | If you would like `wperf` to output more machine-readable statistics, you can do so by adding the `--stats` command-line argument, or the `stats` configuration property. Example use: 883 | 884 | ```sh 885 | wperf https://myserver.com/some/path --stats 886 | ``` 887 | 888 | Example JSON configuration: 889 | 890 | ```json 891 | { 892 | "url": "https://myserver.com/some/path", 893 | "stats": true 894 | } 895 | ``` 896 | 897 | You can alternatively have the statistics appended to a file, instead of printed to the console. To do this, simply include the destination target filename after `--stats` on the command-line, or set the `stats` property to a filename in the JSON configuration file. 898 | 899 | Example output (pretty-printed for display purposes): 900 | 901 | ```json 902 | { 903 | "current_sec": 1567391879, 904 | "count_sec": 39, 905 | "peak_sec": 61, 906 | "total_reqs": 100, 907 | "total_warnings": 0, 908 | "total_errors": 0, 909 | "bytes_sent": 58300, 910 | "bytes_received": 28900, 911 | "time_start": 1567391878.592, 912 | "total": { 913 | "min": 4.689, 914 | "max": 28.387, 915 | "total": 633.4549999999998, 916 | "count": 100, 917 | "avg": 6.334549999999998 918 | }, 919 | "dns": { 920 | "min": 0.59, 921 | "max": 16.118, 922 | "total": 98.68200000000003, 923 | "count": 100, 924 | "avg": 0.9868200000000003 925 | }, 926 | "connect": { 927 | "min": 0.177, 928 | "max": 0.934, 929 | "total": 22.475, 930 | "count": 100, 931 | "avg": 0.22475 932 | }, 933 | "send": { 934 | "min": 0.967, 935 | "max": 1.873, 936 | "total": 110.59200000000003, 937 | "count": 100, 938 | "avg": 1.1059200000000002 939 | }, 940 | "wait": { 941 | "min": 2.491, 942 | "max": 8.302, 943 | "total": 361.1120000000002, 944 | "count": 100, 945 | "avg": 3.611120000000002 946 | }, 947 | "receive": { 948 | "min": 0.054, 949 | "max": 1.07, 950 | "total": 7.630000000000001, 951 | "count": 100, 952 | "avg": 0.0763 953 | }, 954 | "decompress": { 955 | "min": 0.192, 956 | "max": 2.288, 957 | "total": 30.935, 958 | "count": 100, 959 | "avg": 0.30935 960 | } 961 | } 962 | ``` 963 | 964 | Here are descriptions of the properties: 965 | 966 | | Property | Description | 967 | |----------|-------------| 968 | | `current_sec` | Date/time of the current second, expressed as [Epoch](https://en.wikipedia.org/wiki/Unix_time) seconds. | 969 | | `count_sec` | The number of requests completed during the current second (used to compute req/sec). | 970 | | `peak_sec` | The requests per second during the peak (best performing) second. | 971 | | `total_reqs` | The total number of requests sent. | 972 | | `total_warnings` | The total number of warnings received. | 973 | | `total_errors` | The total number of errors received. | 974 | | `bytes_sent` | The total number of bytes sent. | 975 | | `bytes_received` | The total number of bytes received. | 976 | | `time_start` | The date/time of when the run started, expressed as [Epoch](https://en.wikipedia.org/wiki/Unix_time) seconds. | 977 | | `total` | Raw HTTP performance metrics for the total request time (see below). | 978 | | `dns` | Raw HTTP performance metrics for the DNS lookup time (see below).| 979 | | `connect` | Raw HTTP performance metrics for the TCP connect time (see below). | 980 | | `send` | Raw HTTP performance metrics for the data send time (see below). | 981 | | `wait` | Raw HTTP performance metrics for the request wait time (see below). | 982 | | `receive` | Raw HTTP performance metrics for the data receive time (see below). | 983 | | `decompress` | Raw HTTP performance metrics for the decompress time (see below). | 984 | 985 | Each of the HTTP performance objects contain the following properties: 986 | 987 | | Property | Description | 988 | |----------|-------------| 989 | | `min` | The minimum time recorded for the metric, in milliseconds. | 990 | | `max` | The maximum time recorded for the metric, in milliseconds. | 991 | | `total` | The total of all time measurements added together (used to compute the average), in milliseconds. | 992 | | `count` | The total number of samples recorded for the metric. | 993 | | `avg` | The average time for the metric (`total` divided by `count`), in milliseconds. | 994 | 995 | ### quiet 996 | 997 | To suppress all `wperf` script output (with some exceptions -- see below), include the `--quiet` argument on the command-line, or set the `quiet` JSON configuration property to `true`. Example: 998 | 999 | ```sh 1000 | wperf https://myserver.com/some/path --quiet 1001 | ``` 1002 | 1003 | Example JSON configuration: 1004 | 1005 | ```json 1006 | { 1007 | "url": "https://myserver.com/some/path", 1008 | "quiet": true 1009 | } 1010 | ``` 1011 | 1012 | This will cause all output to be suppressed, with these two exceptions: 1013 | 1014 | - If the script detects that it is running on an actual terminal, vs. being called from a script or pipe, then the progress bar is displayed. 1015 | - If [stats](#stats) mode is activated, this busts through quiet mode (the assumption is, if you asked for stats, you want stats). 1016 | 1017 | ### color 1018 | 1019 | By default, `wperf` outputs its reports and tables using ANSI colors in your terminal. However, if for some reason you don't want color output, you can disable it by either setting the `--color` command-line argument to `false`, or setting the `color` JSON configuration property to `false`. Example: 1020 | 1021 | ```sh 1022 | wperf https://myserver.com/some/path --color false 1023 | ``` 1024 | 1025 | Example JSON configuration: 1026 | 1027 | ```json 1028 | { 1029 | "url": "https://myserver.com/some/path", 1030 | "color": false 1031 | } 1032 | ``` 1033 | 1034 | ### wrapper (Advanced) 1035 | 1036 | **Note:** This is an advanced feature, which requires Node.js programming knowledge. 1037 | 1038 | `wperf` allows you to "wrap" (hook) the internal request cycle, to run your own Node.js function and perform actions before and/or after each request. Using this you can manipulate or generate your own URLs in code, and/or filter or change the HTTP responses as well. To do this, you will have to write your own Node.js script. 1039 | 1040 | The feature is activated by the `--wrapper` command-line argument, or the `wrapper` JSON configuration property. Either way, it should be set to a filesystem path pointing at your Node.js script. Example: 1041 | 1042 | ```sh 1043 | wperf https://myserver.com/some/path --wrapper /path/to/my-request-wrapper.js 1044 | ``` 1045 | 1046 | Example JSON configuration: 1047 | 1048 | ```json 1049 | { 1050 | "url": "https://myserver.com/some/path", 1051 | "wrapper": "/path/to/my-request-wrapper.js" 1052 | } 1053 | ``` 1054 | 1055 | Your Node.js script should export a class with the following definition: 1056 | 1057 | ```js 1058 | module.exports = class RequestWrapper { 1059 | 1060 | constructor(request, args) { 1061 | // class constructor, save request and args for later use 1062 | this.request = request; 1063 | this.args = args; 1064 | } 1065 | 1066 | get(url, opts, callback) { 1067 | // called for every `GET` request 1068 | // callback expects (err, resp, data, perf) 1069 | this.request.get( url, opts, callback ); 1070 | } 1071 | 1072 | }; 1073 | ``` 1074 | 1075 | #### Wrapper Constructor 1076 | 1077 | Your class constructor is passed exactly two arguments: 1078 | 1079 | - A reference to the request library, which is an instance of [pixl-request](https://github.com/jhuckaby/pixl-request). 1080 | - The current configuration object (usually parsed command-line arguments, but may also include a parsed JSON configuration file). 1081 | 1082 | It is recommended that you store both of these in your class for later use. 1083 | 1084 | #### Wrapper Method Hook 1085 | 1086 | For each HTTP request, your wrapper class is invoked by calling a method named after the current HTTP method, converted to lower-case. For example, if the current configuration is set to send HTTP GET requests, then your wrapper will need to have a `get()` method defined. For HTTP POST, you will need a `post()` method, and so on. In all cases, the function is passed 3 arguments: 1087 | 1088 | - The current URL, which may have been dynamically generated using placeholder substitution variables. 1089 | - The current request options object (see [http.request](https://nodejs.org/api/http.html#http_http_request_options_callback) for all the properties you can set here). 1090 | - A callback function to fire when the request completes. 1091 | 1092 | Upon the completion of each request, success or fail, the callback function expects four arguments: 1093 | 1094 | - An Error object, if an error occurred (otherwise this should be falsey). 1095 | - The HTTP response object, which should be a [http.ServerResponse](https://nodejs.org/api/http.html#http_class_http_serverresponse) object from Node.js. 1096 | - The HTTP response body, as a Buffer object. 1097 | - The performance metrics, which should be an instance of [pixl-perf](https://github.com/jhuckaby/pixl-perf). 1098 | 1099 | # Related 1100 | 1101 | - [pixl-request](https://github.com/jhuckaby/pixl-request) 1102 | - Underlying HTTP library used to send all requests and track detailed performance metrics. 1103 | - [pixl-cli](https://github.com/jhuckaby/pixl-cli) 1104 | - Command-line library, used to generate the progress bar, time remaining and colored tables. 1105 | 1106 | # License (MIT) 1107 | 1108 | **The MIT License** 1109 | 1110 | *Copyright (c) 2019 - 2025 Joseph Huckaby.* 1111 | 1112 | Permission is hereby granted, free of charge, to any person obtaining a copy 1113 | of this software and associated documentation files (the "Software"), to deal 1114 | in the Software without restriction, including without limitation the rights 1115 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 1116 | copies of the Software, and to permit persons to whom the Software is 1117 | furnished to do so, subject to the following conditions: 1118 | 1119 | The above copyright notice and this permission notice shall be included in 1120 | all copies or substantial portions of the Software. 1121 | 1122 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 1123 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 1124 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 1125 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 1126 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 1127 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 1128 | THE SOFTWARE. 1129 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wperf", 3 | "version": "1.0.11", 4 | "description": "A simple HTTP load testing utility with detailed performance metrics.", 5 | "author": "Joseph Huckaby ", 6 | "homepage": "https://github.com/jhuckaby/wperf", 7 | "license": "MIT", 8 | "main": "wperf.js", 9 | "bin": "wperf.js", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/jhuckaby/wperf" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/jhuckaby/wperf/issues" 16 | }, 17 | "keywords": [ 18 | "http", 19 | "performance", 20 | "test" 21 | ], 22 | "dependencies": { 23 | "pixl-cli": "^1.0.0", 24 | "pixl-request": "^1.0.0" 25 | }, 26 | "devDependencies": {} 27 | } 28 | -------------------------------------------------------------------------------- /wperf.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Simple CLI HTTP Test Script 4 | // Requests a URL via HTTP GET continuously, up to N iterations using N threads 5 | // Usage: wperf URL --max 1000 --threads 1 --cache_dns 0 --keepalive 0 --timeout 5 --verbose 0 --warn 1.0 6 | // Copyright (c) 2016 - 2019 by Joseph Huckaby, MIT License 7 | 8 | /* 9 | Usage: wperf URL [OPTIONS...] 10 | 11 | --max 1000 Total number of requests to send. 12 | --threads 1 Number of concurrent threads to use. 13 | --keepalive 0 Use HTTP Keep-Alive sockets. 14 | --throttle 0 Limit request rate to N per sec. 15 | --timeout 5 Timeout for each request in seconds. 16 | --fatal 0 Abort on first HTTP error. 17 | --verbose 0 Print metrics for every request. 18 | --warn 1.0 Emit warnings at N seconds and longer. 19 | --cache_dns 0 Cache DNS for duration of run. 20 | --compress 1 Allow compressed responses. 21 | --follow Follow HTTP 3xx redirects. 22 | --retries 5 Retry errors N times. 23 | --auth "U:P" HTTP Basic Auth (username:password). 24 | --useragent "Foo" Custom User-Agent string. 25 | --h_X-Test "foo" Add any HTTP request headers. 26 | --method get Specify HTTP request method. 27 | --f_file1 "1.txt" Attach file to HTTP POST request. 28 | --data "foo=bar" Provide raw HTTP POST data. 29 | 30 | Hit Ctrl-Z during run to see progress reports. 31 | For more info: https://github.com/jhuckaby/wperf 32 | */ 33 | 34 | // Capture stdin in raw mode for Ctrl-Z 35 | var readline = require('readline'); 36 | if (process.stdin.isTTY) { 37 | readline.emitKeypressEvents(process.stdin); 38 | process.stdin.setRawMode(true); 39 | } 40 | 41 | var fs = require('fs'); 42 | var Path = require('path'); 43 | var querystring = require('querystring'); 44 | var package = require('./package.json'); 45 | var PixlRequest = require('pixl-request'); 46 | var cli = require('pixl-cli'); 47 | cli.global(); 48 | 49 | var Tools = cli.Tools; 50 | var async = Tools.async; 51 | 52 | // Process CLI args 53 | var args = cli.args; 54 | if (!args.other || !args.other.length || args.help) { 55 | fs.readFileSync( process.argv[1], 'utf8' ).match(/([\S\s]+)<\/Help>/); 56 | print( RegExp.$1 + "\n" ); 57 | process.exit(0); 58 | } 59 | var url = args.other.shift(); 60 | var config_file = ''; 61 | 62 | // first argument may be config file 63 | if (!url.match(/^\w+\:\/\//) && fs.existsSync(url)) { 64 | config_file = url; 65 | var config = null; 66 | try { config = JSON.parse( fs.readFileSync(url), 'utf8' ); } 67 | catch (err) { 68 | die("Failed to read configuration file: " + url + ": " + err + "\n"); 69 | } 70 | if (!config.url) { 71 | die("Configuration file is missing required 'url' property: " + url + "\n"); 72 | } 73 | url = args.url || config.url; 74 | for (var key in config) { 75 | if (!(key in args)) args[key] = config[key]; 76 | } 77 | } 78 | 79 | if (args.params && (typeof(args.params) == 'string')) { 80 | // params may live in a separate file 81 | var params_file = args.params; 82 | try { args.params = JSON.parse( fs.readFileSync(params_file), 'utf8' ); } 83 | catch (err) { 84 | die("Failed to read parameters file: " + params_file + ": " + err + "\n"); 85 | } 86 | } 87 | 88 | // support string "false" as boolean false in certain cases 89 | if (args.compress === "false") args.compress = false; 90 | if (args.color === "false") args.color = false; 91 | 92 | var max_iter = args.max || 1; 93 | var max_threads = args.threads || 1; 94 | var timeout_sec = ("timeout" in args) ? args.timeout : 5.0; 95 | var warn_sec = ("warn" in args) ? args.warn : 1.0; 96 | var warn_ms = warn_sec * 1000; 97 | var allow_compress = ("compress" in args) ? args.compress : 1; 98 | var keep_alive = args.keepalive || args.keepalives || false; 99 | var method = (args.method || 'get').toLowerCase(); 100 | 101 | // optionally disable all ANSI color 102 | if (("color" in args) && !args.color) { 103 | cli.chalk.enabled = false; 104 | } 105 | 106 | print("\n"); 107 | print( bold.magenta("WebPerf (wperf) v" + package.version) + "\n" ); 108 | print( gray.bold("Date/Time: ") + gray((new Date()).toString() ) + "\n" ); 109 | 110 | if (config_file) { 111 | print( gray.bold("Configuration: ") + gray(config_file) + "\n" ); 112 | } 113 | 114 | if (args.params) print( gray.bold("Base ")); 115 | print( gray.bold("URL: ") + gray(url + " (" + method.toUpperCase() + ")") + "\n" ); 116 | 117 | // print( gray( bold("Method: ") + method.toUpperCase()) + "\n" ); 118 | print( gray.bold("Keep-Alives: ") + gray(keep_alive ? 'Enabled' : 'Disabled') + "\n" ); 119 | print( gray.bold("Threads: ") + gray(max_threads) + "\n" ); 120 | 121 | // setup histogram system 122 | var histo = {}; 123 | 124 | histo.cats = args.histo || ['total']; 125 | if (histo.cats === 'all') histo.cats = ['dns', 'connect', 'send', 'wait', 'receive', 'decompress', 'total']; 126 | if (typeof(histo.cats) == 'string') histo.cats = histo.cats.split(/\,\s*/); 127 | 128 | histo.counts = {}; 129 | histo.cats.forEach( function(key) { 130 | histo.counts[key] = {}; 131 | }); 132 | 133 | histo.ranges = args.histo_ranges || [ 134 | '0-1 ms', 135 | '1-2 ms', 136 | '2-3 ms', 137 | '3-4 ms', 138 | '4-5 ms', 139 | '5-10 ms', 140 | '10-20 ms', 141 | '20-30 ms', 142 | '30-40 ms', 143 | '40-50 ms', 144 | '50-100 ms', 145 | '100-200 ms', 146 | '200-300 ms', 147 | '300-400 ms', 148 | '400-500 ms', 149 | '500-1000 ms', 150 | '1-2 sec', 151 | '2-3 sec', 152 | '3-4 sec', 153 | '4-5 sec', 154 | '5+ sec' 155 | ]; 156 | 157 | histo.groups = histo.ranges.map( function(label) { 158 | var low = 0, high = 0; 159 | if (label.match(/(\d+)\-(\d+)\s*(\w+)$/)) { 160 | low = parseInt( RegExp.$1 ); 161 | high = parseInt( RegExp.$2 ); 162 | if (RegExp.$3 == 'sec') { low *= 1000; high *= 1000; } 163 | } 164 | else if (label.match(/^(\d+)\+\s*(\w+)$/)) { 165 | low = parseInt( RegExp.$1 ); 166 | high = 86400; 167 | if (RegExp.$2 == 'sec') { low *= 1000; high *= 1000; } 168 | } 169 | return { low, high, label }; 170 | }); 171 | 172 | // request options 173 | var opts = { 174 | timeout: timeout_sec * 1000, 175 | headers: args.headers || {} 176 | }; 177 | if (args.auth) opts.auth = args.auth; 178 | if (args.multipart) opts.multipart = true; 179 | 180 | // Custom headers with h_ prefix, e.g. --h_Cookie "dtuid=1000000000000000001" 181 | for (var key in args) { 182 | if (key.match(/^h_(.+)$/)) { 183 | var header_name = RegExp.$1; 184 | opts.headers[ header_name ] = args[key]; 185 | } 186 | } 187 | 188 | // Custom file uploads with f_ prefix, e.g. f_file1 "1.txt" 189 | if (args.files) { 190 | method = 'post'; 191 | opts.multipart = true; 192 | opts.files = args.files; 193 | } 194 | for (var key in args) { 195 | if (key.match(/^f_(.+)$/)) { 196 | var param_name = RegExp.$1; 197 | method = 'post'; 198 | opts.multipart = true; 199 | if (!opts.files) opts.files = {}; 200 | opts.files[ param_name ] = args[key]; 201 | } 202 | } 203 | 204 | // Custom raw HTTP POST data, or d_ prefix 205 | if (args.data) { 206 | opts.data = args.data; 207 | if (!opts.headers['Content-Type'] && !opts.headers['content-type'] && (typeof(opts.data) != 'object')) { 208 | // convert data to hash, for application/x-www-form-urlencoded mode 209 | opts.data = querystring.parse(opts.data); 210 | } 211 | } 212 | for (var key in args) { 213 | if (key.match(/^d_(.+)$/)) { 214 | var param_name = RegExp.$1; 215 | method = 'post'; 216 | if (!opts.data) opts.data = {}; 217 | opts.data[ param_name ] = args[key]; 218 | } 219 | } 220 | 221 | // Custom param override using p_ prefix, or if a param already exists with the same name, no prefix is needed 222 | for (var key in args) { 223 | if (key.match(/^p_(.+)$/)) { 224 | var param_name = RegExp.$1; 225 | if (!args.params) args.params = {}; 226 | args.params[ param_name ] = args[key]; 227 | } 228 | else if (args.params && args.params[key]) { 229 | args.params[ key ] = args[key]; 230 | } 231 | } 232 | 233 | // Set User-Agent string, allow full customization 234 | var request = new PixlRequest( args.useragent || ("Mozilla/5.0; wperf/" + package.version) ); 235 | request.setAutoError( true ); 236 | 237 | // Optionally use HTTP Keep-Alives 238 | if (keep_alive) { 239 | request.setKeepAlive( true ); 240 | } 241 | 242 | // Optionally cache DNS lookups 243 | if (args.cache_dns) { 244 | request.setDNSCache( 86400 ); 245 | } 246 | 247 | // Optionally disable compression support 248 | if (!allow_compress) { 249 | request.setHeader( 'Accept-Encoding', "none" ); 250 | } 251 | 252 | // Optionally follow redirects 253 | if (args.follow) { 254 | request.setFollow( args.follow ); 255 | } 256 | 257 | // Optionally retry errors 258 | if (args.retries) { 259 | if (args.retries === true) args.retries = 1; 260 | request.setRetries( args.retries ); 261 | } 262 | 263 | if (args.insecure) { 264 | // Allow this to work with HTTPS when the SSL certificate cannot be verified 265 | // process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; 266 | opts.rejectUnauthorized = false; 267 | } 268 | 269 | // Keep track of stats 270 | var num_warns = 0; 271 | var count = 0; 272 | var stats = { 273 | // Note: sharing namespace with pixl-request perf here 274 | current_sec: Tools.timeNow(true), 275 | count_sec: 0, 276 | peak_sec: 0, 277 | total_reqs: 0, 278 | total_warnings: 0, 279 | total_errors: 0, 280 | bytes_sent: 0, 281 | bytes_received: 0, 282 | time_start: Tools.timeNow() 283 | }; 284 | 285 | print( "\n" ); 286 | 287 | // Begin progress bar 288 | cli.progress.start({ 289 | catchInt: true, 290 | catchTerm: true, 291 | catchCrash: false, 292 | exitOnSig: false 293 | }); 294 | 295 | var floatCheckWarn = function(value) { 296 | // prepare float value (milliseconds) for display, using shortFloat 297 | // also highlight with bold + red if over the warning threshold 298 | var is_warning = !!(warn_ms && (value >= warn_ms)); 299 | return is_warning ? bold.red(shortFloat(value) + ' ms') : (shortFloat(value) + ' ms'); 300 | }; 301 | 302 | var dateTimeStamp = function(epoch) { 303 | // return date/time stamp in [YYYY-MM-DD HH:MI:SS] format 304 | var dargs = Tools.getDateArgs( epoch || Tools.timeNow(true) ); 305 | return "[" + dargs.yyyy_mm_dd + " " + dargs.hh_mi_ss + "] "; 306 | }; 307 | 308 | var printReport = function() { 309 | // Show final stats in table 310 | if (cli.progress.running) cli.progress.erase(); 311 | 312 | // general stats 313 | var now = Tools.timeNow(); 314 | var elapsed = now - stats.time_start; 315 | var elapsed_disp = Tools.getTextFromSeconds(elapsed, false, false); 316 | if (elapsed < 1) elapsed_disp = Math.floor(elapsed * 1000) + " ms"; 317 | 318 | if (args.verbose || num_warns) { 319 | print("\n"); 320 | } 321 | print( bold.yellow("Total requests sent: ") + commify(stats.total_reqs) + "\n" ); 322 | print( bold.yellow("Total time elapsed: ") + elapsed_disp + "\n" ); 323 | print( bold.yellow("Total bytes sent: ") + Tools.getTextFromBytes(stats.bytes_sent) + " (" + Tools.getTextFromBytes(stats.bytes_sent / elapsed) + "/sec)\n" ); 324 | print( bold.yellow("Total bytes received: ") + Tools.getTextFromBytes(stats.bytes_received) + " (" + Tools.getTextFromBytes(stats.bytes_received / elapsed) + "/sec)\n" ); 325 | 326 | print( "\n" ); 327 | print( bold.yellow("Average performance: ") + commify( Math.floor(stats.total_reqs / elapsed) ) + " req/sec\n" ); 328 | if (stats.peak_sec && (elapsed >= 2.0)) { 329 | print( bold.yellow("Peak performance: ") + commify(stats.peak_sec) + " req/sec\n" ); 330 | } 331 | 332 | print( "\n" ); 333 | var err_color = stats.total_errors ? bold.red : bold.yellow; 334 | print( bold.yellow("Number of warnings: ") + commify(stats.total_warnings) + "\n" ); 335 | print( err_color("Number of errors: ") + commify(stats.total_errors) + "\n" ); 336 | 337 | var rows = [ 338 | ["Metric", "Minimum", "Average", "Maximum", "Samples"] 339 | ]; 340 | var labels = ['DNS', 'Connect', 'Send', 'Wait', 'Receive', 'Decompress', 'Total']; 341 | 342 | labels.forEach( function(label) { 343 | var key = label.toLowerCase(); 344 | var is_total = (key == 'total'); 345 | var color = is_total ? cyan : green; 346 | var stat = stats[key] || {}; 347 | stat.avg = stat.total / (stat.count || 1); 348 | 349 | rows.push([ 350 | color( bold( label ) ), 351 | color( floatCheckWarn( stat.min || 0 ) ), 352 | color( floatCheckWarn( stat.avg || 0 ) ), 353 | color( floatCheckWarn( stat.max || 0 ) ), 354 | color( commify( stat.count || 0 ) ) 355 | ]); 356 | } ); // forEach 357 | 358 | print( "\n" ); 359 | print( bold("Performance Metrics:") + "\n" ); 360 | print( table(rows, { textStyles: ["green"] }) + "\n" ); 361 | 362 | // histograms 363 | labels.forEach( function(label) { 364 | var key = label.toLowerCase(); 365 | if (!(key in histo.counts)) return; 366 | 367 | var counts = histo.counts[key]; 368 | var rows = [ 369 | ["Range", "Count", "Visual"] 370 | ]; 371 | 372 | // find highest count (for visual max) 373 | var highest = 0; 374 | for (var range in counts) { 375 | if (counts[range] > highest) highest = counts[range]; 376 | } 377 | 378 | histo.ranges.forEach( function(range) { 379 | var value = counts[range] || 0; 380 | var bar = ""; 381 | var width = Math.max(0, Math.min(value / highest, 1.0)) * (args.width || 40); 382 | var partial = width - Math.floor(width); 383 | 384 | bar += cli.repeat(cli.progress.defaults.filled, Math.floor(width)); 385 | if (partial > 0) { 386 | bar += cli.progress.defaults.filling[ Math.floor(partial * cli.progress.defaults.filling.length) ]; 387 | } 388 | 389 | rows.push([ 390 | range, 391 | Tools.commify(value), 392 | bar 393 | ]); 394 | }); 395 | 396 | print("\n"); 397 | print( bold(label + " Time Histogram:") + "\n" ); 398 | print( table(rows, {}) + "\n" ); 399 | }); // histos 400 | 401 | print( "\n" ); 402 | 403 | if (cli.progress.running) cli.progress.draw(); 404 | }; 405 | 406 | var nextIteration = function(err, callback) { 407 | // check for error, apply throttling, proceed to next iteration 408 | if (err) { 409 | stats.total_errors++; 410 | 411 | if (args.fatal) return callback(err); 412 | else { 413 | cli.progress.erase(); 414 | if (args.verbose) print("\n"); 415 | print( dateTimeStamp() + bold.red("ERROR: ") + err.message + "\n" ); 416 | if (args.verbose && err.url) { 417 | print( dateTimeStamp() + bold.yellow("URL: ") + err.url + "\n" ); 418 | } 419 | if (args.verbose && err.content) { 420 | print( dateTimeStamp() + bold.yellow("Content: ") + JSON.stringify(err.content) + "\n" ); 421 | } 422 | if (args.verbose && err.headers) { 423 | print( dateTimeStamp() + bold.yellow("Headers: ") + JSON.stringify(err.headers) + "\n" ); 424 | } 425 | if (args.verbose && err.perf) { 426 | var metrics = err.perf.metrics(); 427 | print( dateTimeStamp() + bold.yellow("Perf: ") + JSON.stringify(metrics) + "\n" ); 428 | } 429 | cli.progress.draw(); 430 | } 431 | } 432 | 433 | if (args.throttle && (stats.count_sec >= args.throttle)) { 434 | // whoa there, slow down a bit 435 | var cur_sec = stats.current_sec; 436 | async.whilst( 437 | function() { return (Tools.timeNow(true) == cur_sec); }, 438 | function(callback) { setTimeout( function() { callback(); }, 50 ); }, 439 | callback 440 | ); 441 | } 442 | else { 443 | callback(); 444 | } 445 | }; 446 | 447 | // Catch term, int, abort the run 448 | var emergency_abort = false; 449 | 450 | process.once('SIGINT', function() { 451 | if (cli.progress.running) cli.progress.end(); 452 | emergency_abort = true; 453 | // process.stdin.end(); 454 | } ); 455 | process.once('SIGTERM', function() { 456 | if (cli.progress.running) cli.progress.end(); 457 | emergency_abort = true; 458 | // process.stdin.end(); 459 | } ); 460 | 461 | // Capture Ctrl-Z to emit progress reports 462 | process.stdin.on('keypress', function (ch, key) { 463 | if (key && key.ctrl && key.name == 'z') { 464 | printReport(); 465 | } 466 | if (key && key.ctrl && key.name == 'c') { 467 | emergency_abort = true; 468 | } 469 | }); 470 | 471 | // Allow CLI to provide a module that wraps the request object. 472 | // The module must implement METHOD(url, opts, callback) and callback with (err, resp, data, perf) 473 | // Where METHOD is one of `get`, `post`, `head`, `put` or `delete`, depending on config. 474 | if (args.wrapper) { 475 | var reqWrapper = require( args.wrapper.match(/^\//) ? args.wrapper : Path.join(process.cwd(), args.wrapper) ); 476 | request = new reqWrapper(request, args); 477 | } 478 | 479 | var req_per_sec = 0; 480 | var success_match = args.success_match ? (new RegExp(args.success_match)) : null; 481 | var error_match = args.error_match ? (new RegExp(args.error_match)) : null; 482 | var params = args.params || {}; 483 | 484 | for (var key in params) { 485 | params[key] = Tools.alwaysArray( params[key] ); 486 | } 487 | 488 | // Main request loop 489 | async.timesLimit( max_iter, max_threads, 490 | function(idx, callback) { 491 | var current_opts = Tools.copyHash( opts, true ); 492 | var current_url = url; 493 | var ebrake = 0; 494 | 495 | // apply placeholder substitution on URL 496 | while (current_url.match(/\[(\w+|\d+\-\d+)\]/)) { 497 | current_url = current_url.replace( /\[(\d+)\-(\d+)\]/g, function(m_all, m_g1, m_g2) { 498 | var low = parseInt(m_g1); 499 | var high = parseInt(m_g2); 500 | return Math.round( low + ((high - low) * Math.random()) ); 501 | }); 502 | current_url = current_url.replace( /\[(\w+)\]/g, function(m_all, key) { 503 | return params[key] ? Tools.randArray(params[key]) : ''; 504 | }); 505 | if (++ebrake > 32) break; 506 | } 507 | 508 | // Allow URL to override headers for current request only 509 | // Example: "/ads?place=yahoo&size=160x600&chan=tst&cb=1234 [header:Cookie:dtuid=tor00355;]" 510 | current_url = current_url.replace(/\s*\[header\:\s*([\w\-]+)\:\s*([^\]]+)\]/ig, function(m_all, m_g1, m_g2) { 511 | current_opts.headers[ m_g1 ] = m_g2; 512 | return ''; 513 | }).trim(); 514 | 515 | // allow placeholder substitution inside header values as well 516 | for (var key in current_opts.headers) { 517 | current_opts.headers[key] = current_opts.headers[key].toString().replace( /\[(\w+)\]/g, function(m_all, key) { 518 | return params[key] ? Tools.randArray(params[key]) : ''; 519 | }); 520 | } 521 | 522 | // allow placeholder sub in post data, if raw string or hash 523 | if (current_opts.data) { 524 | if (typeof(current_opts.data) == 'string') { 525 | current_opts.data = current_opts.data.replace( /\[(\w+)\]/g, function(m_all, key) { 526 | return params[key] ? Tools.randArray(params[key]) : ''; 527 | }); 528 | } 529 | else if (Tools.isaHash(current_opts.data)) { 530 | for (var key in current_opts.data) { 531 | current_opts.data[key] = current_opts.data[key].toString().replace( /\[(\w+)\]/g, function(m_all, key) { 532 | return params[key] ? Tools.randArray(params[key]) : ''; 533 | }); 534 | } 535 | } 536 | } 537 | 538 | if (args.verbose) { 539 | print( dateTimeStamp() + bold.cyan("Request URL: ") + current_url + "\n" ); 540 | if (current_opts.data) print( dateTimeStamp() + bold.cyan("POST Data: ") + JSON.stringify(current_opts.data) + "\n" ); 541 | if (current_opts.headers && Tools.numKeys(current_opts.headers)) print( dateTimeStamp() + bold.cyan("Request Headers: ") + JSON.stringify(current_opts.headers) + "\n" ); 542 | } 543 | 544 | // send HTTP request 545 | request[method]( current_url, current_opts, function(err, resp, data, perf) { 546 | if (err) err.url = current_url; 547 | 548 | // Track req/sec 549 | var now = Tools.timeNow(); 550 | var now_sec = Math.floor(now); 551 | stats.count_sec++; 552 | stats.total_reqs++; 553 | 554 | if (now_sec != stats.current_sec) { 555 | stats.current_sec = now_sec; 556 | req_per_sec = stats.count_sec; 557 | if (req_per_sec > stats.peak_sec) stats.peak_sec = req_per_sec; 558 | stats.count_sec = 0; 559 | } 560 | 561 | // Update progress bar 562 | count++; 563 | cli.progress.update({ 564 | amount: count / max_iter, 565 | text: ' [' + req_per_sec + " req/sec]" 566 | }); 567 | 568 | if (!err && (success_match || error_match)) { 569 | var text = data.toString(); 570 | if (success_match && !text.match(success_match)) { 571 | err = new Error("Response does not contain success match (" + args.success_match + ")"); 572 | err.headers = resp.headers; 573 | err.content = text; 574 | } 575 | else if (error_match && text.match(error_match)) { 576 | err = new Error("Response contains error match (" + args.error_match + ")"); 577 | err.headers = resp.headers; 578 | err.content = text; 579 | } 580 | } 581 | 582 | // process metrics 583 | var metrics = perf ? perf.metrics() : { perf: { total: 0 }, counters: {} }; 584 | metrics.url = current_url; 585 | 586 | var is_warning = !!(warn_ms && (metrics.perf.total >= warn_ms)); 587 | if (is_warning) { 588 | stats.total_warnings++; 589 | num_warns++; 590 | } 591 | 592 | if (resp && (args.verbose || is_warning)) { 593 | // In verbose mode, print every success and perf metrics 594 | cli.progress.erase(); 595 | cli[is_warning ? 'warn' : 'verbose']( dateTimeStamp() + (is_warning ? bold.red("Perf Warning: ") : '') + 'Req #' + count + ": HTTP " + resp.statusCode + " " + resp.statusMessage + " -- " + JSON.stringify(metrics) + "\n" ); 596 | cli.progress.draw(); 597 | } 598 | 599 | if (resp && is_warning && args.warnings) { 600 | var warn_data = Tools.mergeHashes( metrics, { 601 | code: resp.statusCode, 602 | status: resp.statusMessage, 603 | req_num: count, 604 | now: now, 605 | date_time: dateTimeStamp().trim() 606 | }); 607 | fs.appendFileSync( args.warnings, JSON.stringify(warn_data) + "\n" ); 608 | } 609 | if (resp && args.log) { 610 | var log_data = Tools.mergeHashes( metrics, { 611 | code: resp.statusCode, 612 | status: resp.statusMessage, 613 | req_num: count, 614 | now: now, 615 | date_time: dateTimeStamp().trim() 616 | }); 617 | fs.appendFileSync( args.log, JSON.stringify(log_data) + "\n" ); 618 | } 619 | 620 | // Compute min/avg/max stats 621 | for (var key in metrics.perf) { 622 | var value = metrics.perf[key]; 623 | 624 | if (!stats[key]) stats[key] = {}; 625 | var stat = stats[key]; 626 | 627 | if (!("min" in stat) || (value < stat.min)) stat.min = value; 628 | if (!("max" in stat) || (value > stat.max)) stat.max = value; 629 | if (!("total" in stat)) stat.total = 0; 630 | if (!("count" in stat)) stat.count = 0; 631 | 632 | stat.total += value; 633 | stat.count++; 634 | } 635 | 636 | // Increment total counters 637 | for (var key in metrics.counters) { 638 | if (!stats[key]) stats[key] = 0; 639 | stats[key] += metrics.counters[key]; 640 | } 641 | 642 | // Compute historgram data 643 | histo.cats.forEach( function(cat) { 644 | var value = metrics.perf[cat] || 0; 645 | var group = null; 646 | 647 | for (var idx = 0, len = histo.groups.length; idx < len; idx++) { 648 | group = histo.groups[idx]; 649 | if ((value >= group.low) && (value < group.high)) { 650 | if (!histo.counts[cat][group.label]) histo.counts[cat][group.label] = 0; 651 | histo.counts[cat][group.label]++; 652 | idx = len; 653 | } 654 | } 655 | }); // histo 656 | 657 | if (emergency_abort) { 658 | // User hit Ctrl-C or someone TERM'ed us 659 | return callback( new Error("User Abort") ); 660 | } 661 | else if (err) { 662 | // Core error such as DNS failure, socket connect timeout, custom error, etc. 663 | err.perf = perf; 664 | return nextIteration(err, callback); 665 | } 666 | else { 667 | // URL request was a success 668 | if (args.verbose) { 669 | print( dateTimeStamp() + bold.green("Response Status: ") + resp.statusCode + " " + resp.statusMessage + "\n" ); 670 | print( dateTimeStamp() + bold.green("Response Content: ") + JSON.stringify(data.toString()) + "\n" ); 671 | print( dateTimeStamp() + bold.green("Response Headers: ") + JSON.stringify(resp.headers) + "\n" ); 672 | } 673 | 674 | nextIteration(null, callback); 675 | } 676 | } ); 677 | }, 678 | function(err) { 679 | // All requests complete 680 | cli.progress.end(); 681 | 682 | if (err) { 683 | if (args.verbose || num_warns) { 684 | print("\n"); 685 | } 686 | print( dateTimeStamp() + bold.red("ERROR: ") + err.message + "\n" ); 687 | if (args.verbose && err.url) { 688 | print( dateTimeStamp() + bold.yellow("URL: ") + err.url + "\n" ); 689 | } 690 | if (args.verbose && err.content) { 691 | print( dateTimeStamp() + bold.yellow("Content: ") + JSON.stringify(err.content) + "\n" ); 692 | } 693 | if (args.verbose && err.headers) { 694 | print( dateTimeStamp() + bold.yellow("Headers: ") + JSON.stringify(err.headers) + "\n" ); 695 | } 696 | if (args.verbose && err.perf) { 697 | var metrics = err.perf.metrics(); 698 | print( dateTimeStamp() + bold.yellow("Perf: ") + JSON.stringify(metrics) + "\n" ); 699 | } 700 | print( dateTimeStamp() + "Stopped test prematurely: " + count + " of " + max_iter + " requests completed.\n" ); 701 | num_warns++; 702 | } 703 | 704 | printReport(); 705 | 706 | if (args.stats) { 707 | // emit final stats as JSON, bust through `quiet` mode too 708 | if (args.stats === true) process.stdout.write( JSON.stringify(stats) + "\n" ); 709 | else fs.appendFileSync( args.stats, JSON.stringify(stats) + "\n" ); 710 | } 711 | 712 | // process.stdin.end(); 713 | process.exit(0); 714 | 715 | } // complete 716 | ); 717 | --------------------------------------------------------------------------------