#!/usr/bin/env node // -*- mode: js -*- var fs = require('fs'); var path = require('path'); var spawn = require('child_process').spawn; var readline = require('readline'); var sprintf = require('util').format; var nopt = require('nopt'); ///--- Globals var BUCKETS = {}; var REQUEST_IDS = {}; var OPTS = { 'average': Boolean, 'help': Boolean, 'end': Date, 'max-latency': Number, 'max-requests': Number, 'output': String, 'percentile': [Number, Array], 'period': Number, 'requests': Boolean, 'start': Date }; var SHORT_OPTS = { 'a': ['--average'], 'h': ['--help'], 'i': ['--period'], 'e': ['--end'], 'l': ['--max-latency'], 'n': ['--max-requests'], 'o': ['--output'], 'p': ['--percentile'], 'r': ['--requests'], 's': ['--start'] }; ///--- Functions function percentile(p, vals) { p = parseInt(p, 10); return vals[(Math.round(((p / 100) * vals.length) + 1 / 2) - 1)].latency; } function report(buckets, output) { Object.keys(buckets).sort(function (a, b) { return parseInt(a, 10) - parseInt(b, 10); }).forEach(function (k) { var avg = 0; var perc = []; var req = buckets[k].length; var sum = 0; var t = Math.round(buckets[k]._time); buckets[k] = buckets[k].sort(function (a, b) { return a.latency - b.latency; }); buckets[k].forEach(function (v) { sum += v.latency; }); if (sum > 0 && req > 0) { if (output.average) output.average.push([t, Math.round(sum / req)]); if (output.requests) output.requests.push([t, buckets[k].length]); Object.keys(output.percentile).forEach(function (p) { var _p = percentile(p, buckets[k]); output.percentile[p].push([t, _p]); }); } }); return output; } function usage(code, message) { var str = ''; Object.keys(SHORT_OPTS).forEach(function (k) { if (!Array.isArray(SHORT_OPTS[k])) return; var opt = SHORT_OPTS[k][0].replace('--', ''); var type = OPTS[opt].name || 'string'; if (type && type === 'boolean') type = ''; type = type.toLowerCase(); str += ' [--' + opt + ' ' + type + ']'; }); str += ' [file ...]'; if (message) console.error(message); console.error('usage: ' + path.basename(process.argv[1]) + str); process.exit(code); } ///--- Mainline var parsed; try { parsed = nopt(OPTS, SHORT_OPTS, process.argv, 2); } catch (e) { usage(1, e.toString()); } if (parsed.help) usage(0); if (!parsed.average && !parsed.percentile) usage(1, '--average or --percentile required'); if (parsed.argv.remain.length < 1) usage(1, 'log file required'); var config = { average: parsed.average || false, maxLatency: parsed['max-latency'] || 1000, maxRequests: parsed['max-requests'] || 10000, percentile: parsed.percentile || [], period: parsed.period || 60, requests: parsed.requests || false, start: parsed.start ? (parsed.start.getTime() / 1000) : 0, end: parsed.end ? (parsed.end.getTime() / 1000) : Number.MAX_VALUE }; var buckets = {}; var done = 0; parsed.argv.remain.forEach(function (f) { var stream = readline.createInterface({ input: fs.createReadStream(f), output: null }) stream.on('line', function (l) { var record; var t = -1; try { record = JSON.parse(l); } catch (e) { } if (!record) return; var t = -1; if (record.time) t = (new Date(record.time).getTime() / 1000); if (record._audit !== true || REQUEST_IDS[record.req_id] || t < config.start || t > config.end) { console.error('Skipping %s', l); } REQUEST_IDS[record.req_id] = true; record.time = t; var b = Math.round(record.time / config.period) + ''; if (!buckets[b]) buckets[b] = []; buckets[b].push(record); buckets[b]._time = record.time // good enough }); stream.on('end', function () { if (++done === parsed.argv.remain.length) { console.error('Generating report...'); var output = { average: config.average ? [] : false, requests: config.requests ? [] : false, percentile: {} }; config.percentile.forEach(function (p) { output.percentile[p] = []; }); output = report(buckets, output); var finalOutput = []; if (output.average) { finalOutput.push({ name: 'avg', values: output.average }); } if (output.requests) { finalOutput.push({ name: 'n', values: output.requests }); } Object.keys(output.percentile).forEach(function (k) { finalOutput.push({ name: 'p' + k, values: output.percentile[k] }); }); console.log(JSON.stringify(finalOutput)); } }); });