mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-20 00:08:02 +01:00
add metrics projections for last hour
This commit is contained in:
parent
7e86867de5
commit
7c53bfa60b
@ -1,29 +1,42 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const Projection = require('./projection.js');
|
||||||
|
const TTLList = require('./ttl-list.js');
|
||||||
|
|
||||||
module.exports = class UnleashClientMetrics {
|
module.exports = class UnleashClientMetrics {
|
||||||
constructor () {
|
constructor () {
|
||||||
this.globalCount = 0;
|
this.globalCount = 0;
|
||||||
this.apps = [];
|
this.apps = {};
|
||||||
this.clients = {};
|
this.clients = {};
|
||||||
this.strategies = {};
|
this.strategies = {};
|
||||||
this.buckets = {};
|
this.buckets = {};
|
||||||
|
|
||||||
|
this.hourProjectionValue = new Projection();
|
||||||
|
this.oneHourLruCache = new TTLList();
|
||||||
|
this.oneHourLruCache.on('expire', (toggles) => {
|
||||||
|
Object.keys(toggles).forEach(toggleName => {
|
||||||
|
this.hourProjectionValue.substract(toggleName, toggles[toggleName]);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON () {
|
toJSON () {
|
||||||
return JSON.stringify(this.getState(), null, 4);
|
return JSON.stringify(this.getMetricsOverview(), null, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
getState () {
|
getMetricsOverview () {
|
||||||
// TODO need to flatten the store / possibly evict/flag stale clients
|
|
||||||
return {
|
return {
|
||||||
globalCount: this.globalCount,
|
globalCount: this.globalCount,
|
||||||
apps: this.apps,
|
apps: this.apps,
|
||||||
clients: this.clients,
|
clients: this.clients,
|
||||||
strategies: this.strategies,
|
strategies: this.strategies,
|
||||||
buckets: this.buckets,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTogglesMetrics () {
|
||||||
|
return this.hourProjectionValue.getProjection();
|
||||||
|
}
|
||||||
|
|
||||||
registerClient (data) {
|
registerClient (data) {
|
||||||
this.addClient(data.appName, data.instanceId, data.started);
|
this.addClient(data.appName, data.instanceId, data.started);
|
||||||
this.addStrategies(data.appName, data.strategies);
|
this.addStrategies(data.appName, data.strategies);
|
||||||
@ -35,23 +48,19 @@ module.exports = class UnleashClientMetrics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addBucket (appName, instanceId, bucket) {
|
addBucket (appName, instanceId, bucket) {
|
||||||
// TODO normalize time client-server-time / NTP?
|
|
||||||
let count = 0;
|
let count = 0;
|
||||||
const { start, stop, toggles } = bucket;
|
// TODO stop should be createdAt
|
||||||
Object.keys(toggles).forEach((n) => {
|
const { stop, toggles } = bucket;
|
||||||
if (this.buckets[n]) {
|
|
||||||
this.buckets[n].yes.push({ start, stop, count: toggles[n].yes });
|
|
||||||
this.buckets[n].no.push({ start, stop, count: toggles[n].no });
|
|
||||||
} else {
|
|
||||||
this.buckets[n] = {
|
|
||||||
yes: [{ start, stop, count: toggles[n].yes }],
|
|
||||||
no: [{ start, stop, count: toggles[n].no }],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
count += (toggles[n].yes + toggles[n].no);
|
Object.keys(toggles).forEach((n) => {
|
||||||
|
const entry = toggles[n];
|
||||||
|
this.hourProjectionValue.add(n, entry);
|
||||||
|
count += (entry.yes + entry.no);
|
||||||
});
|
});
|
||||||
this.addClientCount(instanceId, count);
|
|
||||||
|
this.oneHourLruCache.add(toggles, stop);
|
||||||
|
|
||||||
|
this.addClientCount(appName, instanceId, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
addStrategies (appName, strategyNames) {
|
addStrategies (appName, strategyNames) {
|
||||||
@ -63,7 +72,7 @@ module.exports = class UnleashClientMetrics {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addClientCount (instanceId, count) {
|
addClientCount (appName, instanceId, count) {
|
||||||
if (typeof count === 'number' && count > 0) {
|
if (typeof count === 'number' && count > 0) {
|
||||||
this.globalCount += count;
|
this.globalCount += count;
|
||||||
if (this.clients[instanceId]) {
|
if (this.clients[instanceId]) {
|
||||||
@ -73,7 +82,7 @@ module.exports = class UnleashClientMetrics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addClient (appName, instanceId, started = new Date()) {
|
addClient (appName, instanceId, started = new Date()) {
|
||||||
this.addApp(appName);
|
this.addApp(appName, instanceId);
|
||||||
if (instanceId) {
|
if (instanceId) {
|
||||||
if (this.clients[instanceId]) {
|
if (this.clients[instanceId]) {
|
||||||
this.clients[instanceId].ping = new Date();
|
this.clients[instanceId].ping = new Date();
|
||||||
@ -89,9 +98,16 @@ module.exports = class UnleashClientMetrics {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addApp (v) {
|
addApp (appName, instanceId) {
|
||||||
if (v && !this.apps.includes(v)) {
|
if (appName && !this.apps[appName]) {
|
||||||
this.apps.push(v);
|
this.apps[appName] = {
|
||||||
|
count: 0,
|
||||||
|
clients: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instanceId && !this.apps[appName].clients.includes(instanceId)) {
|
||||||
|
this.apps[appName].clients.push(instanceId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
35
packages/unleash-api/lib/client-metrics/projection.js
Normal file
35
packages/unleash-api/lib/client-metrics/projection.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = class Projection {
|
||||||
|
constructor () {
|
||||||
|
this.store = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
getProjection () {
|
||||||
|
return this.store;
|
||||||
|
}
|
||||||
|
|
||||||
|
add (name, countObj) {
|
||||||
|
if (this.store[name]) {
|
||||||
|
this.store[name].yes += countObj.yes;
|
||||||
|
this.store[name].no += countObj.no;
|
||||||
|
} else {
|
||||||
|
this.store[name] = {
|
||||||
|
yes: countObj.yes,
|
||||||
|
no: countObj.no,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
substract (name, countObj) {
|
||||||
|
if (this.store[name]) {
|
||||||
|
this.store[name].yes -= countObj.yes;
|
||||||
|
this.store[name].no -= countObj.no;
|
||||||
|
} else {
|
||||||
|
this.store[name] = {
|
||||||
|
yes: 0,
|
||||||
|
no: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
48
packages/unleash-api/lib/client-metrics/ttl-list.js
Normal file
48
packages/unleash-api/lib/client-metrics/ttl-list.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { EventEmitter } = require('events');
|
||||||
|
const yallist = require('yallist');
|
||||||
|
const moment = require('moment');
|
||||||
|
|
||||||
|
// this list must have entires with sorted ttl range
|
||||||
|
module.exports = class TTLList extends EventEmitter {
|
||||||
|
constructor () {
|
||||||
|
super();
|
||||||
|
this.cache = yallist.create();
|
||||||
|
setInterval(() => {
|
||||||
|
this.timedCheck();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
expire (entry) {
|
||||||
|
this.emit('expire', entry.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
add (value, timestamp) {
|
||||||
|
const ttl = moment(timestamp).add(1, 'hour');
|
||||||
|
this.cache.push({ ttl, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
timedCheck () {
|
||||||
|
const now = moment(new Date());
|
||||||
|
// find index to remove
|
||||||
|
let done = false;
|
||||||
|
// TODO: might use internal linkedlist
|
||||||
|
this.cache.forEachReverse((entry, index) => {
|
||||||
|
console.log(now.format(), entry.ttl.format());
|
||||||
|
if (done) {
|
||||||
|
return;
|
||||||
|
} else if (now.isBefore(entry.ttl)) {
|
||||||
|
// When we hit a valid ttl, remove next items in list (iteration is reversed)
|
||||||
|
this.cache = this.cache.slice(0, index + 1);
|
||||||
|
done = true;
|
||||||
|
} else if (index === 0) {
|
||||||
|
this.expire(entry);
|
||||||
|
// if rest of list has timed out, let it DIE!
|
||||||
|
this.cache = yallist.create(); // empty=
|
||||||
|
} else {
|
||||||
|
this.expire(entry);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -14,22 +14,25 @@ module.exports = function (app, config) {
|
|||||||
|
|
||||||
|
|
||||||
service.on('metrics', (entries) => {
|
service.on('metrics', (entries) => {
|
||||||
entries.forEach((m) => metrics.addPayload(m.metrics));
|
entries.forEach((m) => {
|
||||||
});
|
metrics.addPayload(m.metrics);
|
||||||
|
});
|
||||||
app.get('/service-metrics', (req, res) => {
|
|
||||||
res.json(service.getMetrics());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/metrics', (req, res) => {
|
app.get('/metrics', (req, res) => {
|
||||||
res.json(metrics.getState());
|
res.json(metrics.getMetricsOverview());
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/toggle-metrics', (req, res) => {
|
||||||
|
res.json(metrics.getTogglesMetrics());
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/client/metrics', (req, res) => {
|
app.post('/client/metrics', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const data = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
|
const data = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
|
||||||
metrics.addPayload(data);
|
service
|
||||||
service.insert(data);
|
.insert(data)
|
||||||
|
.catch(e => logger.error('Error inserting metrics data', e));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error receiving metrics', e);
|
logger.error('Error receiving metrics', e);
|
||||||
}
|
}
|
||||||
|
@ -59,8 +59,10 @@
|
|||||||
"install": "^0.8.1",
|
"install": "^0.8.1",
|
||||||
"knex": "^0.11.10",
|
"knex": "^0.11.10",
|
||||||
"log4js": "^0.6.38",
|
"log4js": "^0.6.38",
|
||||||
|
"moment": "^2.15.2",
|
||||||
"pg": "^6.1.0",
|
"pg": "^6.1.0",
|
||||||
"serve-favicon": "^2.3.0"
|
"serve-favicon": "^2.3.0",
|
||||||
|
"yallist": "^2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"coveralls": "^2.11.12",
|
"coveralls": "^2.11.12",
|
||||||
|
Loading…
Reference in New Issue
Block a user