1 | #!/usr/bin/env node
|
2 | // -*- mode: js -*-
|
3 |
|
4 | var fs = require('fs');
|
5 | var path = require('path');
|
6 | var spawn = require('child_process').spawn;
|
7 | var readline = require('readline');
|
8 | var sprintf = require('util').format;
|
9 |
|
10 | var nopt = require('nopt');
|
11 |
|
12 |
|
13 | ///--- Globals
|
14 |
|
15 | var BUCKETS = {};
|
16 | var REQUEST_IDS = {};
|
17 |
|
18 | var OPTS = {
|
19 | 'average': Boolean,
|
20 | 'help': Boolean,
|
21 | 'end': Date,
|
22 | 'max-latency': Number,
|
23 | 'max-requests': Number,
|
24 | 'output': String,
|
25 | 'percentile': [Number, Array],
|
26 | 'period': Number,
|
27 | 'requests': Boolean,
|
28 | 'start': Date
|
29 | };
|
30 |
|
31 | var SHORT_OPTS = {
|
32 | 'a': ['--average'],
|
33 | 'h': ['--help'],
|
34 | 'i': ['--period'],
|
35 | 'e': ['--end'],
|
36 | 'l': ['--max-latency'],
|
37 | 'n': ['--max-requests'],
|
38 | 'o': ['--output'],
|
39 | 'p': ['--percentile'],
|
40 | 'r': ['--requests'],
|
41 | 's': ['--start']
|
42 | };
|
43 |
|
44 |
|
45 | ///--- Functions
|
46 |
|
47 | function percentile(p, vals) {
|
48 | p = parseInt(p, 10);
|
49 | return vals[(Math.round(((p / 100) * vals.length) + 1 / 2) - 1)].latency;
|
50 | }
|
51 |
|
52 |
|
53 | function report(buckets, output) {
|
54 | Object.keys(buckets).sort(function (a, b) {
|
55 | return parseInt(a, 10) - parseInt(b, 10);
|
56 | }).forEach(function (k) {
|
57 | var avg = 0;
|
58 | var perc = [];
|
59 | var req = buckets[k].length;
|
60 | var sum = 0;
|
61 | var t = Math.round(buckets[k]._time);
|
62 |
|
63 | buckets[k] = buckets[k].sort(function (a, b) {
|
64 | return a.latency - b.latency;
|
65 | });
|
66 |
|
67 | buckets[k].forEach(function (v) {
|
68 | sum += v.latency;
|
69 | });
|
70 |
|
71 | if (sum > 0 && req > 0) {
|
72 | if (output.average)
|
73 | output.average.push([t, Math.round(sum / req)]);
|
74 | if (output.requests)
|
75 | output.requests.push([t, buckets[k].length]);
|
76 | Object.keys(output.percentile).forEach(function (p) {
|
77 | var _p = percentile(p, buckets[k]);
|
78 | output.percentile[p].push([t, _p]);
|
79 | });
|
80 | }
|
81 | });
|
82 |
|
83 | return output;
|
84 | }
|
85 |
|
86 |
|
87 | function usage(code, message) {
|
88 | var str = '';
|
89 | Object.keys(SHORT_OPTS).forEach(function (k) {
|
90 | if (!Array.isArray(SHORT_OPTS[k]))
|
91 | return;
|
92 |
|
93 | var opt = SHORT_OPTS[k][0].replace('--', '');
|
94 | var type = OPTS[opt].name || 'string';
|
95 | if (type && type === 'boolean')
|
96 | type = '';
|
97 | type = type.toLowerCase();
|
98 |
|
99 | str += ' [--' + opt + ' ' + type + ']';
|
100 | });
|
101 | str += ' [file ...]';
|
102 |
|
103 | if (message)
|
104 | console.error(message);
|
105 |
|
106 | console.error('usage: ' + path.basename(process.argv[1]) + str);
|
107 | process.exit(code);
|
108 | }
|
109 |
|
110 |
|
111 | ///--- Mainline
|
112 |
|
113 | var parsed;
|
114 |
|
115 | try {
|
116 | parsed = nopt(OPTS, SHORT_OPTS, process.argv, 2);
|
117 | } catch (e) {
|
118 | usage(1, e.toString());
|
119 | }
|
120 |
|
121 | if (parsed.help)
|
122 | usage(0);
|
123 | if (!parsed.average && !parsed.percentile)
|
124 | usage(1, '--average or --percentile required');
|
125 | if (parsed.argv.remain.length < 1)
|
126 | usage(1, 'log file required');
|
127 |
|
128 | var config = {
|
129 | average: parsed.average || false,
|
130 | maxLatency: parsed['max-latency'] || 1000,
|
131 | maxRequests: parsed['max-requests'] || 10000,
|
132 | percentile: parsed.percentile || [],
|
133 | period: parsed.period || 60,
|
134 | requests: parsed.requests || false,
|
135 | start: parsed.start ? (parsed.start.getTime() / 1000) : 0,
|
136 | end: parsed.end ? (parsed.end.getTime() / 1000) : Number.MAX_VALUE
|
137 | };
|
138 |
|
139 | var buckets = {};
|
140 |
|
141 | var done = 0;
|
142 | parsed.argv.remain.forEach(function (f) {
|
143 | var stream = readline.createInterface({
|
144 | input: fs.createReadStream(f),
|
145 | output: null
|
146 | })
|
147 | stream.on('line', function (l) {
|
148 | var record;
|
149 | var t = -1;
|
150 |
|
151 | try {
|
152 | record = JSON.parse(l);
|
153 | } catch (e) {
|
154 | }
|
155 |
|
156 | if (!record)
|
157 | return;
|
158 |
|
159 | var t = -1;
|
160 | if (record.time)
|
161 | t = (new Date(record.time).getTime() / 1000);
|
162 |
|
163 | if (record._audit !== true ||
|
164 | REQUEST_IDS[record.req_id] ||
|
165 | t < config.start ||
|
166 | t > config.end) {
|
167 |
|
168 | console.error('Skipping %s', l);
|
169 | }
|
170 |
|
171 | REQUEST_IDS[record.req_id] = true;
|
172 | record.time = t;
|
173 |
|
174 | var b = Math.round(record.time / config.period) + '';
|
175 | if (!buckets[b])
|
176 | buckets[b] = [];
|
177 |
|
178 | buckets[b].push(record);
|
179 | buckets[b]._time = record.time // good enough
|
180 | });
|
181 |
|
182 | stream.on('end', function () {
|
183 | if (++done === parsed.argv.remain.length) {
|
184 | console.error('Generating report...');
|
185 |
|
186 | var output = {
|
187 | average: config.average ? [] : false,
|
188 | requests: config.requests ? [] : false,
|
189 | percentile: {}
|
190 | };
|
191 | config.percentile.forEach(function (p) {
|
192 | output.percentile[p] = [];
|
193 | });
|
194 |
|
195 | output = report(buckets, output);
|
196 | var finalOutput = [];
|
197 | if (output.average) {
|
198 | finalOutput.push({
|
199 | name: 'avg',
|
200 | values: output.average
|
201 | });
|
202 | }
|
203 | if (output.requests) {
|
204 | finalOutput.push({
|
205 | name: 'n',
|
206 | values: output.requests
|
207 | });
|
208 | }
|
209 | Object.keys(output.percentile).forEach(function (k) {
|
210 | finalOutput.push({
|
211 | name: 'p' + k,
|
212 | values: output.percentile[k]
|
213 | });
|
214 | });
|
215 |
|
216 | console.log(JSON.stringify(finalOutput));
|
217 | }
|
218 | });
|
219 | });
|