Compare commits
2 Commits
developmen
...
wip/2064
Author | SHA1 | Date |
---|---|---|
philipyoo | 31bf4c5c59 | |
philipyoo | a09ef77e8e |
|
@ -1,4 +1,7 @@
|
||||||
|
const async = require('async');
|
||||||
|
|
||||||
const StatsClient = require('./StatsClient');
|
const StatsClient = require('./StatsClient');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class StatsModel
|
* @class StatsModel
|
||||||
*
|
*
|
||||||
|
@ -6,6 +9,141 @@ const StatsClient = require('./StatsClient');
|
||||||
* rather than by seconds
|
* rather than by seconds
|
||||||
*/
|
*/
|
||||||
class StatsModel extends StatsClient {
|
class StatsModel extends StatsClient {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to convert 2d array rows to columns, and vice versa
|
||||||
|
* See also: https://docs.ruby-lang.org/en/2.0.0/Array.html#method-i-zip
|
||||||
|
* @param {array} arrays - 2d array of integers
|
||||||
|
* @return {array} converted array
|
||||||
|
*/
|
||||||
|
_zip(arrays) {
|
||||||
|
if (arrays.length > 0 && arrays.every(a => Array.isArray(a))) {
|
||||||
|
return arrays[0].map((_, i) => arrays.map(a => a[i]));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* normalize to the nearest interval
|
||||||
|
* @param {object} d - Date instance
|
||||||
|
* @return {number} timestamp - normalized to the nearest interval
|
||||||
|
*/
|
||||||
|
_normalizeTimestamp(d) {
|
||||||
|
const m = d.getMinutes();
|
||||||
|
return d.setMinutes(m - m % (Math.floor(this._interval / 60)), 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* override the method to get the count as an array of integers separated
|
||||||
|
* by each interval
|
||||||
|
* typical input looks like [[null, '1'], [null, '2'], [null, null]...]
|
||||||
|
* @param {array} arr - each index contains the result of each batch command
|
||||||
|
* where index 0 signifies the error and index 1 contains the result
|
||||||
|
* @return {array} array of integers, ordered from most recent interval to
|
||||||
|
* oldest interval with length of (expiry / interval)
|
||||||
|
*/
|
||||||
|
_getCount(arr) {
|
||||||
|
const size = Math.floor(this._expiry / this._interval);
|
||||||
|
const array = arr.reduce((store, i) => {
|
||||||
|
let num = parseInt(i[1], 10);
|
||||||
|
num = Number.isNaN(num) ? 0 : num;
|
||||||
|
store.push(num);
|
||||||
|
return store;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (array.length < size) {
|
||||||
|
array.push(...Array(size - array.length).fill(0));
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* wrapper on `getStats` that handles a list of keys
|
||||||
|
* override the method to reduce the returned 2d array from `_getCount`
|
||||||
|
* @param {object} log - Werelogs request logger
|
||||||
|
* @param {array} ids - service identifiers
|
||||||
|
* @param {callback} cb - callback to call with the err/result
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
getAllStats(log, ids, cb) {
|
||||||
|
if (!this._redis) {
|
||||||
|
return cb(null, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const size = Math.floor(this._expiry / this._interval);
|
||||||
|
const statsRes = {
|
||||||
|
'requests': Array(size).fill(0),
|
||||||
|
'500s': Array(size).fill(0),
|
||||||
|
'sampleDuration': this._expiry,
|
||||||
|
};
|
||||||
|
const requests = [];
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (ids.length === 0) {
|
||||||
|
return cb(null, statsRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// for now set concurrency to default of 10
|
||||||
|
return async.eachLimit(ids, 10, (id, done) => {
|
||||||
|
this.getStats(log, id, (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
requests.push(res.requests);
|
||||||
|
errors.push(res['500s']);
|
||||||
|
return done();
|
||||||
|
});
|
||||||
|
}, error => {
|
||||||
|
if (error) {
|
||||||
|
log.error('error getting stats', {
|
||||||
|
error,
|
||||||
|
method: 'StatsModel.getAllStats',
|
||||||
|
});
|
||||||
|
return cb(null, statsRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
statsRes.requests = this._zip(requests).map(arr =>
|
||||||
|
arr.reduce((acc, i) => acc + i), 0);
|
||||||
|
statsRes['500s'] = this._zip(errors).map(arr =>
|
||||||
|
arr.reduce((acc, i) => acc + i), 0);
|
||||||
|
|
||||||
|
return cb(null, statsRes);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles getting a list of global keys.
|
||||||
|
* @param {array} ids - Service identifiers
|
||||||
|
* @param {object} log - Werelogs request logger
|
||||||
|
* @param {function} cb - Callback
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
getAllGlobalStats(ids, log, cb) {
|
||||||
|
const reqsKeys = ids.map(key => (['get', key]));
|
||||||
|
return this._redis.batch(reqsKeys, (err, res) => {
|
||||||
|
const statsRes = { requests: 0 };
|
||||||
|
if (err) {
|
||||||
|
log.error('error getting metrics', {
|
||||||
|
error: err,
|
||||||
|
method: 'StatsClient.getAllGlobalStats',
|
||||||
|
});
|
||||||
|
return cb(null, statsRes);
|
||||||
|
}
|
||||||
|
statsRes.requests = res.reduce((sum, curr) => {
|
||||||
|
const [cmdErr, val] = curr;
|
||||||
|
if (cmdErr) {
|
||||||
|
// Log any individual request errors from the batch request.
|
||||||
|
log.error('error getting metrics', {
|
||||||
|
error: cmdErr,
|
||||||
|
method: 'StatsClient.getAllGlobalStats',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return sum + (Number.parseInt(val, 10) || 0);
|
||||||
|
}, 0);
|
||||||
|
return cb(null, statsRes);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* normalize date timestamp to the nearest hour
|
* normalize date timestamp to the nearest hour
|
||||||
* @param {Date} d - Date instance
|
* @param {Date} d - Date instance
|
||||||
|
@ -34,24 +172,6 @@ class StatsModel extends StatsClient {
|
||||||
return d.setMinutes(m - m % (Math.floor(this._interval / 60)), 0, 0);
|
return d.setMinutes(m - m % (Math.floor(this._interval / 60)), 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* override the method to get the result as an array of integers separated
|
|
||||||
* by each interval
|
|
||||||
* typical input looks like [[null, '1'], [null, '2'], [null, null]...]
|
|
||||||
* @param {array} arr - each index contains the result of each batch command
|
|
||||||
* where index 0 signifies the error and index 1 contains the result
|
|
||||||
* @return {array} array of integers, ordered from most recent interval to
|
|
||||||
* oldest interval
|
|
||||||
*/
|
|
||||||
_getCount(arr) {
|
|
||||||
return arr.reduce((store, i) => {
|
|
||||||
let num = parseInt(i[1], 10);
|
|
||||||
num = Number.isNaN(num) ? 0 : num;
|
|
||||||
store.push(num);
|
|
||||||
return store;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get list of sorted set key timestamps
|
* get list of sorted set key timestamps
|
||||||
* @param {number} epoch - epoch time
|
* @param {number} epoch - epoch time
|
||||||
|
|
|
@ -0,0 +1,246 @@
|
||||||
|
'use strict'; // eslint-disable-line strict
|
||||||
|
|
||||||
|
const assert = require('assert');
|
||||||
|
const async = require('async');
|
||||||
|
|
||||||
|
const RedisClient = require('../../../lib/metrics/RedisClient');
|
||||||
|
const StatsModel = require('../../../lib/metrics/StatsModel');
|
||||||
|
|
||||||
|
// setup redis client
|
||||||
|
const config = {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 6379,
|
||||||
|
enableOfflineQueue: false,
|
||||||
|
};
|
||||||
|
const fakeLogger = {
|
||||||
|
trace: () => {},
|
||||||
|
error: () => {},
|
||||||
|
};
|
||||||
|
const redisClient = new RedisClient(config, fakeLogger);
|
||||||
|
|
||||||
|
// setup stats model
|
||||||
|
const STATS_INTERVAL = 300; // 5 minutes
|
||||||
|
const STATS_EXPIRY = 86400; // 24 hours
|
||||||
|
const statsModel = new StatsModel(redisClient, STATS_INTERVAL, STATS_EXPIRY);
|
||||||
|
|
||||||
|
function setExpectedStats(expected) {
|
||||||
|
return expected.concat(
|
||||||
|
Array((STATS_EXPIRY / STATS_INTERVAL) - expected.length).fill(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since many methods were overwritten, these tests should validate the changes
|
||||||
|
// made to the original methods
|
||||||
|
describe('StatsModel class', () => {
|
||||||
|
const id = 'arsenal-test';
|
||||||
|
const id2 = 'test-2';
|
||||||
|
const id3 = 'test-3';
|
||||||
|
|
||||||
|
afterEach(() => redisClient.clear(() => {}));
|
||||||
|
|
||||||
|
it('should convert a 2d array columns into rows and vice versa using _zip',
|
||||||
|
() => {
|
||||||
|
const arrays = [
|
||||||
|
[1, 2, 3],
|
||||||
|
[4, 5, 6],
|
||||||
|
[7, 8, 9],
|
||||||
|
];
|
||||||
|
|
||||||
|
const res = statsModel._zip(arrays);
|
||||||
|
const expected = [
|
||||||
|
[1, 4, 7],
|
||||||
|
[2, 5, 8],
|
||||||
|
[3, 6, 9],
|
||||||
|
];
|
||||||
|
|
||||||
|
assert.deepStrictEqual(res, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('_zip should return an empty array if given an invalid array', () => {
|
||||||
|
const arrays = [];
|
||||||
|
|
||||||
|
const res = statsModel._zip(arrays);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(res, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('_getCount should return a an array of all valid integer values',
|
||||||
|
() => {
|
||||||
|
const res = statsModel._getCount([
|
||||||
|
[null, '1'],
|
||||||
|
[null, '2'],
|
||||||
|
[null, null],
|
||||||
|
]);
|
||||||
|
assert.deepStrictEqual(res, setExpectedStats([1, 2, 0]));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly record a new request by default one increment',
|
||||||
|
done => {
|
||||||
|
async.series([
|
||||||
|
next => {
|
||||||
|
statsModel.reportNewRequest(id, (err, res) => {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
const expected = [[null, 1], [null, 1]];
|
||||||
|
assert.deepStrictEqual(res, expected);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
next => {
|
||||||
|
statsModel.reportNewRequest(id, (err, res) => {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
const expected = [[null, 2], [null, 1]];
|
||||||
|
assert.deepStrictEqual(res, expected);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
], done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should record new requests by defined amount increments', done => {
|
||||||
|
function noop() {}
|
||||||
|
|
||||||
|
async.series([
|
||||||
|
next => {
|
||||||
|
statsModel.reportNewRequest(id, 9);
|
||||||
|
statsModel.getStats(fakeLogger, id, (err, res) => {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(res.requests, setExpectedStats([9]));
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
next => {
|
||||||
|
statsModel.reportNewRequest(id);
|
||||||
|
statsModel.getStats(fakeLogger, id, (err, res) => {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(res.requests,
|
||||||
|
setExpectedStats([10]));
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
next => {
|
||||||
|
statsModel.reportNewRequest(id, noop);
|
||||||
|
statsModel.getStats(fakeLogger, id, (err, res) => {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(res.requests,
|
||||||
|
setExpectedStats([11]));
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
], done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly record a 500 on the server', done => {
|
||||||
|
statsModel.report500(id, (err, res) => {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
const expected = [[null, 1], [null, 1]];
|
||||||
|
assert.deepStrictEqual(res, expected);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond back with total requests as an array', done => {
|
||||||
|
async.series([
|
||||||
|
next => {
|
||||||
|
statsModel.reportNewRequest(id, err => {
|
||||||
|
assert.ifError(err);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
next => {
|
||||||
|
statsModel.report500(id, err => {
|
||||||
|
assert.ifError(err);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
next => {
|
||||||
|
statsModel.getStats(fakeLogger, id, (err, res) => {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
'requests': setExpectedStats([1]),
|
||||||
|
'500s': setExpectedStats([1]),
|
||||||
|
'sampleDuration': STATS_EXPIRY,
|
||||||
|
};
|
||||||
|
assert.deepStrictEqual(res, expected);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
], done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not crash on empty results', done => {
|
||||||
|
async.series([
|
||||||
|
next => {
|
||||||
|
statsModel.getStats(fakeLogger, id, (err, res) => {
|
||||||
|
assert.ifError(err);
|
||||||
|
const expected = {
|
||||||
|
'requests': setExpectedStats([]),
|
||||||
|
'500s': setExpectedStats([]),
|
||||||
|
'sampleDuration': STATS_EXPIRY,
|
||||||
|
};
|
||||||
|
assert.deepStrictEqual(res, expected);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
next => {
|
||||||
|
statsModel.getAllStats(fakeLogger, id, (err, res) => {
|
||||||
|
assert.ifError(err);
|
||||||
|
const expected = {
|
||||||
|
'requests': setExpectedStats([]),
|
||||||
|
'500s': setExpectedStats([]),
|
||||||
|
'sampleDuration': STATS_EXPIRY,
|
||||||
|
};
|
||||||
|
assert.deepStrictEqual(res, expected);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
], done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a zero-filled array if no ids are passed to getAllStats',
|
||||||
|
done => {
|
||||||
|
statsModel.getAllStats(fakeLogger, [], (err, res) => {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(res.requests, setExpectedStats([]));
|
||||||
|
assert.deepStrictEqual(res['500s'], setExpectedStats([]));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get accurately reported data for given id from getAllStats',
|
||||||
|
done => {
|
||||||
|
statsModel.reportNewRequest(id, 9);
|
||||||
|
statsModel.reportNewRequest(id2, 2);
|
||||||
|
statsModel.reportNewRequest(id3, 3);
|
||||||
|
statsModel.report500(id);
|
||||||
|
|
||||||
|
async.series([
|
||||||
|
next => {
|
||||||
|
statsModel.getAllStats(fakeLogger, [id], (err, res) => {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
assert.equal(res.requests[0], 9);
|
||||||
|
assert.equal(res['500s'][0], 1);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
next => {
|
||||||
|
statsModel.getAllStats(fakeLogger, [id, id2, id3],
|
||||||
|
(err, res) => {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
assert.equal(res.requests[0], 14);
|
||||||
|
assert.deepStrictEqual(res.requests,
|
||||||
|
setExpectedStats([14]));
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
], done);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue