1 | # flowbench
|
2 |
|
3 | [![Build Status](https://travis-ci.org/pgte/flowbench.svg?branch=master)](https://travis-ci.org/pgte/flowbench)
|
4 |
|
5 | HTTP traffic generator. Supports user flows with alternative paths.
|
6 | Stores stats on latency.
|
7 |
|
8 | # Install
|
9 |
|
10 | ```
|
11 | $ npm install flowbench
|
12 | ```
|
13 |
|
14 |
|
15 | # Use
|
16 |
|
17 | ## Programatically
|
18 |
|
19 | ```js
|
20 | var flowbench = require('flowbench');
|
21 |
|
22 | var experiment = flowbench({
|
23 | base: 'http://localhost:3000',
|
24 | population: 100,
|
25 | maxConcurrentFlows: 50,
|
26 | requestDefaults: {
|
27 | timeout: 10000,
|
28 | jar: false
|
29 | }
|
30 | });
|
31 |
|
32 | experiment
|
33 | .flow({probability: 0.6})
|
34 | .get('/', {id: 1})
|
35 | .verify(verifyResponse1Function)
|
36 | .wait(500)
|
37 | .post('/abc', {
|
38 | id: 2,
|
39 | body: {a: "static value", b: "<%=fixtures.b.random()%>"},
|
40 | fixtures: {
|
41 | b: ['VALUE1', 'VALUE2', 'VALUE3']},
|
42 | timeout: 4000
|
43 | })
|
44 | .verify(
|
45 | flowbench.verify.response.status(200),
|
46 | flowbench.verify.response.body({a: '#{req.body.b}'}))
|
47 | .flow({probability: 0.5})
|
48 | .post('/abc/<%= res[2].prop2 %>',
|
49 | {body: {a: "<%= res[1].prop1 %>", "b": "<%= res[2].prop2} %>"}})
|
50 | .verify(...)
|
51 | .end()
|
52 | .flow({probability: 0.5})
|
53 | .get('/abc')
|
54 | .verify(...)
|
55 | .end()
|
56 | .end()
|
57 | .flow({probability: 0.4})
|
58 | .get('/')
|
59 | .verify(verifyResponse1Function);
|
60 |
|
61 |
|
62 | experiment.begin(function(err, stats) {
|
63 |
|
64 | });
|
65 | experiment.once('end', function(results) {
|
66 | console.log('results:', results);
|
67 | });
|
68 | ```
|
69 |
|
70 | # API
|
71 |
|
72 | ## flowbench(options)
|
73 |
|
74 | Options defaults:
|
75 |
|
76 | ```js
|
77 | {
|
78 | population: 1,
|
79 | maxConcurrentFlows: Infinity,
|
80 | requestDefaults: {
|
81 | pool: {
|
82 | maxSockets: Infinity
|
83 | },
|
84 | timeout: 10e3
|
85 | }
|
86 | };
|
87 | ```
|
88 |
|
89 | the `requestDefaults` object is the options for creating a [scoped request](https://github.com/request/request#requestdefaultsoptions).
|
90 |
|
91 | Returns an Experiment
|
92 |
|
93 | ## Experiment
|
94 |
|
95 | ### experient.flow(options)
|
96 |
|
97 | Adds an alternative flow to the experiment.
|
98 |
|
99 | Options:
|
100 |
|
101 | * `probability` - when more than one sibiling flow is present, this represents the probability of this flow getting executed.
|
102 |
|
103 | All flows within an experiment are alternative, and are given equal probability (unless otherwise specified.)
|
104 |
|
105 | Returns an instance of a Flow.
|
106 |
|
107 | ### experiment.begin(cb)
|
108 |
|
109 | Begins an experiment. Callsback when there is an error or the experiment finishes.
|
110 |
|
111 | The callback has the following signature:
|
112 |
|
113 | ```
|
114 | function callback(err, stats) {}
|
115 | ```
|
116 |
|
117 | The `stats` object is something like this:
|
118 |
|
119 | ```js
|
120 | {
|
121 | "requestsPerSecond": {
|
122 | "mean": 1651.547543071806,
|
123 | "count": 2000,
|
124 | "currentRate": 1651.4908801787194,
|
125 | "1MinuteRate": 0,
|
126 | "5MinuteRate": 0,
|
127 | "15MinuteRate": 0
|
128 | },
|
129 | "latencyNs": {
|
130 | "min": 397537333,
|
131 | "max": 489818898,
|
132 | "sum": 881597582934,
|
133 | "variance": 493325414798874.75,
|
134 | "mean": 440798791.467,
|
135 | "stddev": 22210930.07505257,
|
136 | "count": 2000,
|
137 | "median": 446440646.5,
|
138 | "p75": 454043121.5,
|
139 | "p95": 478719555.34999996,
|
140 | "p99": 488775828.4,
|
141 | "p999": 489641718.259
|
142 | },
|
143 | "requests": {
|
144 | "GET http://localhost:9000/abc": {
|
145 | "latencyNs": {
|
146 | "min": 429215073,
|
147 | "max": 489818898,
|
148 | "sum": 454618892085,
|
149 | "variance": 201579551941901.38,
|
150 | "mean": 454618892.085,
|
151 | "stddev": 14197871.387708137,
|
152 | "count": 1000,
|
153 | "median": 449254332.5,
|
154 | "p75": 463742870,
|
155 | "p95": 486903385.4,
|
156 | "p99": 488928787.48,
|
157 | "p999": 489818732.511
|
158 | },
|
159 | "statusCodes": {
|
160 | "200": {
|
161 | "count": 1000,
|
162 | "percentage": 1
|
163 | }
|
164 | }
|
165 | },
|
166 | "POST http://localhost:9000/def": {
|
167 | "latencyNs": {
|
168 | "min": 397537333,
|
169 | "max": 459961256,
|
170 | "sum": 426978690849,
|
171 | "variance": 403192361971691.8,
|
172 | "mean": 426978690.849,
|
173 | "stddev": 20079650.44445973,
|
174 | "count": 1000,
|
175 | "median": 419389668,
|
176 | "p75": 445073831.5,
|
177 | "p95": 459471652.6,
|
178 | "p99": 459851196.18,
|
179 | "p999": 459961244.691
|
180 | },
|
181 | "statusCodes": {
|
182 | "201": {
|
183 | "count": 1000,
|
184 | "percentage": 1
|
185 | }
|
186 | }
|
187 | }
|
188 | },
|
189 | "statusCodes": {
|
190 | "200": {
|
191 | "count": 1000,
|
192 | "percentage": 0.5
|
193 | },
|
194 | "201": {
|
195 | "count": 1000,
|
196 | "percentage": 0.5
|
197 | }
|
198 | }
|
199 | }
|
200 | ```
|
201 |
|
202 | ## Flow
|
203 |
|
204 | One flow executes the requests added to it in sequence. You can add subflows to a flow (only after the requests have been specified).
|
205 |
|
206 | ### flow.flow(options)
|
207 |
|
208 | Creates a child flow.
|
209 |
|
210 | Options:
|
211 |
|
212 | * `probability` - when more than one sibiling flow is present, this represents the probability of this flow getting executed.
|
213 |
|
214 | Returns a flow.
|
215 |
|
216 |
|
217 | ### flow.end()
|
218 |
|
219 | Returns the parent flow (or experiment, if at root).
|
220 |
|
221 | ### flow.request(method, url[, options])
|
222 |
|
223 | Add a request to a flow.
|
224 |
|
225 | Options:
|
226 |
|
227 | * id: a string identifying the request. Can access it later inside templates.
|
228 | * fixtures: See the [Fixtures](#fixtures) section below.
|
229 | * body: object or string, representing the request body
|
230 | * headers: object with headers
|
231 | * qs: an object with the query string names and values
|
232 | * form: sets the body to a querystring representation
|
233 | * jar: cookie jar or `false`
|
234 | * ... all other options supported by [request](https://github.com/request/request).
|
235 |
|
236 | ### flow.get(url[, options]), flow.post, flow.put, flow.delete, flow.head
|
237 |
|
238 | Helpers for `flow.request()`.
|
239 |
|
240 |
|
241 | ### flow.verify(fn)
|
242 |
|
243 | Pass in a verification function. This function has the following signature:
|
244 |
|
245 | ```js
|
246 | function(req, res) {}
|
247 | ```
|
248 |
|
249 | This function will then be responsible for verifying the latest request and response.
|
250 |
|
251 | If the verification fails, this function can either:
|
252 |
|
253 | * return an `Error` object
|
254 | * return `false`
|
255 | * throw an `Error` object
|
256 |
|
257 | Otherwise, if verification passed, this function should return `true`.
|
258 |
|
259 | ### flow builtin verifiers
|
260 |
|
261 | You can use the following verifiers:
|
262 |
|
263 | * flowbench.verify.response.status(201)
|
264 | * flowbench.verify.response.body({a:1, b:2})
|
265 |
|
266 |
|
267 | ## About string interpolation and templating
|
268 |
|
269 | In option you pass into the request (url, options), you can use strings as EJS templates. These templates can access these objects:
|
270 |
|
271 | * req: an object with all requests performed, addressed by `id`.
|
272 | * res: an object with all the responses received, addressed by `id`.
|
273 |
|
274 | (see first example above of using ids and templates).
|
275 |
|
276 |
|
277 | ### Functions instead of values
|
278 |
|
279 | In any of the `url` or `options` for a request, you can pass in a function with the followig signature to be evaluated at run time:
|
280 |
|
281 | ```js
|
282 | function (req, res, fixtures) {}
|
283 | ```
|
284 |
|
285 | ## Fixtures
|
286 |
|
287 | You can define fixtures for any given request, and you can use these fixtures in your request options.
|
288 |
|
289 | For instance, you can have a given set of airports as fixtures that you can use randomly throughout the request like this:
|
290 |
|
291 | ```js
|
292 | experiment
|
293 | .flow.get('/search', {
|
294 | qs: {
|
295 | 'airportcode': '<%= fixtures.airports.random() %>'
|
296 | },
|
297 | fixtures: {
|
298 | airports: require('./airport-codes.json')
|
299 | }
|
300 | });
|
301 | ```
|
302 |
|
303 | If you wish, you can then verify the response by looking at the request:
|
304 |
|
305 | ```js
|
306 | experiment
|
307 | .flow.get('/search', {
|
308 | qs: {
|
309 | 'airportcode': '<%= fixtures.airports.random() %>'
|
310 | },
|
311 | fixtures: {
|
312 | airports: require('./airport-codes.json')
|
313 | }
|
314 | })
|
315 | .verify(function(req, res) {
|
316 | return res.body.airportcode == req.qs.airportcode
|
317 | });
|
318 | ```
|
319 |
|
320 | ## flowbench.humanize (experimental)
|
321 |
|
322 | Once you get the stats, you can get a more humanized version of it by passing it through `flowbench.humanize` like this:
|
323 |
|
324 | ```js
|
325 | experiment.begin(function(err, stats) {
|
326 | if (err) {
|
327 | throw err;
|
328 | }
|
329 | stats = flowbench.humanize(stats);
|
330 | console.log(JSON.stringify(stats, null, ' '));
|
331 | });
|
332 | ```
|
333 |
|
334 | # License
|
335 |
|
336 | ISC |
\ | No newline at end of file |