1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-04 00:18:01 +01:00
This commit is contained in:
sveisvei 2016-12-04 14:09:37 +01:00
parent de55bbc9ef
commit 5bf0b36588
23 changed files with 106 additions and 94 deletions

View File

@ -12,7 +12,7 @@ const routes = require('./routes');
const path = require('path'); const path = require('path');
const errorHandler = require('errorhandler'); 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();
@ -42,7 +42,7 @@ 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(log4js.connectLogger(logger, {
format: ':status :method :url :response-timems', format: ':status :method :url :response-timems',
level: 'auto', // 3XX=WARN, 4xx/5xx=ERROR level: 'auto', // 3XX=WARN, 4xx/5xx=ERROR

View File

@ -65,7 +65,7 @@ test.cb('data should expire', (t) => {
test('should listen to metrics from store', t => { test('should listen to metrics from store', t => {
const store = new EventEmitter(); const store = new EventEmitter();
const metrics = new UnleashClientMetrics(store); const metrics = new UnleashClientMetrics(store);
store.emit('metrics', { store.emit('metrics', {
appName, appName,
instanceId, instanceId,
bucket: { bucket: {
@ -146,14 +146,13 @@ test('should build up list of seend toggles when new metrics arrives', t => {
}); });
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);
const toggleCounts = {}; const toggleCounts = {};
for (let i=0; i<100; i++) { for (let i = 0; i < 100; i++) {
toggleCounts[`toggle${i}`] = {yes: i, no: i} toggleCounts[`toggle${i}`] = { yes: i, no: i };
} }
store.emit('metrics', { store.emit('metrics', {

View File

@ -5,7 +5,6 @@ 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 = {};
@ -40,11 +39,11 @@ module.exports = class UnleashClientMetrics {
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);
const metricsCount = this.apps[appName].count; const metricsCount = this.apps[appName].count;
apps.push({appName, seenToggles, metricsCount}) apps.push({ appName, seenToggles, metricsCount });
}); });
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) : [];
} }
@ -57,12 +56,12 @@ module.exports = class UnleashClientMetrics {
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, data.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];
} }
@ -89,7 +88,9 @@ module.exports = class UnleashClientMetrics {
} }
addSeenToggles (app, toggleNames) { addSeenToggles (app, toggleNames) {
toggleNames.forEach(t => app.seenToggles[t] = true); toggleNames.forEach(t => {
app.seenToggles[t] = true;
});
} }
destroy () { destroy () {

View File

@ -36,11 +36,9 @@ function toNewFormat (feature) {
description: feature.description, description: feature.description,
enabled: feature.enabled, enabled: feature.enabled,
strategies: feature.strategies, strategies: feature.strategies,
createdAt: feature.createdAt createdAt: feature.createdAt,
} };
} }
} }
module.exports = { addOldFields, toNewFormat }; module.exports = { addOldFields, toNewFormat };

View File

@ -27,7 +27,7 @@ test('adds old fields to feature', t => {
test('adds old fields to feature handles missing strategies field', t => { test('adds old fields to feature handles missing strategies field', t => {
const feature = { const feature = {
name: 'test', name: 'test',
enabled: 0 enabled: 0,
}; };
const mappedFeature = mapper.addOldFields(feature); const mappedFeature = mapper.addOldFields(feature);

View File

@ -13,10 +13,10 @@ const mapRow = (row) => ({
createdAt: row.created_at, createdAt: row.created_at,
}); });
const mapAppsRow = (row) => ({ // const mapAppsRow = (row) => ({
appName: row.app_name, // appName: row.app_name,
createdAt: row.created_at, // createdAt: row.created_at,
}); // });
class ClientInstanceStore { class ClientInstanceStore {

View File

@ -15,7 +15,7 @@ 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();
} }

View File

@ -12,9 +12,8 @@ class ClientMetricsStore extends EventEmitter {
super(); super();
this.metricsDb = metricsDb; this.metricsDb = metricsDb;
this.highestIdSeen = 0; this.highestIdSeen = 0;
this.timer;
//Build internal state // Build internal state
metricsDb.getMetricsLastHour() metricsDb.getMetricsLastHour()
.then((metrics) => this._emitMetrics(metrics)) .then((metrics) => this._emitMetrics(metrics))
.then(() => this._startPoller(pollInterval)) .then(() => this._startPoller(pollInterval))
@ -27,9 +26,9 @@ class ClientMetricsStore extends EventEmitter {
this.timer.unref(); this.timer.unref();
} }
_fetchNewAndEmit() { _fetchNewAndEmit () {
this.metricsDb.getNewMetrics(this.highestIdSeen) this.metricsDb.getNewMetrics(this.highestIdSeen)
.then((metrics) => this._emitMetrics(metrics)) .then((metrics) => this._emitMetrics(metrics));
} }
_emitMetrics (metrics) { _emitMetrics (metrics) {
@ -41,10 +40,10 @@ 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) {}

View File

@ -5,15 +5,19 @@ const ClientMetricStore = require('./client-metrics-store');
const sinon = require('sinon'); const sinon = require('sinon');
function getMockDb () { function getMockDb () {
const list = [{ id: 4, metrics: {appName: 'test'} }, { id: 3, metrics: {appName: 'test'} }, { id: 2, metrics: {appName: 'test'} }]; const list = [
{ id: 4, metrics: { appName: 'test' } },
{ id: 3, 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 (v) { getNewMetrics () {
return Promise.resolve([list.pop() || { id: 0 }]); return Promise.resolve([list.pop() || { id: 0 }]);
} },
}; };
} }

View File

@ -1,3 +1,5 @@
'use strict';
module.exports = { module.exports = {
REQUEST_TIME: 'request_time', REQUEST_TIME: 'request_time',
} };

View File

@ -2,9 +2,9 @@
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');

View File

@ -1,3 +1,5 @@
'use strict';
const events = require('./events'); const events = require('./events');
exports.startMonitoring = (enable, eventBus) => { exports.startMonitoring = (enable, eventBus) => {
@ -11,7 +13,7 @@ exports.startMonitoring = (enable, eventBus) => {
percentiles: [0.1, 0.5, 0.9, 0.99], 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);
}); });
}; };

View File

@ -1,3 +1,4 @@
'use strict';
const test = require('ava'); const test = require('ava');
const { EventEmitter } = require('events'); const { EventEmitter } = require('events');
@ -11,6 +12,5 @@ test('should collect metrics for requests', t => {
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/);
}); });

View File

@ -1,4 +1,5 @@
'use strict'; 'use strict';
const { publicFolder } = require('unleash-frontend'); const { publicFolder } = require('unleash-frontend');
const isDev = () => process.env.NODE_ENV === 'development'; const isDev = () => process.env.NODE_ENV === 'development';
@ -9,7 +10,7 @@ const DEFAULT_OPTIONS = {
baseUriPath: process.env.BASE_URI_PATH || '', baseUriPath: process.env.BASE_URI_PATH || '',
serverMetrics: true, serverMetrics: true,
publicFolder, publicFolder,
enableRequestLogger: isDev() ? true : false enableRequestLogger: isDev(),
}; };
module.exports = { module.exports = {
@ -17,7 +18,7 @@ module.exports = {
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';
} }
@ -25,5 +26,5 @@ module.exports = {
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;
} },
} };

View File

@ -3,11 +3,12 @@
const test = require('ava'); const test = require('ava');
delete process.env.DATABASE_URL; delete process.env.DATABASE_URL;
const { createOptions } = require('./options');
test('should require DATABASE_URI', t => { test('should require DATABASE_URI', t => {
const { createOptions } = require('./options');
t.throws(() => { t.throws(() => {
const options = createOptions({}); createOptions({});
}); });
}); });
@ -25,7 +26,7 @@ test('should not override provided options', t => {
process.env.NODE_ENV = 'production'; process.env.NODE_ENV = 'production';
const { createOptions } = require('./options'); const { createOptions } = require('./options');
const options = createOptions({databaseUrl: 'test', port: 1111}); const options = createOptions({ databaseUrl: 'test', port: 1111 });
t.true(options.databaseUrl === 'test'); t.true(options.databaseUrl === 'test');
t.true(options.port === 1111); t.true(options.port === 1111);

View File

@ -3,7 +3,7 @@
const prometheusRegister = require('prom-client/lib/register'); const prometheusRegister = require('prom-client/lib/register');
module.exports = function (app, config) { module.exports = function (app, config) {
if(config.serverMetrics) { if (config.serverMetrics) {
app.get('/internal-backstage/prometheus', (req, res) => { app.get('/internal-backstage/prometheus', (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' }); res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(prometheusRegister.metrics()); res.end(prometheusRegister.metrics());

View File

@ -70,7 +70,7 @@ module.exports = function (app, config) {
validateRequest(req) validateRequest(req)
.then(validateFormat) .then(validateFormat)
.then(validateUniqueName) .then(validateUniqueName)
.then((req) => legacyFeatureMapper.toNewFormat(req.body)) .then((_req) => legacyFeatureMapper.toNewFormat(_req.body))
.then(validateStrategy) .then(validateStrategy)
.then((featureToggle) => eventStore.store({ .then((featureToggle) => eventStore.store({
type: eventType.featureCreated, type: eventType.featureCreated,

View File

@ -23,4 +23,4 @@ const clientRegisterSchema = joi.object().keys({
interval: joi.number().required(), interval: joi.number().required(),
}); });
module.exports = { clientMetricsSchema, clientRegisterSchema } module.exports = { clientMetricsSchema, clientRegisterSchema };

View File

@ -72,7 +72,7 @@ module.exports = function (app, config) {
app.get('/client/strategies', (req, res) => { app.get('/client/strategies', (req, res) => {
const appName = req.query.appName; const appName = req.query.appName;
if(appName) { if (appName) {
clientStrategyStore.getByAppName(appName) clientStrategyStore.getByAppName(appName)
.then(data => res.json(data)) .then(data => res.json(data))
.catch(err => catchLogAndSendErrorResponse(err, res)); .catch(err => catchLogAndSendErrorResponse(err, res));
@ -86,13 +86,13 @@ module.exports = function (app, config) {
app.get('/client/applications/', (req, res) => { app.get('/client/applications/', (req, res) => {
clientInstanceStore.getApplications() clientInstanceStore.getApplications()
.then(apps => { .then(apps => {
const applications = apps.map(({appName}) => ({ const applications = apps.map(({ appName }) => ({
appName: appName, appName,
links: { links: {
appDetails: `/api/client/applications/${appName}` appDetails: `/api/client/applications/${appName}`,
} },
})) }));
res.json({applications}) res.json({ applications });
}) })
.catch(err => catchLogAndSendErrorResponse(err, res)); .catch(err => catchLogAndSendErrorResponse(err, res));
}); });
@ -101,10 +101,10 @@ module.exports = function (app, config) {
const appName = req.params.appName; const appName = req.params.appName;
const seenToggles = metrics.getSeenTogglesByAppName(appName); const seenToggles = metrics.getSeenTogglesByAppName(appName);
Promise.all([ Promise.all([
clientInstanceStore.getByAppName(appName), clientInstanceStore.getByAppName(appName),
clientStrategyStore.getByAppName(appName) clientStrategyStore.getByAppName(appName),
]) ])
.then(([instances, strategies]) => res.json({appName, instances, strategies, seenToggles})) .then(([instances, strategies]) => res.json({ appName, instances, strategies, seenToggles }))
.catch(err => catchLogAndSendErrorResponse(err, res)); .catch(err => catchLogAndSendErrorResponse(err, res));
}); });
}; };

View File

@ -1,8 +1,10 @@
'use strict';
const logger = require('../logger'); const logger = require('../logger');
const catchLogAndSendErrorResponse = (err, res) => { const catchLogAndSendErrorResponse = (err, res) => {
logger.error(err); logger.error(err);
res.status(500).end(); res.status(500).end();
} };
module.exports = { catchLogAndSendErrorResponse }; module.exports = { catchLogAndSendErrorResponse };

View File

@ -5,7 +5,6 @@ const { EventEmitter } = require('events');
const logger = require('./logger'); const logger = require('./logger');
const migrator = require('../migrator'); const migrator = require('../migrator');
const getApp = require('./app'); const getApp = require('./app');
const events = require('./events');
const { startMonitoring } = require('./metrics'); const { startMonitoring } = require('./metrics');
const { createStores } = require('./db'); const { createStores } = require('./db');

View File

@ -39,6 +39,8 @@
"start:dev:pg-chain": "export DATABASE_URL=postgres://$PGUSER:$PGPASSWORD@localhost:$PGPORT/postgres ; db-migrate up && npm run start:dev", "start:dev:pg-chain": "export DATABASE_URL=postgres://$PGUSER:$PGPASSWORD@localhost:$PGPORT/postgres ; db-migrate up && npm run start:dev",
"db-migrate": "db-migrate up", "db-migrate": "db-migrate up",
"db-migrate:down": "db-migrate down", "db-migrate:down": "db-migrate down",
"lint": "eslint lib",
"pretest": "npm run lint",
"test": "PORT=4243 ava test lib/*.test.js lib/**/*.test.js", "test": "PORT=4243 ava test lib/*.test.js lib/**/*.test.js",
"test:docker": "./scripts/docker-postgres.sh", "test:docker": "./scripts/docker-postgres.sh",
"test:watch": "npm run test -- --watch", "test:watch": "npm run test -- --watch",
@ -82,6 +84,8 @@
"@types/node": "^6.0.46", "@types/node": "^6.0.46",
"ava": "^0.17.0", "ava": "^0.17.0",
"coveralls": "^2.11.15", "coveralls": "^2.11.15",
"eslint": "^3.11.1",
"eslint-config-finn": "^1.0.0-beta.1",
"nyc": "^9.0.1", "nyc": "^9.0.1",
"sinon": "^1.17.5", "sinon": "^1.17.5",
"superagent": "^2.3.0", "superagent": "^2.3.0",

View File

@ -23,7 +23,7 @@ function getSetup () {
return { return {
request: supertest(app), request: supertest(app),
stores stores,
}; };
} }
@ -66,7 +66,7 @@ test('should validate client metrics', () => {
const { request } = getSetup(); const { request } = getSetup();
return request return request
.post('/api/client/metrics') .post('/api/client/metrics')
.send({random: 'blush'}) .send({ random: 'blush' })
.expect(400); .expect(400);
}); });
@ -99,7 +99,7 @@ test('should return seen toggles even when there is nothing', t => {
test('should return list of seen-toggles per app', t => { test('should return list of seen-toggles per app', t => {
const { request, stores } = getSetup(); const { request, stores } = getSetup();
const appName = 'asd!23' const appName = 'asd!23';
stores.clientMetricsStore.emit('metrics', { stores.clientMetricsStore.emit('metrics', {
appName, appName,
instanceId: 'instanceId', instanceId: 'instanceId',
@ -107,8 +107,8 @@ test('should return list of seen-toggles per app', t => {
start: new Date(), start: new Date(),
stop: new Date(), stop: new Date(),
toggles: { toggles: {
toggleX: {yes: 123,no: 0}, toggleX: { yes: 123, no: 0 },
toggleY: {yes: 123,no: 0} toggleY: { yes: 123, no: 0 }
}, },
}, },
}); });
@ -128,12 +128,12 @@ test('should return feature-toggles metrics even when there is nothing', t => {
const { request } = getSetup(); const { request } = getSetup();
return request return request
.get('/api/client/metrics/feature-toggles') .get('/api/client/metrics/feature-toggles')
.expect(200) .expect(200);
}); });
test('should return metrics for all toggles', t => { test('should return metrics for all toggles', t => {
const { request, stores } = getSetup(); const { request, stores } = getSetup();
const appName = 'asd!23' const appName = 'asd!23';
stores.clientMetricsStore.emit('metrics', { stores.clientMetricsStore.emit('metrics', {
appName, appName,
instanceId: 'instanceId', instanceId: 'instanceId',
@ -141,8 +141,8 @@ test('should return metrics for all toggles', t => {
start: new Date(), start: new Date(),
stop: new Date(), stop: new Date(),
toggles: { toggles: {
toggleX: {yes: 123,no: 0}, toggleX: { yes: 123, no: 0 },
toggleY: {yes: 123,no: 0} toggleY: { yes: 123, no: 0 },
}, },
}, },
}); });
@ -160,7 +160,7 @@ test('should return metrics for all toggles', t => {
test('should return list of client strategies', t => { test('should return list of client strategies', t => {
const { request, stores } = getSetup(); const { request } = getSetup();
return request return request
.get('/api/client/strategies') .get('/api/client/strategies')
.expect(200) .expect(200)
@ -170,7 +170,7 @@ test('should return list of client strategies', t => {
}); });
test('should return list of client applications', t => { test('should return list of client applications', t => {
const { request, stores } = getSetup(); const { request } = getSetup();
return request return request
.get('/api/client/applications') .get('/api/client/applications')
.expect(200) .expect(200)