mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-31 01:16:01 +02:00
Prettier/lint
This commit is contained in:
parent
0ea10bc9b2
commit
375a8b1d4f
14
.eslintrc
14
.eslintrc
@ -1,12 +1,22 @@
|
|||||||
{
|
{
|
||||||
"extends": [
|
"extends": [
|
||||||
"finn",
|
"finn",
|
||||||
"finn/node"
|
"finn/node",
|
||||||
|
"finn-prettier"
|
||||||
],
|
],
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": "2017"
|
"ecmaVersion": "2017"
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"max-nested-callbacks": "off"
|
"max-nested-callbacks": "off",
|
||||||
|
"new-cap": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"capIsNewExceptions": [
|
||||||
|
"Router",
|
||||||
|
"Mitm"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
27
lib/app.js
27
lib/app.js
@ -14,7 +14,7 @@ const errorHandler = require('errorhandler');
|
|||||||
|
|
||||||
const { REQUEST_TIME } = require('./events');
|
const { REQUEST_TIME } = require('./events');
|
||||||
|
|
||||||
module.exports = function (config) {
|
module.exports = function(config) {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
const baseUriPath = config.baseUriPath || '';
|
const baseUriPath = config.baseUriPath || '';
|
||||||
@ -34,10 +34,17 @@ module.exports = function (config) {
|
|||||||
app.use(favicon(path.join(publicFolder, 'favicon.ico')));
|
app.use(favicon(path.join(publicFolder, 'favicon.ico')));
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(responseTime((req, res, time) => {
|
app.use(
|
||||||
const timingInfo = { path: req.path, method: req.method, statusCode: res.statusCode, time };
|
responseTime((req, res, time) => {
|
||||||
config.eventBus.emit(REQUEST_TIME, timingInfo);
|
const timingInfo = {
|
||||||
}));
|
path: req.path,
|
||||||
|
method: req.method,
|
||||||
|
statusCode: res.statusCode,
|
||||||
|
time,
|
||||||
|
};
|
||||||
|
config.eventBus.emit(REQUEST_TIME, timingInfo);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
app.use(validator([]));
|
app.use(validator([]));
|
||||||
|
|
||||||
@ -48,10 +55,12 @@ module.exports = function (config) {
|
|||||||
app.use(bodyParser.json({ strict: false }));
|
app.use(bodyParser.json({ strict: false }));
|
||||||
|
|
||||||
if (config.enableRequestLogger) {
|
if (config.enableRequestLogger) {
|
||||||
app.use(log4js.connectLogger(logger, {
|
app.use(
|
||||||
format: ':status :method :url :response-timems',
|
log4js.connectLogger(logger, {
|
||||||
level: 'auto', // 3XX=WARN, 4xx/5xx=ERROR
|
format: ':status :method :url :response-timems',
|
||||||
}));
|
level: 'auto', // 3XX=WARN, 4xx/5xx=ERROR
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof config.preRouterHook === 'function') {
|
if (typeof config.preRouterHook === 'function') {
|
||||||
|
@ -16,16 +16,20 @@ test('should not throw when valid config', t => {
|
|||||||
|
|
||||||
test('should call preHook', t => {
|
test('should call preHook', t => {
|
||||||
let called = 0;
|
let called = 0;
|
||||||
getApp({ preHook: () => {
|
getApp({
|
||||||
called++;
|
preHook: () => {
|
||||||
} });
|
called++;
|
||||||
|
},
|
||||||
|
});
|
||||||
t.true(called === 1);
|
t.true(called === 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should call preRouterHook', t => {
|
test('should call preRouterHook', t => {
|
||||||
let called = 0;
|
let called = 0;
|
||||||
getApp({ preRouterHook: () => {
|
getApp({
|
||||||
called++;
|
preRouterHook: () => {
|
||||||
} });
|
called++;
|
||||||
|
},
|
||||||
|
});
|
||||||
t.true(called === 1);
|
t.true(called === 1);
|
||||||
});
|
});
|
||||||
|
@ -10,7 +10,7 @@ const { EventEmitter } = require('events');
|
|||||||
const appName = 'appName';
|
const appName = 'appName';
|
||||||
const instanceId = 'instanceId';
|
const instanceId = 'instanceId';
|
||||||
|
|
||||||
test('should work without state', (t) => {
|
test('should work without state', t => {
|
||||||
const store = new EventEmitter();
|
const store = new EventEmitter();
|
||||||
const metrics = new UnleashClientMetrics(store);
|
const metrics = new UnleashClientMetrics(store);
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ test('should work without state', (t) => {
|
|||||||
metrics.destroy();
|
metrics.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test.cb('data should expire', (t) => {
|
test.cb('data should expire', t => {
|
||||||
const clock = sinon.useFakeTimers();
|
const clock = sinon.useFakeTimers();
|
||||||
|
|
||||||
const store = new EventEmitter();
|
const store = new EventEmitter();
|
||||||
@ -84,9 +84,14 @@ test('should listen to metrics from store', t => {
|
|||||||
t.truthy(metrics.apps[appName].count === 123);
|
t.truthy(metrics.apps[appName].count === 123);
|
||||||
t.truthy(metrics.globalCount === 123);
|
t.truthy(metrics.globalCount === 123);
|
||||||
|
|
||||||
t.deepEqual(metrics.getTogglesMetrics().lastHour.toggleX, { yes: 123, no: 0 });
|
t.deepEqual(metrics.getTogglesMetrics().lastHour.toggleX, {
|
||||||
t.deepEqual(metrics.getTogglesMetrics().lastMinute.toggleX, { yes: 123, no: 0 });
|
yes: 123,
|
||||||
|
no: 0,
|
||||||
|
});
|
||||||
|
t.deepEqual(metrics.getTogglesMetrics().lastMinute.toggleX, {
|
||||||
|
yes: 123,
|
||||||
|
no: 0,
|
||||||
|
});
|
||||||
|
|
||||||
metrics.addPayload({
|
metrics.addPayload({
|
||||||
appName,
|
appName,
|
||||||
@ -104,8 +109,14 @@ test('should listen to metrics from store', t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
t.truthy(metrics.globalCount === 143);
|
t.truthy(metrics.globalCount === 143);
|
||||||
t.deepEqual(metrics.getTogglesMetrics().lastHour.toggleX, { yes: 133, no: 10 });
|
t.deepEqual(metrics.getTogglesMetrics().lastHour.toggleX, {
|
||||||
t.deepEqual(metrics.getTogglesMetrics().lastMinute.toggleX, { yes: 133, no: 10 });
|
yes: 133,
|
||||||
|
no: 10,
|
||||||
|
});
|
||||||
|
t.deepEqual(metrics.getTogglesMetrics().lastMinute.toggleX, {
|
||||||
|
yes: 133,
|
||||||
|
no: 10,
|
||||||
|
});
|
||||||
|
|
||||||
metrics.destroy();
|
metrics.destroy();
|
||||||
});
|
});
|
||||||
@ -146,7 +157,6 @@ test('should build up list of seend toggles when new metrics arrives', t => {
|
|||||||
metrics.destroy();
|
metrics.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
test('should handle a lot of toggles', t => {
|
test('should handle a lot of toggles', t => {
|
||||||
const store = new EventEmitter();
|
const store = new EventEmitter();
|
||||||
const metrics = new UnleashClientMetrics(store);
|
const metrics = new UnleashClientMetrics(store);
|
||||||
@ -244,7 +254,6 @@ test('should have correct values for lastMinute', t => {
|
|||||||
clock.restore();
|
clock.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
test('should have correct values for lastHour', t => {
|
test('should have correct values for lastHour', t => {
|
||||||
const clock = sinon.useFakeTimers();
|
const clock = sinon.useFakeTimers();
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ const Projection = require('./projection.js');
|
|||||||
const TTLList = require('./ttl-list.js');
|
const TTLList = require('./ttl-list.js');
|
||||||
|
|
||||||
module.exports = class UnleashClientMetrics {
|
module.exports = class UnleashClientMetrics {
|
||||||
constructor (clientMetricsStore) {
|
constructor(clientMetricsStore) {
|
||||||
this.globalCount = 0;
|
this.globalCount = 0;
|
||||||
this.apps = {};
|
this.apps = {};
|
||||||
|
|
||||||
@ -21,20 +21,26 @@ module.exports = class UnleashClientMetrics {
|
|||||||
expireAmount: 1,
|
expireAmount: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.lastHourList.on('expire', (toggles) => {
|
this.lastHourList.on('expire', toggles => {
|
||||||
Object.keys(toggles).forEach(toggleName => {
|
Object.keys(toggles).forEach(toggleName => {
|
||||||
this.lastHourProjection.substract(toggleName, toggles[toggleName]);
|
this.lastHourProjection.substract(
|
||||||
|
toggleName,
|
||||||
|
toggles[toggleName]
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.lastMinuteList.on('expire', (toggles) => {
|
this.lastMinuteList.on('expire', toggles => {
|
||||||
Object.keys(toggles).forEach(toggleName => {
|
Object.keys(toggles).forEach(toggleName => {
|
||||||
this.lastMinuteProjection.substract(toggleName, toggles[toggleName]);
|
this.lastMinuteProjection.substract(
|
||||||
|
toggleName,
|
||||||
|
toggles[toggleName]
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
clientMetricsStore.on('metrics', (m) => this.addPayload(m));
|
clientMetricsStore.on('metrics', m => this.addPayload(m));
|
||||||
}
|
}
|
||||||
|
|
||||||
getAppsWithToggles () {
|
getAppsWithToggles() {
|
||||||
const apps = [];
|
const apps = [];
|
||||||
Object.keys(this.apps).forEach(appName => {
|
Object.keys(this.apps).forEach(appName => {
|
||||||
const seenToggles = Object.keys(this.apps[appName].seenToggles);
|
const seenToggles = Object.keys(this.apps[appName].seenToggles);
|
||||||
@ -43,14 +49,18 @@ module.exports = class UnleashClientMetrics {
|
|||||||
});
|
});
|
||||||
return apps;
|
return apps;
|
||||||
}
|
}
|
||||||
getSeenTogglesByAppName (appName) {
|
getSeenTogglesByAppName(appName) {
|
||||||
return this.apps[appName] ? Object.keys(this.apps[appName].seenToggles) : [];
|
return this.apps[appName]
|
||||||
|
? Object.keys(this.apps[appName].seenToggles)
|
||||||
|
: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getSeenAppsPerToggle () {
|
getSeenAppsPerToggle() {
|
||||||
const toggles = {};
|
const toggles = {};
|
||||||
Object.keys(this.apps).forEach(appName => {
|
Object.keys(this.apps).forEach(appName => {
|
||||||
Object.keys(this.apps[appName].seenToggles).forEach((seenToggleName) => {
|
Object.keys(
|
||||||
|
this.apps[appName].seenToggles
|
||||||
|
).forEach(seenToggleName => {
|
||||||
if (!toggles[seenToggleName]) {
|
if (!toggles[seenToggleName]) {
|
||||||
toggles[seenToggleName] = [];
|
toggles[seenToggleName] = [];
|
||||||
}
|
}
|
||||||
@ -60,36 +70,39 @@ module.exports = class UnleashClientMetrics {
|
|||||||
return toggles;
|
return toggles;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTogglesMetrics () {
|
getTogglesMetrics() {
|
||||||
return {
|
return {
|
||||||
lastHour: this.lastHourProjection.getProjection(),
|
lastHour: this.lastHourProjection.getProjection(),
|
||||||
lastMinute: this.lastMinuteProjection.getProjection(),
|
lastMinute: this.lastMinuteProjection.getProjection(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
addPayload (data) {
|
addPayload(data) {
|
||||||
const { appName, bucket } = data;
|
const { appName, bucket } = data;
|
||||||
const app = this.getApp(appName);
|
const app = this.getApp(appName);
|
||||||
this.addBucket(app, bucket);
|
this.addBucket(app, bucket);
|
||||||
}
|
}
|
||||||
|
|
||||||
getApp (appName) {
|
getApp(appName) {
|
||||||
this.apps[appName] = this.apps[appName] || { seenToggles: {}, count: 0 };
|
this.apps[appName] = this.apps[appName] || {
|
||||||
|
seenToggles: {},
|
||||||
|
count: 0,
|
||||||
|
};
|
||||||
return this.apps[appName];
|
return this.apps[appName];
|
||||||
}
|
}
|
||||||
|
|
||||||
addBucket (app, bucket) {
|
addBucket(app, bucket) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
// TODO stop should be createdAt
|
// TODO stop should be createdAt
|
||||||
const { stop, toggles } = bucket;
|
const { stop, toggles } = bucket;
|
||||||
|
|
||||||
const toggleNames = Object.keys(toggles);
|
const toggleNames = Object.keys(toggles);
|
||||||
|
|
||||||
toggleNames.forEach((n) => {
|
toggleNames.forEach(n => {
|
||||||
const entry = toggles[n];
|
const entry = toggles[n];
|
||||||
this.lastHourProjection.add(n, entry);
|
this.lastHourProjection.add(n, entry);
|
||||||
this.lastMinuteProjection.add(n, entry);
|
this.lastMinuteProjection.add(n, entry);
|
||||||
count += (entry.yes + entry.no);
|
count += entry.yes + entry.no;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.lastHourList.add(toggles, stop);
|
this.lastHourList.add(toggles, stop);
|
||||||
@ -100,13 +113,13 @@ module.exports = class UnleashClientMetrics {
|
|||||||
this.addSeenToggles(app, toggleNames);
|
this.addSeenToggles(app, toggleNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
addSeenToggles (app, toggleNames) {
|
addSeenToggles(app, toggleNames) {
|
||||||
toggleNames.forEach(t => {
|
toggleNames.forEach(t => {
|
||||||
app.seenToggles[t] = true;
|
app.seenToggles[t] = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy () {
|
destroy() {
|
||||||
this.lastHourList.destroy();
|
this.lastHourList.destroy();
|
||||||
this.lastMinuteList.destroy();
|
this.lastMinuteList.destroy();
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,12 @@
|
|||||||
const { EventEmitter } = require('events');
|
const { EventEmitter } = require('events');
|
||||||
|
|
||||||
class Node {
|
class Node {
|
||||||
constructor (value) {
|
constructor(value) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
this.next = null;
|
this.next = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
link (next) {
|
link(next) {
|
||||||
this.next = next;
|
this.next = next;
|
||||||
next.prev = this;
|
next.prev = this;
|
||||||
return this;
|
return this;
|
||||||
@ -16,13 +16,13 @@ class Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = class List extends EventEmitter {
|
module.exports = class List extends EventEmitter {
|
||||||
constructor () {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.start = null;
|
this.start = null;
|
||||||
this.tail = null;
|
this.tail = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
add (obj) {
|
add(obj) {
|
||||||
const node = new Node(obj);
|
const node = new Node(obj);
|
||||||
if (this.start) {
|
if (this.start) {
|
||||||
this.start = node.link(this.start);
|
this.start = node.link(this.start);
|
||||||
@ -33,7 +33,7 @@ module.exports = class List extends EventEmitter {
|
|||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
iterate (fn) {
|
iterate(fn) {
|
||||||
if (!this.start) {
|
if (!this.start) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -48,7 +48,7 @@ module.exports = class List extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
iterateReverse (fn) {
|
iterateReverse(fn) {
|
||||||
if (!this.tail) {
|
if (!this.tail) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -63,7 +63,7 @@ module.exports = class List extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reverseRemoveUntilTrue (fn) {
|
reverseRemoveUntilTrue(fn) {
|
||||||
if (!this.tail) {
|
if (!this.tail) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -95,7 +95,7 @@ module.exports = class List extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toArray () {
|
toArray() {
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
if (this.start) {
|
if (this.start) {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const test = require('ava');
|
const { test } = require('ava');
|
||||||
const List = require('./list');
|
const List = require('./list');
|
||||||
|
|
||||||
function getList () {
|
function getList() {
|
||||||
const list = new List();
|
const list = new List();
|
||||||
list.add(1);
|
list.add(1);
|
||||||
list.add(2);
|
list.add(2);
|
||||||
@ -15,10 +15,10 @@ function getList () {
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
test('should emit "evicted" events for objects leaving list', (t) => {
|
test('should emit "evicted" events for objects leaving list', t => {
|
||||||
const list = getList();
|
const list = getList();
|
||||||
const evictedList = [];
|
const evictedList = [];
|
||||||
list.on('evicted', (value) => {
|
list.on('evicted', value => {
|
||||||
evictedList.push(value);
|
evictedList.push(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -43,7 +43,7 @@ test('should emit "evicted" events for objects leaving list', (t) => {
|
|||||||
t.true(evictedList.length === 8);
|
t.true(evictedList.length === 8);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('list should be able remove until given value', (t) => {
|
test('list should be able remove until given value', t => {
|
||||||
const list = getList();
|
const list = getList();
|
||||||
|
|
||||||
t.true(list.toArray().length === 7);
|
t.true(list.toArray().length === 7);
|
||||||
@ -58,7 +58,7 @@ test('list should be able remove until given value', (t) => {
|
|||||||
t.true(list.toArray().length === 3);
|
t.true(list.toArray().length === 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('list can be cleared and re-add entries', (t) => {
|
test('list can be cleared and re-add entries', t => {
|
||||||
const list = getList();
|
const list = getList();
|
||||||
|
|
||||||
list.add(8);
|
list.add(8);
|
||||||
@ -77,7 +77,7 @@ test('list can be cleared and re-add entries', (t) => {
|
|||||||
t.true(list.toArray().length === 3);
|
t.true(list.toArray().length === 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not iterate empty list ', (t) => {
|
test('should not iterate empty list ', t => {
|
||||||
const list = new List();
|
const list = new List();
|
||||||
|
|
||||||
let iterateCount = 0;
|
let iterateCount = 0;
|
||||||
@ -87,8 +87,7 @@ test('should not iterate empty list ', (t) => {
|
|||||||
t.true(iterateCount === 0);
|
t.true(iterateCount === 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should iterate', t => {
|
||||||
test('should iterate', (t) => {
|
|
||||||
const list = getList();
|
const list = getList();
|
||||||
|
|
||||||
let iterateCount = 0;
|
let iterateCount = 0;
|
||||||
@ -102,7 +101,7 @@ test('should iterate', (t) => {
|
|||||||
t.true(iterateCount === 4);
|
t.true(iterateCount === 4);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should reverse iterate', (t) => {
|
test('should reverse iterate', t => {
|
||||||
const list = getList();
|
const list = getList();
|
||||||
|
|
||||||
let iterateCount = 0;
|
let iterateCount = 0;
|
||||||
@ -116,7 +115,7 @@ test('should reverse iterate', (t) => {
|
|||||||
t.true(iterateCount === 5);
|
t.true(iterateCount === 5);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not reverse iterate empty list', (t) => {
|
test('should not reverse iterate empty list', t => {
|
||||||
const list = new List();
|
const list = new List();
|
||||||
|
|
||||||
let iterateCount = 0;
|
let iterateCount = 0;
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
module.exports = class Projection {
|
module.exports = class Projection {
|
||||||
constructor () {
|
constructor() {
|
||||||
this.store = {};
|
this.store = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
getProjection () {
|
getProjection() {
|
||||||
return this.store;
|
return this.store;
|
||||||
}
|
}
|
||||||
|
|
||||||
add (name, countObj) {
|
add(name, countObj) {
|
||||||
if (this.store[name]) {
|
if (this.store[name]) {
|
||||||
this.store[name].yes += countObj.yes;
|
this.store[name].yes += countObj.yes;
|
||||||
this.store[name].no += countObj.no;
|
this.store[name].no += countObj.no;
|
||||||
@ -21,7 +21,7 @@ module.exports = class Projection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
substract (name, countObj) {
|
substract(name, countObj) {
|
||||||
if (this.store[name]) {
|
if (this.store[name]) {
|
||||||
this.store[name].yes -= countObj.yes;
|
this.store[name].yes -= countObj.yes;
|
||||||
this.store[name].no -= countObj.no;
|
this.store[name].no -= countObj.no;
|
||||||
|
@ -6,11 +6,9 @@ const moment = require('moment');
|
|||||||
|
|
||||||
// this list must have entries with sorted ttl range
|
// this list must have entries with sorted ttl range
|
||||||
module.exports = class TTLList extends EventEmitter {
|
module.exports = class TTLList extends EventEmitter {
|
||||||
constructor ({
|
constructor(
|
||||||
interval = 1000,
|
{ interval = 1000, expireAmount = 1, expireType = 'hours' } = {}
|
||||||
expireAmount = 1,
|
) {
|
||||||
expireType = 'hours',
|
|
||||||
} = {}) {
|
|
||||||
super();
|
super();
|
||||||
this.interval = interval;
|
this.interval = interval;
|
||||||
this.expireAmount = expireAmount;
|
this.expireAmount = expireAmount;
|
||||||
@ -24,7 +22,7 @@ module.exports = class TTLList extends EventEmitter {
|
|||||||
this.startTimer();
|
this.startTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
startTimer () {
|
startTimer() {
|
||||||
if (this.list) {
|
if (this.list) {
|
||||||
this.timer = setTimeout(() => {
|
this.timer = setTimeout(() => {
|
||||||
if (this.list) {
|
if (this.list) {
|
||||||
@ -35,7 +33,7 @@ module.exports = class TTLList extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
add (value, timestamp = new Date()) {
|
add(value, timestamp = new Date()) {
|
||||||
const ttl = moment(timestamp).add(this.expireAmount, this.expireType);
|
const ttl = moment(timestamp).add(this.expireAmount, this.expireType);
|
||||||
if (moment().isBefore(ttl)) {
|
if (moment().isBefore(ttl)) {
|
||||||
this.list.add({ ttl, value });
|
this.list.add({ ttl, value });
|
||||||
@ -44,13 +42,15 @@ module.exports = class TTLList extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
timedCheck () {
|
timedCheck() {
|
||||||
const now = moment();
|
const now = moment();
|
||||||
this.list.reverseRemoveUntilTrue(({ value }) => now.isBefore(value.ttl));
|
this.list.reverseRemoveUntilTrue(({ value }) =>
|
||||||
|
now.isBefore(value.ttl)
|
||||||
|
);
|
||||||
this.startTimer();
|
this.startTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy () {
|
destroy() {
|
||||||
// https://github.com/nodejs/node/issues/9561
|
// https://github.com/nodejs/node/issues/9561
|
||||||
// clearTimeout(this.timer);
|
// clearTimeout(this.timer);
|
||||||
// this.timer = null;
|
// this.timer = null;
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const test = require('ava');
|
const { test } = require('ava');
|
||||||
const TTLList = require('./ttl-list');
|
const TTLList = require('./ttl-list');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
|
|
||||||
test.cb('should emit expire', (t) => {
|
test.cb('should emit expire', t => {
|
||||||
const list = new TTLList({
|
const list = new TTLList({
|
||||||
interval: 20,
|
interval: 20,
|
||||||
expireAmount: 10,
|
expireAmount: 10,
|
||||||
expireType: 'milliseconds',
|
expireType: 'milliseconds',
|
||||||
});
|
});
|
||||||
|
|
||||||
list.on('expire', (entry) => {
|
list.on('expire', entry => {
|
||||||
list.destroy();
|
list.destroy();
|
||||||
t.true(entry.n === 1);
|
t.true(entry.n === 1);
|
||||||
t.end();
|
t.end();
|
||||||
@ -21,7 +21,7 @@ test.cb('should emit expire', (t) => {
|
|||||||
list.add({ n: 1 });
|
list.add({ n: 1 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test.cb('should slice off list', (t) => {
|
test.cb('should slice off list', t => {
|
||||||
const clock = sinon.useFakeTimers();
|
const clock = sinon.useFakeTimers();
|
||||||
|
|
||||||
const list = new TTLList({
|
const list = new TTLList({
|
||||||
@ -30,7 +30,6 @@ test.cb('should slice off list', (t) => {
|
|||||||
expireType: 'milliseconds',
|
expireType: 'milliseconds',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
list.add({ n: '1' }, moment().add(1, 'milliseconds'));
|
list.add({ n: '1' }, moment().add(1, 'milliseconds'));
|
||||||
list.add({ n: '2' }, moment().add(50, 'milliseconds'));
|
list.add({ n: '2' }, moment().add(50, 'milliseconds'));
|
||||||
list.add({ n: '3' }, moment().add(200, 'milliseconds'));
|
list.add({ n: '3' }, moment().add(200, 'milliseconds'));
|
||||||
@ -38,7 +37,7 @@ test.cb('should slice off list', (t) => {
|
|||||||
|
|
||||||
const expired = [];
|
const expired = [];
|
||||||
|
|
||||||
list.on('expire', (entry) => {
|
list.on('expire', entry => {
|
||||||
// console.timeEnd(entry.n);
|
// console.timeEnd(entry.n);
|
||||||
expired.push(entry);
|
expired.push(entry);
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,19 @@
|
|||||||
/* eslint camelcase:off */
|
/* eslint camelcase:off */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const COLUMNS = ['app_name', 'created_at', 'updated_at', 'description', 'strategies', 'url', 'color', 'icon'];
|
const COLUMNS = [
|
||||||
|
'app_name',
|
||||||
|
'created_at',
|
||||||
|
'updated_at',
|
||||||
|
'description',
|
||||||
|
'strategies',
|
||||||
|
'url',
|
||||||
|
'color',
|
||||||
|
'icon',
|
||||||
|
];
|
||||||
const TABLE = 'client_applications';
|
const TABLE = 'client_applications';
|
||||||
|
|
||||||
const mapRow = (row) => ({
|
const mapRow = row => ({
|
||||||
appName: row.app_name,
|
appName: row.app_name,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
updatedAt: row.updated_at,
|
updatedAt: row.updated_at,
|
||||||
@ -25,24 +34,23 @@ const remapRow = (input, old = {}) => ({
|
|||||||
strategies: JSON.stringify(input.strategies || old.strategies),
|
strategies: JSON.stringify(input.strategies || old.strategies),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
class ClientApplicationsDb {
|
class ClientApplicationsDb {
|
||||||
constructor (db) {
|
constructor(db) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRow (details, prev) {
|
updateRow(details, prev) {
|
||||||
details.updatedAt = 'now()';
|
details.updatedAt = 'now()';
|
||||||
return this.db(TABLE)
|
return this.db(TABLE)
|
||||||
.where('app_name', details.appName)
|
.where('app_name', details.appName)
|
||||||
.update(remapRow(details, prev));
|
.update(remapRow(details, prev));
|
||||||
}
|
}
|
||||||
|
|
||||||
insertNewRow (details) {
|
insertNewRow(details) {
|
||||||
return this.db(TABLE).insert(remapRow(details));
|
return this.db(TABLE).insert(remapRow(details));
|
||||||
}
|
}
|
||||||
|
|
||||||
upsert (data) {
|
upsert(data) {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
throw new Error('Missing data to add / update');
|
throw new Error('Missing data to add / update');
|
||||||
}
|
}
|
||||||
@ -58,20 +66,17 @@ class ClientApplicationsDb {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getAll () {
|
getAll() {
|
||||||
return this.db
|
return this.db.select(COLUMNS).from(TABLE).map(mapRow);
|
||||||
.select(COLUMNS)
|
|
||||||
.from(TABLE)
|
|
||||||
.map(mapRow);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getApplication (appName) {
|
getApplication(appName) {
|
||||||
return this.db
|
return this.db
|
||||||
.select(COLUMNS)
|
.select(COLUMNS)
|
||||||
.where('app_name', appName)
|
.where('app_name', appName)
|
||||||
.from(TABLE)
|
.from(TABLE)
|
||||||
.map(mapRow)
|
.map(mapRow)
|
||||||
.then(list => list[0]);
|
.then(list => list[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -83,19 +88,21 @@ class ClientApplicationsDb {
|
|||||||
* ) as foo
|
* ) as foo
|
||||||
* WHERE foo.strategyName = '"other"';
|
* WHERE foo.strategyName = '"other"';
|
||||||
*/
|
*/
|
||||||
getAppsForStrategy (strategyName) {
|
getAppsForStrategy(strategyName) {
|
||||||
return this.db
|
return this.db
|
||||||
.select(COLUMNS)
|
.select(COLUMNS)
|
||||||
.from(TABLE)
|
.from(TABLE)
|
||||||
.map(mapRow)
|
.map(mapRow)
|
||||||
.then(apps => apps
|
.then(apps =>
|
||||||
.filter(app => app.strategies.includes(strategyName)));
|
apps.filter(app => app.strategies.includes(strategyName))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getApplications (filter) {
|
getApplications(filter) {
|
||||||
return filter && filter.strategyName ?
|
return filter && filter.strategyName
|
||||||
this.getAppsForStrategy(filter.strategyName) : this.getAll();
|
? this.getAppsForStrategy(filter.strategyName)
|
||||||
|
: this.getAll();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
module.exports = ClientApplicationsDb;
|
module.exports = ClientApplicationsDb;
|
||||||
|
@ -2,10 +2,16 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const logger = require('../logger');
|
const logger = require('../logger');
|
||||||
const COLUMNS = ['app_name', 'instance_id', 'client_ip', 'last_seen', 'created_at'];
|
const COLUMNS = [
|
||||||
|
'app_name',
|
||||||
|
'instance_id',
|
||||||
|
'client_ip',
|
||||||
|
'last_seen',
|
||||||
|
'created_at',
|
||||||
|
];
|
||||||
const TABLE = 'client_instances';
|
const TABLE = 'client_instances';
|
||||||
|
|
||||||
const mapRow = (row) => ({
|
const mapRow = row => ({
|
||||||
appName: row.app_name,
|
appName: row.app_name,
|
||||||
instanceId: row.instance_id,
|
instanceId: row.instance_id,
|
||||||
clientIp: row.client_ip,
|
clientIp: row.client_ip,
|
||||||
@ -19,21 +25,23 @@ const mapRow = (row) => ({
|
|||||||
// });
|
// });
|
||||||
|
|
||||||
class ClientInstanceStore {
|
class ClientInstanceStore {
|
||||||
|
constructor(db) {
|
||||||
constructor (db) {
|
|
||||||
this.db = db;
|
this.db = db;
|
||||||
setTimeout(() => this._removeInstancesOlderThanTwoDays(), 10).unref();
|
setTimeout(() => this._removeInstancesOlderThanTwoDays(), 10).unref();
|
||||||
setInterval(() => this._removeInstancesOlderThanTwoDays(), 24 * 61 * 60 * 1000).unref();
|
setInterval(
|
||||||
|
() => this._removeInstancesOlderThanTwoDays(),
|
||||||
|
24 * 61 * 60 * 1000
|
||||||
|
).unref();
|
||||||
}
|
}
|
||||||
|
|
||||||
_removeInstancesOlderThanTwoDays () {
|
_removeInstancesOlderThanTwoDays() {
|
||||||
this.db(TABLE)
|
this.db(TABLE)
|
||||||
.whereRaw('created_at < now() - interval \'2 days\'')
|
.whereRaw("created_at < now() - interval '2 days'")
|
||||||
.del()
|
.del()
|
||||||
.then((res) => res > 0 && logger.info(`Deleted ${res} instances`));
|
.then(res => res > 0 && logger.info(`Deleted ${res} instances`));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRow (details) {
|
updateRow(details) {
|
||||||
return this.db(TABLE)
|
return this.db(TABLE)
|
||||||
.where('app_name', details.appName)
|
.where('app_name', details.appName)
|
||||||
.where('instance_id', details.instanceId)
|
.where('instance_id', details.instanceId)
|
||||||
@ -43,7 +51,7 @@ class ClientInstanceStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
insertNewRow (details) {
|
insertNewRow(details) {
|
||||||
return this.db(TABLE).insert({
|
return this.db(TABLE).insert({
|
||||||
app_name: details.appName,
|
app_name: details.appName,
|
||||||
instance_id: details.instanceId,
|
instance_id: details.instanceId,
|
||||||
@ -51,7 +59,7 @@ class ClientInstanceStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
insert (details) {
|
insert(details) {
|
||||||
return this.db(TABLE)
|
return this.db(TABLE)
|
||||||
.count('*')
|
.count('*')
|
||||||
.where('app_name', details.appName)
|
.where('app_name', details.appName)
|
||||||
@ -66,7 +74,7 @@ class ClientInstanceStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getAll () {
|
getAll() {
|
||||||
return this.db
|
return this.db
|
||||||
.select(COLUMNS)
|
.select(COLUMNS)
|
||||||
.from(TABLE)
|
.from(TABLE)
|
||||||
@ -74,7 +82,7 @@ class ClientInstanceStore {
|
|||||||
.map(mapRow);
|
.map(mapRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
getByAppName (appName) {
|
getByAppName(appName) {
|
||||||
return this.db
|
return this.db
|
||||||
.select()
|
.select()
|
||||||
.from(TABLE)
|
.from(TABLE)
|
||||||
@ -83,7 +91,7 @@ class ClientInstanceStore {
|
|||||||
.map(mapRow);
|
.map(mapRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
getApplications () {
|
getApplications() {
|
||||||
return this.db
|
return this.db
|
||||||
.distinct('app_name')
|
.distinct('app_name')
|
||||||
.select(['app_name'])
|
.select(['app_name'])
|
||||||
@ -91,6 +99,6 @@ class ClientInstanceStore {
|
|||||||
.orderBy('app_name', 'desc')
|
.orderBy('app_name', 'desc')
|
||||||
.map(mapRow);
|
.map(mapRow);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
module.exports = ClientInstanceStore;
|
module.exports = ClientInstanceStore;
|
||||||
|
@ -5,46 +5,49 @@ const logger = require('../logger');
|
|||||||
const METRICS_COLUMNS = ['id', 'created_at', 'metrics'];
|
const METRICS_COLUMNS = ['id', 'created_at', 'metrics'];
|
||||||
const TABLE = 'client_metrics';
|
const TABLE = 'client_metrics';
|
||||||
|
|
||||||
const mapRow = (row) => ({
|
const mapRow = row => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
metrics: row.metrics,
|
metrics: row.metrics,
|
||||||
});
|
});
|
||||||
|
|
||||||
class ClientMetricsDb {
|
class ClientMetricsDb {
|
||||||
constructor (db) {
|
constructor(db) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
|
|
||||||
// Clear old metrics regulary
|
// Clear old metrics regulary
|
||||||
setTimeout(() => this.removeMetricsOlderThanOneHour(), 10).unref();
|
setTimeout(() => this.removeMetricsOlderThanOneHour(), 10).unref();
|
||||||
setInterval(() => this.removeMetricsOlderThanOneHour(), 60 * 1000).unref();
|
setInterval(
|
||||||
|
() => this.removeMetricsOlderThanOneHour(),
|
||||||
|
60 * 1000
|
||||||
|
).unref();
|
||||||
}
|
}
|
||||||
|
|
||||||
removeMetricsOlderThanOneHour () {
|
removeMetricsOlderThanOneHour() {
|
||||||
this.db(TABLE)
|
this.db(TABLE)
|
||||||
.whereRaw('created_at < now() - interval \'1 hour\'')
|
.whereRaw("created_at < now() - interval '1 hour'")
|
||||||
.del()
|
.del()
|
||||||
.then((res) => res > 0 && logger.info(`Deleted ${res} metrics`));
|
.then(res => res > 0 && logger.info(`Deleted ${res} metrics`));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert new client metrics
|
// Insert new client metrics
|
||||||
insert (metrics) {
|
insert(metrics) {
|
||||||
return this.db(TABLE).insert({ metrics });
|
return this.db(TABLE).insert({ metrics });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used at startup to load all metrics last week into memory!
|
// Used at startup to load all metrics last week into memory!
|
||||||
getMetricsLastHour () {
|
getMetricsLastHour() {
|
||||||
return this.db
|
return this.db
|
||||||
.select(METRICS_COLUMNS)
|
.select(METRICS_COLUMNS)
|
||||||
.from(TABLE)
|
.from(TABLE)
|
||||||
.limit(2000)
|
.limit(2000)
|
||||||
.whereRaw('created_at > now() - interval \'1 hour\'')
|
.whereRaw("created_at > now() - interval '1 hour'")
|
||||||
.orderBy('created_at', 'asc')
|
.orderBy('created_at', 'asc')
|
||||||
.map(mapRow);
|
.map(mapRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used to poll for new metrics
|
// Used to poll for new metrics
|
||||||
getNewMetrics (lastKnownId) {
|
getNewMetrics(lastKnownId) {
|
||||||
return this.db
|
return this.db
|
||||||
.select(METRICS_COLUMNS)
|
.select(METRICS_COLUMNS)
|
||||||
.from(TABLE)
|
.from(TABLE)
|
||||||
@ -53,6 +56,6 @@ class ClientMetricsDb {
|
|||||||
.orderBy('created_at', 'asc')
|
.orderBy('created_at', 'asc')
|
||||||
.map(mapRow);
|
.map(mapRow);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
module.exports = ClientMetricsDb;
|
module.exports = ClientMetricsDb;
|
||||||
|
@ -7,31 +7,32 @@ const { EventEmitter } = require('events');
|
|||||||
const TEN_SECONDS = 10 * 1000;
|
const TEN_SECONDS = 10 * 1000;
|
||||||
|
|
||||||
class ClientMetricsStore extends EventEmitter {
|
class ClientMetricsStore extends EventEmitter {
|
||||||
|
constructor(metricsDb, pollInterval = TEN_SECONDS) {
|
||||||
constructor (metricsDb, pollInterval = TEN_SECONDS) {
|
|
||||||
super();
|
super();
|
||||||
this.metricsDb = metricsDb;
|
this.metricsDb = metricsDb;
|
||||||
this.highestIdSeen = 0;
|
this.highestIdSeen = 0;
|
||||||
|
|
||||||
// Build internal state
|
// Build internal state
|
||||||
metricsDb.getMetricsLastHour()
|
metricsDb
|
||||||
.then((metrics) => this._emitMetrics(metrics))
|
.getMetricsLastHour()
|
||||||
|
.then(metrics => this._emitMetrics(metrics))
|
||||||
.then(() => this._startPoller(pollInterval))
|
.then(() => this._startPoller(pollInterval))
|
||||||
.then(() => this.emit('ready'))
|
.then(() => this.emit('ready'))
|
||||||
.catch((err) => logger.error(err));
|
.catch(err => logger.error(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
_startPoller (pollInterval) {
|
_startPoller(pollInterval) {
|
||||||
this.timer = setInterval(() => this._fetchNewAndEmit(), pollInterval);
|
this.timer = setInterval(() => this._fetchNewAndEmit(), pollInterval);
|
||||||
this.timer.unref();
|
this.timer.unref();
|
||||||
}
|
}
|
||||||
|
|
||||||
_fetchNewAndEmit () {
|
_fetchNewAndEmit() {
|
||||||
this.metricsDb.getNewMetrics(this.highestIdSeen)
|
this.metricsDb
|
||||||
.then((metrics) => this._emitMetrics(metrics));
|
.getNewMetrics(this.highestIdSeen)
|
||||||
|
.then(metrics => this._emitMetrics(metrics));
|
||||||
}
|
}
|
||||||
|
|
||||||
_emitMetrics (metrics) {
|
_emitMetrics(metrics) {
|
||||||
if (metrics && metrics.length > 0) {
|
if (metrics && metrics.length > 0) {
|
||||||
this.highestIdSeen = metrics[metrics.length - 1].id;
|
this.highestIdSeen = metrics[metrics.length - 1].id;
|
||||||
metrics.forEach(m => this.emit('metrics', m.metrics));
|
metrics.forEach(m => this.emit('metrics', m.metrics));
|
||||||
@ -39,15 +40,15 @@ class ClientMetricsStore extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Insert new client metrics
|
// Insert new client metrics
|
||||||
insert (metrics) {
|
insert(metrics) {
|
||||||
return this.metricsDb.insert(metrics);
|
return this.metricsDb.insert(metrics);
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy () {
|
destroy() {
|
||||||
try {
|
try {
|
||||||
clearInterval(this.timer);
|
clearInterval(this.timer);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
module.exports = ClientMetricsStore;
|
module.exports = ClientMetricsStore;
|
||||||
|
@ -4,33 +4,31 @@ const { test } = require('ava');
|
|||||||
const ClientMetricStore = require('./client-metrics-store');
|
const ClientMetricStore = require('./client-metrics-store');
|
||||||
const sinon = require('sinon');
|
const sinon = require('sinon');
|
||||||
|
|
||||||
function getMockDb () {
|
function getMockDb() {
|
||||||
const list = [
|
const list = [
|
||||||
{ id: 4, metrics: { appName: 'test' } },
|
{ id: 4, metrics: { appName: 'test' } },
|
||||||
{ id: 3, metrics: { appName: 'test' } },
|
{ id: 3, metrics: { appName: 'test' } },
|
||||||
{ id: 2, metrics: { appName: 'test' } },
|
{ id: 2, metrics: { appName: 'test' } },
|
||||||
];
|
];
|
||||||
return {
|
return {
|
||||||
getMetricsLastHour () {
|
getMetricsLastHour() {
|
||||||
return Promise.resolve([{ id: 1, metrics: { appName: 'test' } }]);
|
return Promise.resolve([{ id: 1, metrics: { appName: 'test' } }]);
|
||||||
},
|
},
|
||||||
|
|
||||||
getNewMetrics () {
|
getNewMetrics() {
|
||||||
return Promise.resolve([list.pop() || { id: 0 }]);
|
return Promise.resolve([list.pop() || { id: 0 }]);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test.cb('should call database on startup', t => {
|
||||||
test.cb('should call database on startup', (t) => {
|
|
||||||
const mock = getMockDb();
|
const mock = getMockDb();
|
||||||
|
|
||||||
const store = new ClientMetricStore(mock);
|
const store = new ClientMetricStore(mock);
|
||||||
|
|
||||||
t.plan(2);
|
t.plan(2);
|
||||||
|
|
||||||
|
store.on('metrics', metrics => {
|
||||||
store.on('metrics', (metrics) => {
|
|
||||||
t.true(store.highestIdSeen === 1);
|
t.true(store.highestIdSeen === 1);
|
||||||
t.true(metrics.appName === 'test');
|
t.true(metrics.appName === 'test');
|
||||||
store.destroy();
|
store.destroy();
|
||||||
@ -39,15 +37,14 @@ test.cb('should call database on startup', (t) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.cb('should poll for updates', t => {
|
||||||
test.cb('should poll for updates', (t) => {
|
|
||||||
const clock = sinon.useFakeTimers();
|
const clock = sinon.useFakeTimers();
|
||||||
|
|
||||||
const mock = getMockDb();
|
const mock = getMockDb();
|
||||||
const store = new ClientMetricStore(mock, 100);
|
const store = new ClientMetricStore(mock, 100);
|
||||||
|
|
||||||
const metrics = [];
|
const metrics = [];
|
||||||
store.on('metrics', (m) => metrics.push(m));
|
store.on('metrics', m => metrics.push(m));
|
||||||
|
|
||||||
t.true(metrics.length === 0);
|
t.true(metrics.length === 0);
|
||||||
|
|
||||||
@ -63,4 +60,3 @@ test.cb('should poll for updates', (t) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,7 +2,12 @@
|
|||||||
|
|
||||||
const knex = require('knex');
|
const knex = require('knex');
|
||||||
|
|
||||||
module.exports.createDb = function ({ databaseUrl, poolMin = 2, poolMax = 20, databaseSchema = 'public' }) {
|
module.exports.createDb = function({
|
||||||
|
databaseUrl,
|
||||||
|
poolMin = 2,
|
||||||
|
poolMax = 20,
|
||||||
|
databaseSchema = 'public',
|
||||||
|
}) {
|
||||||
const db = knex({
|
const db = knex({
|
||||||
client: 'pg',
|
client: 'pg',
|
||||||
connection: databaseUrl,
|
connection: databaseUrl,
|
||||||
|
@ -5,22 +5,22 @@ const { EventEmitter } = require('events');
|
|||||||
const EVENT_COLUMNS = ['id', 'type', 'created_by', 'created_at', 'data'];
|
const EVENT_COLUMNS = ['id', 'type', 'created_by', 'created_at', 'data'];
|
||||||
|
|
||||||
class EventStore extends EventEmitter {
|
class EventStore extends EventEmitter {
|
||||||
|
constructor(db) {
|
||||||
constructor (db) {
|
|
||||||
super();
|
super();
|
||||||
this.db = db;
|
this.db = db;
|
||||||
}
|
}
|
||||||
|
|
||||||
store (event) {
|
store(event) {
|
||||||
return this.db('events').insert({
|
return this.db('events')
|
||||||
type: event.type,
|
.insert({
|
||||||
|
type: event.type,
|
||||||
created_by: event.createdBy, // eslint-disable-line
|
created_by: event.createdBy, // eslint-disable-line
|
||||||
data: event.data,
|
data: event.data,
|
||||||
})
|
})
|
||||||
.then(() => this.emit(event.type, event));
|
.then(() => this.emit(event.type, event));
|
||||||
}
|
}
|
||||||
|
|
||||||
getEvents () {
|
getEvents() {
|
||||||
return this.db
|
return this.db
|
||||||
.select(EVENT_COLUMNS)
|
.select(EVENT_COLUMNS)
|
||||||
.from('events')
|
.from('events')
|
||||||
@ -29,17 +29,17 @@ class EventStore extends EventEmitter {
|
|||||||
.map(this.rowToEvent);
|
.map(this.rowToEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
getEventsFilterByName (name) {
|
getEventsFilterByName(name) {
|
||||||
return this.db
|
return this.db
|
||||||
.select(EVENT_COLUMNS)
|
.select(EVENT_COLUMNS)
|
||||||
.from('events')
|
.from('events')
|
||||||
.limit(100)
|
.limit(100)
|
||||||
.whereRaw('data ->> \'name\' = ?', [name])
|
.whereRaw("data ->> 'name' = ?", [name])
|
||||||
.orderBy('created_at', 'desc')
|
.orderBy('created_at', 'desc')
|
||||||
.map(this.rowToEvent);
|
.map(this.rowToEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
rowToEvent (row) {
|
rowToEvent(row) {
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
type: row.type,
|
type: row.type,
|
||||||
@ -48,7 +48,6 @@ class EventStore extends EventEmitter {
|
|||||||
data: row.data,
|
data: row.data,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
module.exports = EventStore;
|
module.exports = EventStore;
|
||||||
|
|
||||||
|
@ -1,21 +1,40 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { FEATURE_CREATED, FEATURE_UPDATED, FEATURE_ARCHIVED, FEATURE_REVIVED } = require('../event-type');
|
const {
|
||||||
|
FEATURE_CREATED,
|
||||||
|
FEATURE_UPDATED,
|
||||||
|
FEATURE_ARCHIVED,
|
||||||
|
FEATURE_REVIVED,
|
||||||
|
} = require('../event-type');
|
||||||
const logger = require('../logger');
|
const logger = require('../logger');
|
||||||
const NotFoundError = require('../error/notfound-error');
|
const NotFoundError = require('../error/notfound-error');
|
||||||
const FEATURE_COLUMNS = ['name', 'description', 'enabled', 'strategies', 'created_at'];
|
const FEATURE_COLUMNS = [
|
||||||
|
'name',
|
||||||
|
'description',
|
||||||
|
'enabled',
|
||||||
|
'strategies',
|
||||||
|
'created_at',
|
||||||
|
];
|
||||||
const TABLE = 'features';
|
const TABLE = 'features';
|
||||||
|
|
||||||
class FeatureToggleStore {
|
class FeatureToggleStore {
|
||||||
constructor (db, eventStore) {
|
constructor(db, eventStore) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
eventStore.on(FEATURE_CREATED, event => this._createFeature(event.data));
|
eventStore.on(FEATURE_CREATED, event =>
|
||||||
eventStore.on(FEATURE_UPDATED, event => this._updateFeature(event.data));
|
this._createFeature(event.data)
|
||||||
eventStore.on(FEATURE_ARCHIVED, event => this._archiveFeature(event.data));
|
);
|
||||||
eventStore.on(FEATURE_REVIVED, event => this._reviveFeature(event.data));
|
eventStore.on(FEATURE_UPDATED, event =>
|
||||||
|
this._updateFeature(event.data)
|
||||||
|
);
|
||||||
|
eventStore.on(FEATURE_ARCHIVED, event =>
|
||||||
|
this._archiveFeature(event.data)
|
||||||
|
);
|
||||||
|
eventStore.on(FEATURE_REVIVED, event =>
|
||||||
|
this._reviveFeature(event.data)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getFeatures () {
|
getFeatures() {
|
||||||
return this.db
|
return this.db
|
||||||
.select(FEATURE_COLUMNS)
|
.select(FEATURE_COLUMNS)
|
||||||
.from(TABLE)
|
.from(TABLE)
|
||||||
@ -24,7 +43,7 @@ class FeatureToggleStore {
|
|||||||
.map(this.rowToFeature);
|
.map(this.rowToFeature);
|
||||||
}
|
}
|
||||||
|
|
||||||
getFeature (name) {
|
getFeature(name) {
|
||||||
return this.db
|
return this.db
|
||||||
.first(FEATURE_COLUMNS)
|
.first(FEATURE_COLUMNS)
|
||||||
.from(TABLE)
|
.from(TABLE)
|
||||||
@ -32,7 +51,7 @@ class FeatureToggleStore {
|
|||||||
.then(this.rowToFeature);
|
.then(this.rowToFeature);
|
||||||
}
|
}
|
||||||
|
|
||||||
getArchivedFeatures () {
|
getArchivedFeatures() {
|
||||||
return this.db
|
return this.db
|
||||||
.select(FEATURE_COLUMNS)
|
.select(FEATURE_COLUMNS)
|
||||||
.from(TABLE)
|
.from(TABLE)
|
||||||
@ -41,7 +60,7 @@ class FeatureToggleStore {
|
|||||||
.map(this.rowToFeature);
|
.map(this.rowToFeature);
|
||||||
}
|
}
|
||||||
|
|
||||||
rowToFeature (row) {
|
rowToFeature(row) {
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new NotFoundError('No feature toggle found');
|
throw new NotFoundError('No feature toggle found');
|
||||||
}
|
}
|
||||||
@ -54,7 +73,7 @@ class FeatureToggleStore {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
eventDataToRow (data) {
|
eventDataToRow(data) {
|
||||||
return {
|
return {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
@ -64,32 +83,40 @@ class FeatureToggleStore {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_createFeature (data) {
|
_createFeature(data) {
|
||||||
return this.db(TABLE)
|
return this.db(TABLE)
|
||||||
.insert(this.eventDataToRow(data))
|
.insert(this.eventDataToRow(data))
|
||||||
.catch(err => logger.error('Could not insert feature, error was: ', err));
|
.catch(err =>
|
||||||
|
logger.error('Could not insert feature, error was: ', err)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateFeature (data) {
|
_updateFeature(data) {
|
||||||
return this.db(TABLE)
|
return this.db(TABLE)
|
||||||
.where({ name: data.name })
|
.where({ name: data.name })
|
||||||
.update(this.eventDataToRow(data))
|
.update(this.eventDataToRow(data))
|
||||||
.catch(err => logger.error('Could not update feature, error was: ', err));
|
.catch(err =>
|
||||||
|
logger.error('Could not update feature, error was: ', err)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_archiveFeature ({ name }) {
|
_archiveFeature({ name }) {
|
||||||
return this.db(TABLE)
|
return this.db(TABLE)
|
||||||
.where({ name })
|
.where({ name })
|
||||||
.update({ archived: 1 })
|
.update({ archived: 1 })
|
||||||
.catch(err => logger.error('Could not archive feature, error was: ', err));
|
.catch(err =>
|
||||||
|
logger.error('Could not archive feature, error was: ', err)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_reviveFeature ({ name }) {
|
_reviveFeature({ name }) {
|
||||||
return this.db(TABLE)
|
return this.db(TABLE)
|
||||||
.where({ name })
|
.where({ name })
|
||||||
.update({ archived: 0, enabled: 0 })
|
.update({ archived: 0, enabled: 0 })
|
||||||
.catch(err => logger.error('Could not archive feature, error was: ', err));
|
.catch(err =>
|
||||||
|
logger.error('Could not archive feature, error was: ', err)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
module.exports = FeatureToggleStore;
|
module.exports = FeatureToggleStore;
|
||||||
|
@ -9,7 +9,7 @@ const ClientMetricsDb = require('./client-metrics-db');
|
|||||||
const ClientMetricsStore = require('./client-metrics-store');
|
const ClientMetricsStore = require('./client-metrics-store');
|
||||||
const ClientApplicationsStore = require('./client-applications-store');
|
const ClientApplicationsStore = require('./client-applications-store');
|
||||||
|
|
||||||
module.exports.createStores = (config) => {
|
module.exports.createStores = config => {
|
||||||
const db = createDb(config);
|
const db = createDb(config);
|
||||||
const eventStore = new EventStore(db);
|
const eventStore = new EventStore(db);
|
||||||
const clientMetricsDb = new ClientMetricsDb(db);
|
const clientMetricsDb = new ClientMetricsDb(db);
|
||||||
|
@ -1,27 +1,32 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { STRATEGY_CREATED, STRATEGY_DELETED, STRATEGY_UPDATED } = require('../event-type');
|
const {
|
||||||
|
STRATEGY_CREATED,
|
||||||
|
STRATEGY_DELETED,
|
||||||
|
STRATEGY_UPDATED,
|
||||||
|
} = require('../event-type');
|
||||||
const logger = require('../logger');
|
const logger = require('../logger');
|
||||||
const NotFoundError = require('../error/notfound-error');
|
const NotFoundError = require('../error/notfound-error');
|
||||||
const STRATEGY_COLUMNS = ['name', 'description', 'parameters'];
|
const STRATEGY_COLUMNS = ['name', 'description', 'parameters'];
|
||||||
const TABLE = 'strategies';
|
const TABLE = 'strategies';
|
||||||
|
|
||||||
class StrategyStore {
|
class StrategyStore {
|
||||||
constructor (db, eventStore) {
|
constructor(db, eventStore) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
eventStore.on(STRATEGY_CREATED, event => this._createStrategy(event.data));
|
eventStore.on(STRATEGY_CREATED, event =>
|
||||||
eventStore.on(STRATEGY_UPDATED, event => this._updateStrategy(event.data));
|
this._createStrategy(event.data)
|
||||||
|
);
|
||||||
|
eventStore.on(STRATEGY_UPDATED, event =>
|
||||||
|
this._updateStrategy(event.data)
|
||||||
|
);
|
||||||
eventStore.on(STRATEGY_DELETED, event => {
|
eventStore.on(STRATEGY_DELETED, event => {
|
||||||
db(TABLE)
|
db(TABLE).where('name', event.data.name).del().catch(err => {
|
||||||
.where('name', event.data.name)
|
logger.error('Could not delete strategy, error was: ', err);
|
||||||
.del()
|
});
|
||||||
.catch(err => {
|
|
||||||
logger.error('Could not delete strategy, error was: ', err);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getStrategies () {
|
getStrategies() {
|
||||||
return this.db
|
return this.db
|
||||||
.select(STRATEGY_COLUMNS)
|
.select(STRATEGY_COLUMNS)
|
||||||
.from(TABLE)
|
.from(TABLE)
|
||||||
@ -29,7 +34,7 @@ class StrategyStore {
|
|||||||
.map(this.rowToStrategy);
|
.map(this.rowToStrategy);
|
||||||
}
|
}
|
||||||
|
|
||||||
getStrategy (name) {
|
getStrategy(name) {
|
||||||
return this.db
|
return this.db
|
||||||
.first(STRATEGY_COLUMNS)
|
.first(STRATEGY_COLUMNS)
|
||||||
.from(TABLE)
|
.from(TABLE)
|
||||||
@ -37,7 +42,7 @@ class StrategyStore {
|
|||||||
.then(this.rowToStrategy);
|
.then(this.rowToStrategy);
|
||||||
}
|
}
|
||||||
|
|
||||||
rowToStrategy (row) {
|
rowToStrategy(row) {
|
||||||
if (!row) {
|
if (!row) {
|
||||||
throw new NotFoundError('No strategy found');
|
throw new NotFoundError('No strategy found');
|
||||||
}
|
}
|
||||||
@ -49,7 +54,7 @@ class StrategyStore {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
eventDataToRow (data) {
|
eventDataToRow(data) {
|
||||||
return {
|
return {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
@ -57,19 +62,22 @@ class StrategyStore {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_createStrategy (data) {
|
_createStrategy(data) {
|
||||||
this.db(TABLE)
|
this.db(TABLE)
|
||||||
.insert(this.eventDataToRow(data))
|
.insert(this.eventDataToRow(data))
|
||||||
.catch(err => logger.error('Could not insert strategy, error was: ', err));
|
.catch(err =>
|
||||||
|
logger.error('Could not insert strategy, error was: ', err)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateStrategy (data) {
|
_updateStrategy(data) {
|
||||||
this.db(TABLE)
|
this.db(TABLE)
|
||||||
.where({ name: data.name })
|
.where({ name: data.name })
|
||||||
.update(this.eventDataToRow(data))
|
.update(this.eventDataToRow(data))
|
||||||
.catch(err => logger.error('Could not update strategy, error was: ', err));
|
.catch(err =>
|
||||||
|
logger.error('Could not update strategy, error was: ', err)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
module.exports = StrategyStore;
|
module.exports = StrategyStore;
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
class NameExistsError extends Error {
|
class NameExistsError extends Error {
|
||||||
constructor (message) {
|
constructor(message) {
|
||||||
super();
|
super();
|
||||||
Error.captureStackTrace(this, this.constructor);
|
Error.captureStackTrace(this, this.constructor);
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
class NotFoundError extends Error {
|
class NotFoundError extends Error {
|
||||||
constructor (message) {
|
constructor(message) {
|
||||||
super();
|
super();
|
||||||
Error.captureStackTrace(this, this.constructor);
|
Error.captureStackTrace(this, this.constructor);
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
const ValidationError = require('./validation-error');
|
const ValidationError = require('./validation-error');
|
||||||
|
|
||||||
function validateRequest (req) {
|
function validateRequest(req) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (req.validationErrors()) {
|
if (req.validationErrors()) {
|
||||||
reject(new ValidationError('Invalid syntax'));
|
reject(new ValidationError('Invalid syntax'));
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
class ValidationError extends Error {
|
class ValidationError extends Error {
|
||||||
constructor (message) {
|
constructor(message) {
|
||||||
super();
|
super();
|
||||||
Error.captureStackTrace(this, this.constructor);
|
Error.captureStackTrace(this, this.constructor);
|
||||||
|
|
||||||
|
@ -11,11 +11,7 @@ const {
|
|||||||
} = require('./event-type');
|
} = require('./event-type');
|
||||||
const diff = require('deep-diff').diff;
|
const diff = require('deep-diff').diff;
|
||||||
|
|
||||||
const strategyTypes = [
|
const strategyTypes = [STRATEGY_CREATED, STRATEGY_DELETED, STRATEGY_UPDATED];
|
||||||
STRATEGY_CREATED,
|
|
||||||
STRATEGY_DELETED,
|
|
||||||
STRATEGY_UPDATED,
|
|
||||||
];
|
|
||||||
|
|
||||||
const featureTypes = [
|
const featureTypes = [
|
||||||
FEATURE_CREATED,
|
FEATURE_CREATED,
|
||||||
@ -24,7 +20,7 @@ const featureTypes = [
|
|||||||
FEATURE_REVIVED,
|
FEATURE_REVIVED,
|
||||||
];
|
];
|
||||||
|
|
||||||
function baseTypeFor (event) {
|
function baseTypeFor(event) {
|
||||||
if (featureTypes.indexOf(event.type) !== -1) {
|
if (featureTypes.indexOf(event.type) !== -1) {
|
||||||
return 'features';
|
return 'features';
|
||||||
} else if (strategyTypes.indexOf(event.type) !== -1) {
|
} else if (strategyTypes.indexOf(event.type) !== -1) {
|
||||||
@ -33,14 +29,15 @@ function baseTypeFor (event) {
|
|||||||
throw new Error(`unknown event type: ${JSON.stringify(event)}`);
|
throw new Error(`unknown event type: ${JSON.stringify(event)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupByBaseTypeAndName (events) {
|
function groupByBaseTypeAndName(events) {
|
||||||
const groups = {};
|
const groups = {};
|
||||||
|
|
||||||
events.forEach(event => {
|
events.forEach(event => {
|
||||||
const baseType = baseTypeFor(event);
|
const baseType = baseTypeFor(event);
|
||||||
|
|
||||||
groups[baseType] = groups[baseType] || {};
|
groups[baseType] = groups[baseType] || {};
|
||||||
groups[baseType][event.data.name] = groups[baseType][event.data.name] || [];
|
groups[baseType][event.data.name] =
|
||||||
|
groups[baseType][event.data.name] || [];
|
||||||
|
|
||||||
groups[baseType][event.data.name].push(event);
|
groups[baseType][event.data.name].push(event);
|
||||||
});
|
});
|
||||||
@ -48,7 +45,7 @@ function groupByBaseTypeAndName (events) {
|
|||||||
return groups;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
function eachConsecutiveEvent (events, callback) {
|
function eachConsecutiveEvent(events, callback) {
|
||||||
const groups = groupByBaseTypeAndName(events);
|
const groups = groupByBaseTypeAndName(events);
|
||||||
|
|
||||||
Object.keys(groups).forEach(baseType => {
|
Object.keys(groups).forEach(baseType => {
|
||||||
@ -70,7 +67,7 @@ function eachConsecutiveEvent (events, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDiffs (events) {
|
function addDiffs(events) {
|
||||||
eachConsecutiveEvent(events, (left, right) => {
|
eachConsecutiveEvent(events, (left, right) => {
|
||||||
if (right) {
|
if (right) {
|
||||||
left.diffs = diff(right.data, left.data);
|
left.diffs = diff(right.data, left.data);
|
||||||
@ -81,7 +78,6 @@ function addDiffs (events) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
addDiffs,
|
addDiffs,
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const test = require('ava');
|
const { test } = require('ava');
|
||||||
const eventDiffer = require('./event-differ');
|
const eventDiffer = require('./event-differ');
|
||||||
const { FEATURE_CREATED, FEATURE_UPDATED } = require('./event-type');
|
const { FEATURE_CREATED, FEATURE_UPDATED } = require('./event-type');
|
||||||
const logger = require('./logger');
|
const logger = require('./logger');
|
||||||
@ -27,11 +27,23 @@ test('diffs a feature-update event', t => {
|
|||||||
const events = [
|
const events = [
|
||||||
{
|
{
|
||||||
type: FEATURE_UPDATED,
|
type: FEATURE_UPDATED,
|
||||||
data: { name: feature, description: desc, strategy: 'default', enabled: true, parameters: { value: 2 } },
|
data: {
|
||||||
|
name: feature,
|
||||||
|
description: desc,
|
||||||
|
strategy: 'default',
|
||||||
|
enabled: true,
|
||||||
|
parameters: { value: 2 },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FEATURE_CREATED,
|
type: FEATURE_CREATED,
|
||||||
data: { name: feature, description: desc, strategy: 'default', enabled: false, parameters: { value: 1 } },
|
data: {
|
||||||
|
name: feature,
|
||||||
|
description: desc,
|
||||||
|
strategy: 'default',
|
||||||
|
enabled: false,
|
||||||
|
parameters: { value: 1 },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -57,19 +69,43 @@ test('diffs only against features with the same name', t => {
|
|||||||
const events = [
|
const events = [
|
||||||
{
|
{
|
||||||
type: FEATURE_UPDATED,
|
type: FEATURE_UPDATED,
|
||||||
data: { name: 'bar', description: 'desc', strategy: 'default', enabled: true, parameters: {} },
|
data: {
|
||||||
|
name: 'bar',
|
||||||
|
description: 'desc',
|
||||||
|
strategy: 'default',
|
||||||
|
enabled: true,
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FEATURE_UPDATED,
|
type: FEATURE_UPDATED,
|
||||||
data: { name: 'foo', description: 'desc', strategy: 'default', enabled: false, parameters: {} },
|
data: {
|
||||||
|
name: 'foo',
|
||||||
|
description: 'desc',
|
||||||
|
strategy: 'default',
|
||||||
|
enabled: false,
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FEATURE_CREATED,
|
type: FEATURE_CREATED,
|
||||||
data: { name: 'bar', description: 'desc', strategy: 'default', enabled: false, parameters: {} },
|
data: {
|
||||||
|
name: 'bar',
|
||||||
|
description: 'desc',
|
||||||
|
strategy: 'default',
|
||||||
|
enabled: false,
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FEATURE_CREATED,
|
type: FEATURE_CREATED,
|
||||||
data: { name: 'foo', description: 'desc', strategy: 'default', enabled: true, parameters: {} },
|
data: {
|
||||||
|
name: 'foo',
|
||||||
|
description: 'desc',
|
||||||
|
strategy: 'default',
|
||||||
|
enabled: true,
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -85,11 +121,23 @@ test('sets an empty array of diffs if nothing was changed', t => {
|
|||||||
const events = [
|
const events = [
|
||||||
{
|
{
|
||||||
type: FEATURE_UPDATED,
|
type: FEATURE_UPDATED,
|
||||||
data: { name: 'foo', description: 'desc', strategy: 'default', enabled: true, parameters: {} },
|
data: {
|
||||||
|
name: 'foo',
|
||||||
|
description: 'desc',
|
||||||
|
strategy: 'default',
|
||||||
|
enabled: true,
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FEATURE_CREATED,
|
type: FEATURE_CREATED,
|
||||||
data: { name: 'foo', description: 'desc', strategy: 'default', enabled: true, parameters: {} },
|
data: {
|
||||||
|
name: 'foo',
|
||||||
|
description: 'desc',
|
||||||
|
strategy: 'default',
|
||||||
|
enabled: true,
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -101,11 +149,16 @@ test('sets diffs to null if there was nothing to diff against', t => {
|
|||||||
const events = [
|
const events = [
|
||||||
{
|
{
|
||||||
type: FEATURE_UPDATED,
|
type: FEATURE_UPDATED,
|
||||||
data: { name: 'foo', description: 'desc', strategy: 'default', enabled: true, parameters: {} },
|
data: {
|
||||||
|
name: 'foo',
|
||||||
|
description: 'desc',
|
||||||
|
strategy: 'default',
|
||||||
|
enabled: true,
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
eventDiffer.addDiffs(events);
|
eventDiffer.addDiffs(events);
|
||||||
t.true(events[0].diffs === null);
|
t.true(events[0].diffs === null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function extractUsername (req) {
|
function extractUsername(req) {
|
||||||
return req.cookies.username || 'unknown';
|
return req.cookies.username || 'unknown';
|
||||||
}
|
}
|
||||||
module.exports = extractUsername;
|
module.exports = extractUsername;
|
||||||
|
@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
const log4js = require('log4js');
|
const log4js = require('log4js');
|
||||||
log4js.configure({
|
log4js.configure({
|
||||||
appenders: [
|
appenders: [{ type: 'console' }],
|
||||||
{ type: 'console' },
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const logger = log4js.getLogger('unleash');
|
const logger = log4js.getLogger('unleash');
|
||||||
|
@ -9,9 +9,14 @@ exports.startMonitoring = (enable, eventBus) => {
|
|||||||
|
|
||||||
const client = require('prom-client');
|
const client = require('prom-client');
|
||||||
|
|
||||||
const requestDuration = new client.Summary('http_request_duration_milliseconds', 'App response time', ['path', 'method', 'status'], {
|
const requestDuration = new client.Summary(
|
||||||
percentiles: [0.1, 0.5, 0.9, 0.99],
|
'http_request_duration_milliseconds',
|
||||||
});
|
'App response time',
|
||||||
|
['path', 'method', 'status'],
|
||||||
|
{
|
||||||
|
percentiles: [0.1, 0.5, 0.9, 0.99],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
eventBus.on(events.REQUEST_TIME, ({ path, method, time, statusCode }) => {
|
eventBus.on(events.REQUEST_TIME, ({ path, method, time, statusCode }) => {
|
||||||
requestDuration.labels(path, method, statusCode).observe(time);
|
requestDuration.labels(path, method, statusCode).observe(time);
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const test = require('ava');
|
const { test } = require('ava');
|
||||||
const { EventEmitter } = require('events');
|
const { EventEmitter } = require('events');
|
||||||
const eventBus = new EventEmitter();
|
const eventBus = new EventEmitter();
|
||||||
const { REQUEST_TIME } = require('./events');
|
const { REQUEST_TIME } = require('./events');
|
||||||
@ -9,8 +9,16 @@ const prometheusRegister = require('prom-client/lib/register');
|
|||||||
|
|
||||||
test('should collect metrics for requests', t => {
|
test('should collect metrics for requests', t => {
|
||||||
startMonitoring(true, eventBus);
|
startMonitoring(true, eventBus);
|
||||||
eventBus.emit(REQUEST_TIME, { path: 'somePath', method: 'GET', statusCode: 200, time: 1337 });
|
eventBus.emit(REQUEST_TIME, {
|
||||||
|
path: 'somePath',
|
||||||
|
method: 'GET',
|
||||||
|
statusCode: 200,
|
||||||
|
time: 1337,
|
||||||
|
});
|
||||||
|
|
||||||
const metrics = prometheusRegister.metrics();
|
const metrics = prometheusRegister.metrics();
|
||||||
t.regex(metrics, /http_request_duration_milliseconds{quantile="0.99",status="200",method="GET",path="somePath"} 1337/);
|
t.regex(
|
||||||
|
metrics,
|
||||||
|
/http_request_duration_milliseconds{quantile="0.99",status="200",method="GET",path="somePath"} 1337/
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
@ -14,16 +14,19 @@ const DEFAULT_OPTIONS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createOptions: (opts) => {
|
createOptions: opts => {
|
||||||
const options = Object.assign({}, DEFAULT_OPTIONS, opts);
|
const options = Object.assign({}, DEFAULT_OPTIONS, opts);
|
||||||
|
|
||||||
// If we are running in development we should assume local db
|
// If we are running in development we should assume local db
|
||||||
if (isDev() && !options.databaseUrl) {
|
if (isDev() && !options.databaseUrl) {
|
||||||
options.databaseUrl = 'postgres://unleash_user:passord@localhost:5432/unleash';
|
options.databaseUrl =
|
||||||
|
'postgres://unleash_user:passord@localhost:5432/unleash';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.databaseUrl) {
|
if (!options.databaseUrl) {
|
||||||
throw new Error('You must either pass databaseUrl option or set environemnt variable DATABASE_URL');
|
throw new Error(
|
||||||
|
'You must either pass databaseUrl option or set environemnt variable DATABASE_URL'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return options;
|
return options;
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const test = require('ava');
|
const { test } = require('ava');
|
||||||
|
|
||||||
delete process.env.DATABASE_URL;
|
delete process.env.DATABASE_URL;
|
||||||
|
|
||||||
@ -18,7 +18,10 @@ test('should set default databaseUrl for develpment', t => {
|
|||||||
|
|
||||||
const options = createOptions({});
|
const options = createOptions({});
|
||||||
|
|
||||||
t.true(options.databaseUrl === 'postgres://unleash_user:passord@localhost:5432/unleash');
|
t.true(
|
||||||
|
options.databaseUrl ===
|
||||||
|
'postgres://unleash_user:passord@localhost:5432/unleash'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not override provided options', t => {
|
test('should not override provided options', t => {
|
||||||
|
@ -10,15 +10,18 @@ const { startMonitoring } = require('./metrics');
|
|||||||
const { createStores } = require('./db');
|
const { createStores } = require('./db');
|
||||||
const { createOptions } = require('./options');
|
const { createOptions } = require('./options');
|
||||||
|
|
||||||
function createApp (options) {
|
function createApp(options) {
|
||||||
// Database dependecies (statefull)
|
// Database dependecies (statefull)
|
||||||
const stores = createStores(options);
|
const stores = createStores(options);
|
||||||
const eventBus = new EventEmitter();
|
const eventBus = new EventEmitter();
|
||||||
|
|
||||||
const config = Object.assign({
|
const config = Object.assign(
|
||||||
stores,
|
{
|
||||||
eventBus,
|
stores,
|
||||||
}, options);
|
eventBus,
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
const app = getApp(config);
|
const app = getApp(config);
|
||||||
const server = app.listen(app.get('port'), () => {
|
const server = app.listen(app.get('port'), () => {
|
||||||
@ -30,7 +33,7 @@ function createApp (options) {
|
|||||||
return { app, server, eventBus };
|
return { app, server, eventBus };
|
||||||
}
|
}
|
||||||
|
|
||||||
function start (opts) {
|
function start(opts) {
|
||||||
const options = createOptions(opts);
|
const options = createOptions(opts);
|
||||||
|
|
||||||
return migrator({ databaseUrl: options.databaseUrl })
|
return migrator({ databaseUrl: options.databaseUrl })
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const test = require('ava');
|
const { test } = require('ava');
|
||||||
const proxyquire = require('proxyquire');
|
const proxyquire = require('proxyquire');
|
||||||
|
|
||||||
const getApp = proxyquire('./app', {
|
const getApp = proxyquire('./app', {
|
||||||
@ -13,21 +13,21 @@ const getApp = proxyquire('./app', {
|
|||||||
const serverImpl = proxyquire('./server-impl', {
|
const serverImpl = proxyquire('./server-impl', {
|
||||||
'./app': getApp,
|
'./app': getApp,
|
||||||
'./metrics': {
|
'./metrics': {
|
||||||
startMonitoring (o) {
|
startMonitoring(o) {
|
||||||
return o;
|
return o;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'./db': {
|
'./db': {
|
||||||
createStores (o) {
|
createStores(o) {
|
||||||
return o;
|
return o;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'./options': {
|
'./options': {
|
||||||
createOptions (o) {
|
createOptions(o) {
|
||||||
return o;
|
return o;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'../migrator' () {
|
'../migrator'() {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user