mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-31 00:16:47 +01:00
Better client informations from the API.
Impelements: - http://unleash.host.com/api/client/seen-toggles - http://unleash.host.com/api/metrics/feature-toggles - http://localhost:4242/api/client/applications - http://localhost:4242/api/client/applications/:appName
This commit is contained in:
parent
13a93dcf43
commit
e55378e1c4
@ -4,15 +4,17 @@ const { test } = require('ava');
|
||||
const UnleashClientMetrics = require('./index');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
const appName = 'appName';
|
||||
const instanceId = 'instanceId';
|
||||
|
||||
test('should work without state', (t) => {
|
||||
const metrics = new UnleashClientMetrics();
|
||||
const store = new EventEmitter();
|
||||
const metrics = new UnleashClientMetrics(store);
|
||||
|
||||
t.truthy(metrics.getMetricsOverview());
|
||||
t.truthy(metrics.getAppsWitToggles());
|
||||
t.truthy(metrics.getTogglesMetrics());
|
||||
t.truthy(metrics.toJSON());
|
||||
|
||||
metrics.destroy();
|
||||
});
|
||||
@ -20,7 +22,8 @@ test('should work without state', (t) => {
|
||||
test.cb('data should expire', (t) => {
|
||||
const clock = sinon.useFakeTimers();
|
||||
|
||||
const metrics = new UnleashClientMetrics();
|
||||
const store = new EventEmitter();
|
||||
const metrics = new UnleashClientMetrics(store);
|
||||
|
||||
metrics.addPayload({
|
||||
appName,
|
||||
@ -59,9 +62,10 @@ test.cb('data should expire', (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('addPayload', t => {
|
||||
const metrics = new UnleashClientMetrics();
|
||||
metrics.addPayload({
|
||||
test('should listen to metrics from store', t => {
|
||||
const store = new EventEmitter();
|
||||
const metrics = new UnleashClientMetrics(store);
|
||||
store.emit('metrics', {
|
||||
appName,
|
||||
instanceId,
|
||||
bucket: {
|
||||
@ -76,8 +80,6 @@ test('addPayload', t => {
|
||||
},
|
||||
});
|
||||
|
||||
t.truthy(metrics.clients[instanceId].appName === appName);
|
||||
t.truthy(metrics.clients[instanceId].count === 123);
|
||||
t.truthy(metrics.apps[appName].count === 123);
|
||||
t.truthy(metrics.globalCount === 123);
|
||||
|
||||
@ -100,7 +102,6 @@ test('addPayload', t => {
|
||||
},
|
||||
});
|
||||
|
||||
t.truthy(metrics.clients[instanceId].count === 143);
|
||||
t.truthy(metrics.globalCount === 143);
|
||||
t.deepEqual(metrics.getTogglesMetrics().lastHour.toggleX, { yes: 133, no: 10 });
|
||||
t.deepEqual(metrics.getTogglesMetrics().lastMinute.toggleX, { yes: 133, no: 10 });
|
||||
@ -108,10 +109,13 @@ test('addPayload', t => {
|
||||
metrics.destroy();
|
||||
});
|
||||
|
||||
test('addBucket', t => {
|
||||
const metrics = new UnleashClientMetrics();
|
||||
metrics.addClient(appName, instanceId);
|
||||
metrics.addBucket(appName, instanceId, {
|
||||
test('should build up list of seend toggles when new metrics arrives', t => {
|
||||
const store = new EventEmitter();
|
||||
const metrics = new UnleashClientMetrics(store);
|
||||
store.emit('metrics', {
|
||||
appName,
|
||||
instanceId,
|
||||
bucket: {
|
||||
start: new Date(),
|
||||
stop: new Date(),
|
||||
toggles: {
|
||||
@ -119,39 +123,51 @@ test('addBucket', t => {
|
||||
yes: 123,
|
||||
no: 0,
|
||||
},
|
||||
toggleY: {
|
||||
yes: 50,
|
||||
no: 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
t.truthy(metrics.clients[instanceId].count === 123);
|
||||
t.truthy(metrics.globalCount === 123);
|
||||
t.deepEqual(metrics.getTogglesMetrics().lastMinute.toggleX, { yes: 123, no: 0 });
|
||||
|
||||
const appToggles = metrics.getAppsWitToggles();
|
||||
const togglesForApp = metrics.getSeenTogglesByAppName(appName);
|
||||
|
||||
t.truthy(appToggles.length === 1);
|
||||
t.truthy(appToggles[0].seenToggles.length === 2);
|
||||
t.truthy(appToggles[0].seenToggles.includes('toggleX'));
|
||||
t.truthy(appToggles[0].seenToggles.includes('toggleY'));
|
||||
|
||||
t.truthy(togglesForApp.length === 2);
|
||||
t.truthy(togglesForApp.includes('toggleX'));
|
||||
t.truthy(togglesForApp.includes('toggleY'));
|
||||
metrics.destroy();
|
||||
});
|
||||
|
||||
test('addClient', t => {
|
||||
const metrics = new UnleashClientMetrics();
|
||||
|
||||
metrics.addClient(appName, instanceId);
|
||||
metrics.addClient(appName, instanceId, new Date());
|
||||
|
||||
t.truthy(metrics.clients[instanceId].count === 0);
|
||||
t.truthy(metrics.globalCount === 0);
|
||||
|
||||
metrics.destroy();
|
||||
});
|
||||
|
||||
test('addApp', t => {
|
||||
const metrics = new UnleashClientMetrics();
|
||||
|
||||
metrics.addApp(appName, instanceId);
|
||||
t.truthy(metrics.apps[appName].clients.length === 1);
|
||||
metrics.addApp(appName, 'instanceId2');
|
||||
t.truthy(metrics.apps[appName].clients.length === 2);
|
||||
|
||||
metrics.addApp('appName2', 'instanceId2');
|
||||
t.truthy(metrics.apps.appName2.clients.length === 1);
|
||||
metrics.addApp('appName2', instanceId);
|
||||
t.truthy(metrics.apps.appName2.clients.length === 2);
|
||||
test('should handle a lot of toggles', t => {
|
||||
const store = new EventEmitter();
|
||||
const metrics = new UnleashClientMetrics(store);
|
||||
|
||||
const toggleCounts = {};
|
||||
for (let i=0; i<100; i++) {
|
||||
toggleCounts[`toggle${i}`] = {yes: i, no: i}
|
||||
}
|
||||
|
||||
store.emit('metrics', {
|
||||
appName,
|
||||
instanceId,
|
||||
bucket: {
|
||||
start: new Date(),
|
||||
stop: new Date(),
|
||||
toggles: toggleCounts,
|
||||
},
|
||||
});
|
||||
|
||||
const seenToggles = metrics.getSeenTogglesByAppName(appName);
|
||||
|
||||
t.truthy(seenToggles.length === 100);
|
||||
metrics.destroy();
|
||||
});
|
@ -4,10 +4,10 @@ const Projection = require('./projection.js');
|
||||
const TTLList = require('./ttl-list.js');
|
||||
|
||||
module.exports = class UnleashClientMetrics {
|
||||
constructor () {
|
||||
constructor (clientMetricsStore) {
|
||||
|
||||
this.globalCount = 0;
|
||||
this.apps = {};
|
||||
this.clients = {};
|
||||
|
||||
this.lastHourProjection = new Projection();
|
||||
this.lastMinuteProjection = new Projection();
|
||||
@ -15,6 +15,7 @@ module.exports = class UnleashClientMetrics {
|
||||
this.lastHourList = new TTLList({
|
||||
interval: 10000,
|
||||
});
|
||||
|
||||
this.lastMinuteList = new TTLList({
|
||||
interval: 10000,
|
||||
expireType: 'minutes',
|
||||
@ -31,18 +32,20 @@ module.exports = class UnleashClientMetrics {
|
||||
this.lastMinuteProjection.substract(toggleName, toggles[toggleName]);
|
||||
});
|
||||
});
|
||||
clientMetricsStore.on('metrics', (m) => this.addPayload(m));
|
||||
}
|
||||
|
||||
toJSON () {
|
||||
return JSON.stringify(this.getMetricsOverview(), null, 4);
|
||||
getAppsWitToggles () {
|
||||
const apps = [];
|
||||
Object.keys(this.apps).forEach(appName => {
|
||||
const seenToggles = Object.keys(this.apps[appName].seenToggles);
|
||||
const metricsCount = this.apps[appName].count;
|
||||
apps.push({appName, seenToggles, metricsCount})
|
||||
});
|
||||
return apps;
|
||||
}
|
||||
|
||||
getMetricsOverview () {
|
||||
return {
|
||||
globalCount: this.globalCount,
|
||||
apps: this.apps,
|
||||
clients: this.clients,
|
||||
};
|
||||
getSeenTogglesByAppName(appName) {
|
||||
return this.apps[appName] ? Object.keys(this.apps[appName].seenToggles) : [];
|
||||
}
|
||||
|
||||
getTogglesMetrics () {
|
||||
@ -53,16 +56,24 @@ module.exports = class UnleashClientMetrics {
|
||||
}
|
||||
|
||||
addPayload (data) {
|
||||
this.addClient(data.appName, data.instanceId);
|
||||
this.addBucket(data.appName, data.instanceId, data.bucket);
|
||||
const { appName, bucket } = data;
|
||||
const app = this.getApp(appName)
|
||||
this.addBucket(app, data.bucket);
|
||||
}
|
||||
|
||||
addBucket (appName, instanceId, bucket) {
|
||||
getApp(appName) {
|
||||
this.apps[appName] = this.apps[appName] || {seenToggles: {}, count: 0};
|
||||
return this.apps[appName];
|
||||
}
|
||||
|
||||
addBucket (app, bucket) {
|
||||
let count = 0;
|
||||
// TODO stop should be createdAt
|
||||
const { stop, toggles } = bucket;
|
||||
|
||||
Object.keys(toggles).forEach((n) => {
|
||||
const toggleNames = Object.keys(toggles);
|
||||
|
||||
toggleNames.forEach((n) => {
|
||||
const entry = toggles[n];
|
||||
this.lastHourProjection.add(n, entry);
|
||||
this.lastMinuteProjection.add(n, entry);
|
||||
@ -72,49 +83,13 @@ module.exports = class UnleashClientMetrics {
|
||||
this.lastHourList.add(toggles, stop);
|
||||
this.lastMinuteList.add(toggles, stop);
|
||||
|
||||
this.addClientCount(appName, instanceId, count);
|
||||
}
|
||||
|
||||
addClientCount (appName, instanceId, count) {
|
||||
if (typeof count === 'number' && count > 0) {
|
||||
this.globalCount += count;
|
||||
if (this.clients[instanceId]) {
|
||||
this.clients[instanceId].count += count;
|
||||
}
|
||||
if (this.apps[appName]) {
|
||||
this.apps[appName].count += count;
|
||||
}
|
||||
}
|
||||
app.count += count;
|
||||
this.addSeenToggles(app, toggleNames);
|
||||
}
|
||||
|
||||
addClient (appName, instanceId, started = new Date()) {
|
||||
this.addApp(appName, instanceId);
|
||||
if (instanceId) {
|
||||
if (this.clients[instanceId]) {
|
||||
this.clients[instanceId].ping = new Date();
|
||||
} else {
|
||||
this.clients[instanceId] = {
|
||||
appName,
|
||||
count: 0,
|
||||
started,
|
||||
init: new Date(),
|
||||
ping: new Date(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addApp (appName, instanceId) {
|
||||
if (appName && !this.apps[appName]) {
|
||||
this.apps[appName] = {
|
||||
count: 0,
|
||||
clients: [],
|
||||
};
|
||||
}
|
||||
|
||||
if (instanceId && !this.apps[appName].clients.includes(instanceId)) {
|
||||
this.apps[appName].clients.push(instanceId);
|
||||
}
|
||||
addSeenToggles (app, toggleNames) {
|
||||
toggleNames.forEach(t => app.seenToggles[t] = true);
|
||||
}
|
||||
|
||||
destroy () {
|
||||
|
@ -1,43 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
module.exports = class UnleashClientMetrics extends EventEmitter {
|
||||
constructor (metricsDb, interval = 10000) {
|
||||
super();
|
||||
this.interval = interval;
|
||||
this.db = metricsDb;
|
||||
this.highestIdSeen = 0;
|
||||
this.db.getMetricsLastHour().then(metrics => {
|
||||
this.addMetrics(metrics);
|
||||
this.startPoller();
|
||||
this.emit('ready');
|
||||
});
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
addMetrics (metrics) {
|
||||
if (metrics && metrics.length > 0) {
|
||||
this.highestIdSeen = metrics[metrics.length - 1].id;
|
||||
}
|
||||
this.emit('metrics', metrics);
|
||||
}
|
||||
|
||||
startPoller () {
|
||||
this.timer = setInterval(() => {
|
||||
this.db.getNewMetrics(this.highestIdSeen)
|
||||
.then(metrics => this.addMetrics(metrics));
|
||||
}, this.interval);
|
||||
this.timer.unref();
|
||||
}
|
||||
|
||||
insert (metrics) {
|
||||
return this.db.insert(metrics);
|
||||
}
|
||||
|
||||
destroy () {
|
||||
try {
|
||||
clearTimeout(this.timer);
|
||||
} catch (e) {}
|
||||
}
|
||||
};
|
@ -1,66 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { test } = require('ava');
|
||||
const MetricsService = require('./service');
|
||||
const sinon = require('sinon');
|
||||
|
||||
function getMockDb () {
|
||||
const list = [{ id: 2 }, { id: 3 }, { id: 4 }];
|
||||
const db = {
|
||||
getMetricsLastHour () {
|
||||
return Promise.resolve([{ id: 1 }]);
|
||||
},
|
||||
|
||||
getNewMetrics () {
|
||||
return Promise.resolve([list.pop() || { id: 0 }]);
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
db,
|
||||
};
|
||||
}
|
||||
|
||||
test.cb('should call database on startup', (t) => {
|
||||
const mock = getMockDb();
|
||||
const service = new MetricsService(mock.db);
|
||||
t.plan(2);
|
||||
|
||||
service.on('metrics', ([metric]) => {
|
||||
t.true(service.highestIdSeen === 1);
|
||||
t.true(metric.id === 1);
|
||||
t.end();
|
||||
service.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
test.cb('should poll for updates', (t) => {
|
||||
const clock = sinon.useFakeTimers();
|
||||
|
||||
const mock = getMockDb();
|
||||
const service = new MetricsService(mock.db, 100);
|
||||
|
||||
const metrics = [];
|
||||
service.on('metrics', (_metrics) => {
|
||||
_metrics.forEach(m => m && metrics.push(m));
|
||||
});
|
||||
|
||||
t.true(metrics.length === 0);
|
||||
|
||||
service.on('ready', () => {
|
||||
t.true(metrics.length === 1);
|
||||
|
||||
clock.tick(300);
|
||||
clock.restore();
|
||||
|
||||
process.nextTick(() => {
|
||||
t.true(metrics.length === 4);
|
||||
t.true(metrics[0].id === 1);
|
||||
t.true(metrics[1].id === 4);
|
||||
t.true(metrics[2].id === 3);
|
||||
t.true(metrics[3].id === 2);
|
||||
service.destroy();
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
@ -27,10 +27,7 @@ test.cb('should slice off list', (t) => {
|
||||
expireType: 'milliseconds',
|
||||
});
|
||||
|
||||
// console.time('4');
|
||||
// console.time('3');
|
||||
// console.time('2');
|
||||
// console.time('1');
|
||||
|
||||
list.add({ n: '1' }, moment().add(1, 'milliseconds'));
|
||||
list.add({ n: '2' }, moment().add(50, 'milliseconds'));
|
||||
list.add({ n: '3' }, moment().add(200, 'milliseconds'));
|
||||
|
@ -1,6 +1,7 @@
|
||||
/* eslint camelcase: "off" */
|
||||
'use strict';
|
||||
|
||||
const logger = require('../logger');
|
||||
const COLUMNS = ['app_name', 'instance_id', 'client_ip', 'last_seen', 'created_at'];
|
||||
const TABLE = 'client_instances';
|
||||
|
||||
@ -12,10 +13,24 @@ const mapRow = (row) => ({
|
||||
createdAt: row.created_at,
|
||||
});
|
||||
|
||||
const mapAppsRow = (row) => ({
|
||||
appName: row.app_name,
|
||||
createdAt: row.created_at,
|
||||
});
|
||||
|
||||
class ClientInstanceStore {
|
||||
|
||||
constructor (db) {
|
||||
this.db = db;
|
||||
setTimeout(() => this._removeInstancesOlderThanTwoDays(), 10).unref();
|
||||
setInterval(() => this._removeInstancesOlderThanTwoDays(), 24 * 61 * 60 * 1000).unref();
|
||||
}
|
||||
|
||||
_removeInstancesOlderThanTwoDays () {
|
||||
this.db(TABLE)
|
||||
.whereRaw('created_at < now() - interval \'2 days\'')
|
||||
.del()
|
||||
.then((res) => logger.info(`Deleted ${res} instances`));
|
||||
}
|
||||
|
||||
updateRow (details) {
|
||||
@ -58,6 +73,24 @@ class ClientInstanceStore {
|
||||
.orderBy('last_seen', 'desc')
|
||||
.map(mapRow);
|
||||
}
|
||||
|
||||
getByAppName (appName) {
|
||||
return this.db
|
||||
.select(COLUMNS)
|
||||
.from(TABLE)
|
||||
.where('app_name', appName)
|
||||
.orderBy('last_seen', 'desc')
|
||||
.map(mapRow);
|
||||
}
|
||||
|
||||
getApplications () {
|
||||
return this.db
|
||||
.distinct('app_name')
|
||||
.select(['app_name'])
|
||||
.from(TABLE)
|
||||
.orderBy('app_name', 'desc')
|
||||
.map(mapRow);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = ClientInstanceStore;
|
||||
|
58
lib/db/client-metrics-db.js
Normal file
58
lib/db/client-metrics-db.js
Normal file
@ -0,0 +1,58 @@
|
||||
'use strict';
|
||||
|
||||
const logger = require('../logger');
|
||||
|
||||
const METRICS_COLUMNS = ['id', 'created_at', 'metrics'];
|
||||
const TABLE = 'client_metrics';
|
||||
|
||||
const mapRow = (row) => ({
|
||||
id: row.id,
|
||||
createdAt: row.created_at,
|
||||
metrics: row.metrics,
|
||||
});
|
||||
|
||||
class ClientMetricsDb {
|
||||
constructor (db) {
|
||||
this.db = db;
|
||||
|
||||
//Clear old metrics regulary
|
||||
setTimeout(() => this.removeMetricsOlderThanOneHour(), 10).unref();
|
||||
setInterval(() => this.removeMetricsOlderThanOneHour(), 60 * 1000).unref();
|
||||
}
|
||||
|
||||
removeMetricsOlderThanOneHour () {
|
||||
this.db(TABLE)
|
||||
.whereRaw('created_at < now() - interval \'1 hour\'')
|
||||
.del()
|
||||
.then((res) => logger.info(`Deleted ${res} metrics`));
|
||||
}
|
||||
|
||||
// Insert new client metrics
|
||||
insert (metrics) {
|
||||
return this.db(TABLE).insert({ metrics });
|
||||
}
|
||||
|
||||
// Used at startup to load all metrics last week into memory!
|
||||
getMetricsLastHour () {
|
||||
return this.db
|
||||
.select(METRICS_COLUMNS)
|
||||
.from(TABLE)
|
||||
.limit(2000)
|
||||
.whereRaw('created_at > now() - interval \'1 hour\'')
|
||||
.orderBy('created_at', 'asc')
|
||||
.map(mapRow);
|
||||
}
|
||||
|
||||
// Used to poll for new metrics
|
||||
getNewMetrics (lastKnownId) {
|
||||
return this.db
|
||||
.select(METRICS_COLUMNS)
|
||||
.from(TABLE)
|
||||
.limit(1000)
|
||||
.where('id', '>', lastKnownId)
|
||||
.orderBy('created_at', 'asc')
|
||||
.map(mapRow);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = ClientMetricsDb;
|
@ -1,55 +1,53 @@
|
||||
'use strict';
|
||||
|
||||
const logger = require('../logger');
|
||||
const METRICS_COLUMNS = ['id', 'created_at', 'metrics'];
|
||||
const TABLE = 'client_metrics';
|
||||
|
||||
const mapRow = (row) => ({
|
||||
id: row.id,
|
||||
createdAt: row.created_at,
|
||||
metrics: row.metrics,
|
||||
});
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
class ClientMetricsStore {
|
||||
const TEN_SECONDS = 10 * 1000;
|
||||
|
||||
constructor (db) {
|
||||
this.db = db;
|
||||
setTimeout(() => this._removeMetricsOlderThanOneHour(), 10).unref();
|
||||
setInterval(() => this._removeMetricsOlderThanOneHour(), 60 * 60 * 1000).unref();
|
||||
class ClientMetricsStore extends EventEmitter {
|
||||
|
||||
constructor (metricsDb, pollInterval = TEN_SECONDS) {
|
||||
super();
|
||||
this.metricsDb = metricsDb;
|
||||
this.highestIdSeen = 0;
|
||||
this.timer;
|
||||
|
||||
//Build internal state
|
||||
metricsDb.getMetricsLastHour()
|
||||
.then((metrics) => this._emitMetrics(metrics))
|
||||
.then(() => this._startPoller(pollInterval))
|
||||
.then(() => this.emit('ready'))
|
||||
.catch((err) => logger.error(err));
|
||||
}
|
||||
|
||||
_removeMetricsOlderThanOneHour () {
|
||||
this.db(TABLE)
|
||||
.whereRaw('created_at < now() - interval \'1 hour\'')
|
||||
.del()
|
||||
.then((res) => logger.info(`Deleted ${res} metrics`));
|
||||
_startPoller (pollInterval) {
|
||||
this.timer = setInterval(() => this._fetchNewAndEmit(), pollInterval);
|
||||
this.timer.unref();
|
||||
}
|
||||
|
||||
_fetchNewAndEmit() {
|
||||
this.metricsDb.getNewMetrics(this.highestIdSeen)
|
||||
.then((metrics) => this._emitMetrics(metrics))
|
||||
}
|
||||
|
||||
_emitMetrics (metrics) {
|
||||
if (metrics && metrics.length > 0) {
|
||||
this.highestIdSeen = metrics[metrics.length - 1].id;
|
||||
metrics.forEach(m => this.emit('metrics', m.metrics));
|
||||
}
|
||||
}
|
||||
|
||||
// Insert new client metrics
|
||||
insert (metrics) {
|
||||
return this.db(TABLE).insert({ metrics });
|
||||
return this.metricsDb.insert(metrics)
|
||||
}
|
||||
|
||||
// Used at startup to load all metrics last week into memory!
|
||||
getMetricsLastHour () {
|
||||
return this.db
|
||||
.select(METRICS_COLUMNS)
|
||||
.from(TABLE)
|
||||
.limit(2000)
|
||||
.whereRaw('created_at > now() - interval \'1 hour\'')
|
||||
.orderBy('created_at', 'asc')
|
||||
.map(mapRow);
|
||||
}
|
||||
|
||||
// Used to poll for new metrics
|
||||
getNewMetrics (lastKnownId) {
|
||||
return this.db
|
||||
.select(METRICS_COLUMNS)
|
||||
.from(TABLE)
|
||||
.limit(1000)
|
||||
.where('id', '>', lastKnownId)
|
||||
.orderBy('created_at', 'asc')
|
||||
.map(mapRow);
|
||||
destroy () {
|
||||
try {
|
||||
clearInterval(this.timer);
|
||||
} catch (e) {}
|
||||
}
|
||||
};
|
||||
|
||||
|
62
lib/db/client-metrics-store.test.js
Normal file
62
lib/db/client-metrics-store.test.js
Normal file
@ -0,0 +1,62 @@
|
||||
'use strict';
|
||||
|
||||
const { test } = require('ava');
|
||||
const ClientMetricStore = require('./client-metrics-store');
|
||||
const sinon = require('sinon');
|
||||
|
||||
function getMockDb () {
|
||||
const list = [{ id: 4, metrics: {appName: 'test'} }, { id: 3, metrics: {appName: 'test'} }, { id: 2, metrics: {appName: 'test'} }];
|
||||
return {
|
||||
getMetricsLastHour () {
|
||||
return Promise.resolve([{ id: 1, metrics: {appName: 'test'} }]);
|
||||
},
|
||||
|
||||
getNewMetrics (v) {
|
||||
return Promise.resolve([list.pop() || { id: 0 }]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
test.cb('should call database on startup', (t) => {
|
||||
const mock = getMockDb();
|
||||
|
||||
const store = new ClientMetricStore(mock);
|
||||
|
||||
t.plan(2);
|
||||
|
||||
|
||||
store.on('metrics', (metrics) => {
|
||||
t.true(store.highestIdSeen === 1);
|
||||
t.true(metrics.appName === 'test');
|
||||
store.destroy();
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test.cb('should poll for updates', (t) => {
|
||||
const clock = sinon.useFakeTimers();
|
||||
|
||||
const mock = getMockDb();
|
||||
const store = new ClientMetricStore(mock, 100);
|
||||
|
||||
const metrics = [];
|
||||
store.on('metrics', (m) => metrics.push(m));
|
||||
|
||||
t.true(metrics.length === 0);
|
||||
|
||||
store.on('ready', () => {
|
||||
t.true(metrics.length === 1);
|
||||
clock.tick(300);
|
||||
process.nextTick(() => {
|
||||
t.true(metrics.length === 4);
|
||||
t.true(store.highestIdSeen === 4);
|
||||
store.destroy();
|
||||
clock.restore();
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -49,6 +49,14 @@ class ClientStrategyStore {
|
||||
.from(TABLE)
|
||||
.map(mapRow);
|
||||
}
|
||||
|
||||
getByAppName (appName) {
|
||||
return this.db
|
||||
.select(COLUMNS)
|
||||
.where('app_name', appName)
|
||||
.from(TABLE)
|
||||
.map(mapRow);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = ClientStrategyStore;
|
||||
|
@ -5,12 +5,14 @@ const EventStore = require('./event-store');
|
||||
const FeatureToggleStore = require('./feature-toggle-store');
|
||||
const StrategyStore = require('./strategy-store');
|
||||
const ClientInstanceStore = require('./client-instance-store');
|
||||
const ClientMetricsDb = require('./client-metrics-db');
|
||||
const ClientMetricsStore = require('./client-metrics-store');
|
||||
const ClientStrategyStore = require('./client-strategy-store');
|
||||
|
||||
module.exports.createStores = (config) => {
|
||||
const db = createDb(config);
|
||||
const eventStore = new EventStore(db);
|
||||
const clientMetricsDb = new ClientMetricsDb(db);
|
||||
|
||||
return {
|
||||
db,
|
||||
@ -18,7 +20,7 @@ module.exports.createStores = (config) => {
|
||||
featureToggleStore: new FeatureToggleStore(db, eventStore),
|
||||
strategyStore: new StrategyStore(db, eventStore),
|
||||
clientInstanceStore: new ClientInstanceStore(db),
|
||||
clientMetricsStore: new ClientMetricsStore(db),
|
||||
clientMetricsStore: new ClientMetricsStore(clientMetricsDb),
|
||||
clientStrategyStore: new ClientStrategyStore(db),
|
||||
};
|
||||
};
|
||||
|
@ -2,10 +2,16 @@
|
||||
|
||||
const logger = require('../logger');
|
||||
const ClientMetrics = require('../client-metrics');
|
||||
const ClientMetricsService = require('../client-metrics/service');
|
||||
const joi = require('joi');
|
||||
const { clientMetricsSchema, clientRegisterSchema } = require('./metrics-schema');
|
||||
|
||||
/*
|
||||
* TODO:
|
||||
* - always catch errors and always return a response to client!
|
||||
* - clean up and document uri endpoint
|
||||
* - always json response (middleware?)
|
||||
* - fix failing tests
|
||||
*/
|
||||
module.exports = function (app, config) {
|
||||
const {
|
||||
clientMetricsStore,
|
||||
@ -13,17 +19,17 @@ module.exports = function (app, config) {
|
||||
clientInstanceStore,
|
||||
} = config.stores;
|
||||
|
||||
const metrics = new ClientMetrics();
|
||||
const service = new ClientMetricsService(clientMetricsStore);
|
||||
const metrics = new ClientMetrics(clientMetricsStore);
|
||||
|
||||
service.on('metrics', (entries) => {
|
||||
entries.forEach((m) => {
|
||||
metrics.addPayload(m.metrics);
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/metrics', (req, res) => {
|
||||
res.json(metrics.getMetricsOverview());
|
||||
/**
|
||||
* [{
|
||||
"appName": "mfinn",
|
||||
"toggles": ["toggle1", "toggle2"],
|
||||
}]
|
||||
*/
|
||||
app.get('/client/seen-toggles', (req, res) => {
|
||||
const seenAppToggles = metrics.getAppsWitToggles();
|
||||
res.json(seenAppToggles);
|
||||
});
|
||||
|
||||
app.get('/metrics/features', (req, res) => {
|
||||
@ -32,11 +38,20 @@ module.exports = function (app, config) {
|
||||
|
||||
app.post('/client/metrics', (req, res) => {
|
||||
const data = req.body;
|
||||
const clientIp = req.ip;
|
||||
|
||||
joi.validate(data, clientMetricsSchema, (err, cleaned) => {
|
||||
if (err) {
|
||||
return res.status(400).json(err);
|
||||
}
|
||||
service.insert(cleaned)
|
||||
|
||||
clientMetricsStore
|
||||
.insert(cleaned)
|
||||
.then(() => clientInstanceStore.insert({
|
||||
appName: cleaned.appName,
|
||||
instanceId: cleaned.instanceId,
|
||||
clientIp,
|
||||
}))
|
||||
.catch(e => logger.error('Error inserting metrics data', e));
|
||||
|
||||
res.status(202).end();
|
||||
@ -52,7 +67,8 @@ module.exports = function (app, config) {
|
||||
return res.status(400).json(err);
|
||||
}
|
||||
|
||||
clientStrategyStore.insert(cleaned.appName, cleaned.strategies)
|
||||
clientStrategyStore
|
||||
.insert(cleaned.appName, cleaned.strategies)
|
||||
.then(() => clientInstanceStore.insert({
|
||||
appName: cleaned.appName,
|
||||
instanceId: cleaned.instanceId,
|
||||
@ -66,12 +82,40 @@ module.exports = function (app, config) {
|
||||
});
|
||||
|
||||
app.get('/client/strategies', (req, res) => {
|
||||
clientStrategyStore.getAll().then(data => res.json(data));
|
||||
const appName = req.query.appName;
|
||||
if(appName) {
|
||||
clientStrategyStore.getByAppName(appName)
|
||||
.then(data => res.json(data))
|
||||
.catch(err => logger.error(err));
|
||||
} else {
|
||||
clientStrategyStore.getAll()
|
||||
.then(data => res.json(data))
|
||||
.catch(err => logger.error(err));
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/client/instances', (req, res) => {
|
||||
clientInstanceStore.getAll()
|
||||
.then(data => res.json(data))
|
||||
app.get('/client/applications/', (req, res) => {
|
||||
clientInstanceStore.getApplications()
|
||||
.then(apps => {
|
||||
const applications = apps.map(({appName}) => ({
|
||||
appName: appName,
|
||||
links: {
|
||||
appDetails: `/api/client/applications/${appName}`
|
||||
}
|
||||
}))
|
||||
res.json({applications})
|
||||
})
|
||||
.catch(err => logger.error(err));
|
||||
});
|
||||
|
||||
app.get('/client/applications/:appName', (req, res) => {
|
||||
const appName = req.params.appName;
|
||||
const seenToggles = metrics.getSeenTogglesByAppName(appName);
|
||||
Promise.all([
|
||||
clientInstanceStore.getByAppName(appName),
|
||||
clientStrategyStore.getByAppName(appName)
|
||||
])
|
||||
.then(([instances, strategies]) => res.json({appName, instances, strategies, seenToggles}))
|
||||
.catch(err => logger.error(err));
|
||||
});
|
||||
};
|
||||
|
@ -78,6 +78,13 @@ function createClientInstance (stores) {
|
||||
started: Date.now(),
|
||||
interval: 10,
|
||||
},
|
||||
{
|
||||
appName: 'demo-seed-2',
|
||||
instanceId: 'test-2',
|
||||
strategies: ['default'],
|
||||
started: Date.now(),
|
||||
interval: 10,
|
||||
},
|
||||
].map(client => stores.clientInstanceStore.insert(client));
|
||||
}
|
||||
|
||||
|
@ -51,14 +51,27 @@ test.serial('should get client strategies', async t => {
|
||||
.then(destroy);
|
||||
});
|
||||
|
||||
test.serial('should get client instances', async t => {
|
||||
test.serial('should get application details', async t => {
|
||||
const { request, destroy } = await setupApp('metrics_serial');
|
||||
return request
|
||||
.get('/api/client/instances')
|
||||
.get('/api/client/applications/demo-seed')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect((res) => {
|
||||
t.true(res.status === 200);
|
||||
t.true(res.body.length === 1);
|
||||
t.true(res.body.appName === 'demo-seed');
|
||||
t.true(res.body.instances.length === 1);
|
||||
})
|
||||
.then(destroy);
|
||||
});
|
||||
|
||||
test.serial('should get list of applications', async t => {
|
||||
const { request, destroy } = await setupApp('metrics_serial');
|
||||
return request
|
||||
.get('/api/client/applications')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect((res) => {
|
||||
t.true(res.status === 200);
|
||||
t.true(res.body.applications.length === 2);
|
||||
})
|
||||
.then(destroy);
|
||||
});
|
||||
|
@ -3,4 +3,5 @@
|
||||
module.exports = () => ({
|
||||
getMetricsLastHour: () => Promise.resolve([]),
|
||||
insert: () => Promise.resolve(),
|
||||
on: () => {}
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user