1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-02 01:17:58 +02:00

Merge pull request #173 from Unleash/add-app-toggle-list

Add app toggle list
This commit is contained in:
Ivar Conradi Østhus 2016-11-30 19:07:29 +01:00 committed by GitHub
commit 42f5a6c257
17 changed files with 600 additions and 376 deletions

View File

@ -292,104 +292,5 @@ Event types:
# Metrics
**GET: http://unleash.host.com/api/metrics**
Get aggregated state of metrics
```json
{
"globalCount": 1420,
"apps": {
"app-name": {
"count": 1420,
"clients": [
"instance-id"
]
}
},
"clients": {
"instance-id": {
"appName": "app-name",
"count": 1420,
"started": "2016-11-13T19:50:54.395Z",
"init": "2016-11-13T19:50:54.395Z",
"ping": "2016-11-13T19:51:14.403Z"
}
}
}
```
**GET: http://unleash.host.com/api/metrics/features**
Get metrics per toggle
```json
{
"lastHour": {
"toggle-name-1": {
"yes": 0,
"no": 720
},
"toggle-name-2": {
"yes": 0,
"no": 463
},
"toggle-name-3": {
"yes": 237,
"no": 0
}
},
"lastMinute": {
"toggle-name-1": {
"yes": 0,
"no": 0
},
"toggle-name-2": {
"yes": 0,
"no": 0
},
"toggle-name-3": {
"yes": 0,
"no": 0
}
}
}
```
**POST: http://unleash.host.com/api/client/register**
Register a client instance with the unleash server
```json
{
"appName": "appName",
"instanceId": "instanceId",
"strategies": ["default", "some-strategy-1"],
"started": "2016-11-03T07:16:43.572Z",
"interval": 10000,
}
```
**POST: http://unleash.host.com/api/client/metrics**
Register a metrics payload with a timed bucket
```json
{
"appName": "appName",
"instanceId": "instanceId",
"bucket": {
"start": "2016-11-03T07:16:43.572Z",
"stop": "2016-11-03T07:16:53.572Z",
"toggles": {
"toggle-name-1": {
"yes": 123,
"no": 321
}
}
},
}
```
[Metrics](api/metrics.md)

224
docs/api/metrics.md Normal file
View File

@ -0,0 +1,224 @@
# This document describes the client metrics endpoints
### Client registration
`POST: http://unleash.host.com/api/client/register`
Register a client instance with the unleash server. The client should send all fields specified.
```json
{
"appName": "appName",
"instanceId": "instanceId",
"strategies": ["default", "some-strategy-1"],
"started": "2016-11-03T07:16:43.572Z",
"interval": 10000,
}
```
**Fields:**
* **appName** - Name of the application seen by unleash-server
* **instanceId** - Instance id for this application (typically hostname, podId or similar)
* **strategies** - List of strategies implemented by this application
* **started** - When this client started. Should be reported as UTC time.
* **interval** - At wich interval will this client be expected to send metrics?
### Send metrics
`POST http://unleash.host.com/api/client/metrics`
Register a metrics payload with a timed bucket.
```json
{
"appName": "appName",
"instanceId": "instanceId",
"bucket": {
"start": "2016-11-03T07:16:43.572Z",
"stop": "2016-11-03T07:16:53.572Z",
"toggles": {
"toggle-name-1": {
"yes": 123,
"no": 321
},
"toggle-name-2": {
"yes": 111,
"no": 0
}
}
},
}
```
### Seen-toggles
`GET http://unleash.host.com/api/client/seen-toggles`
This enpoints returns a list of applications and what toogles
unleash has seend for each application. It will only guarantee
toggles reported by client applications within the last hour, but
will in most cases remember seen-toggles for applications longer
**Example response:**
```json
[
{
"appName": "demo-app",
"seenToggles": [
"add-feature-2",
"toggle-2",
"toggle-3"
],
"metricsCount": 127
},
{
"appName": "demo-app-2",
"seenToggles": [
"add-feature-2",
"toggle-2",
"toggle-3"
],
"metricsCount": 21
}
]
```
**Fields:**
* **appName** - Name of the application seen by unleash-server
* **seenToggles** - array of toggles names seen by unleash-server for this application
* **metricsCount** - number of metrics counted across all toggles for this application.
### Feature-Toggles metrics
`GET http://unleash.host.com/api/metrics/feature-toggles`
This endpoint gives _last minute_ and _last hour_ metrics for all active toggles. This is based on
metrics reported by client applications. Yes is the number of times a given feature toggle
was evaluated to enabled in a client applcation, and no is the number it avaluated to false.
**Example response:**
```json
{
"lastHour": {
"add-feature-2": {
"yes": 0,
"no": 527
},
"toggle-2": {
"yes": 265,
"no": 85
},
"toggle-3": {
"yes": 257,
"no": 93
}
},
"lastMinute": {
"add-feature-2": {
"yes": 0,
"no": 527
},
"toggle-2": {
"yes": 265,
"no": 85
},
"toggle-3": {
"yes": 257,
"no": 93
}
}
}
```
**Fields:**
* **lastHour** - Hour projection collected metrics for all feature toggles.
* **lastMinute** - Mintue projection collected metrics for all feature toggles.
### Applications
`GET http://unleash.host.com/api/client/applications`
This endpoint returns a list of known applications (seen the last two days) and
a link to follow for more datails.
```json
{
"applications": [
{
"appName": "test",
"links": {
"appDetails": "/api/client/applications/test"
}
},
{
"appName": "demo-app-2",
"links": {
"appDetails": "/api/client/applications/demo-app-2"
}
},
{
"appName": "demo-app",
"links": {
"appDetails": "/api/client/applications/demo-app"
}
}
]
}
```
### Application Details
`GET http://unleash.host.com/api/client/applications/:appName`
This endpoint gives insight into details about a client applcation, such as instances,
strategies implemented and seen toogles.
```json
{
"appName": "demo-app",
"instances": [
{
"instanceId": "generated-732038-17080",
"clientIp": "::ffff:127.0.0.1",
"lastSeen": "2016-11-30T17:32:04.265Z",
"createdAt": "2016-11-30T17:31:08.914Z"
},
{
"instanceId": "generated-639919-11185",
"clientIp": "::ffff:127.0.0.1",
"lastSeen": "2016-11-30T16:04:15.991Z",
"createdAt": "2016-11-30T10:49:11.223Z"
},
],
"strategies": [
{
"appName": "demo-app",
"strategies": [
"default",
"extra"
]
}
],
"seenToggles": [
"add-feature-2",
"toggle-2",
"toggle-3"
]
}
```

View File

@ -4,15 +4,17 @@ const { test } = require('ava');
const UnleashClientMetrics = require('./index');
const sinon = require('sinon');
const { EventEmitter } = require('events');
const appName = 'appName';
const instanceId = 'instanceId';
test('should work without state', (t) => {
const metrics = new UnleashClientMetrics();
const store = new EventEmitter();
const metrics = new UnleashClientMetrics(store);
t.truthy(metrics.getMetricsOverview());
t.truthy(metrics.getAppsWitToggles());
t.truthy(metrics.getTogglesMetrics());
t.truthy(metrics.toJSON());
metrics.destroy();
});
@ -20,7 +22,8 @@ test('should work without state', (t) => {
test.cb('data should expire', (t) => {
const clock = sinon.useFakeTimers();
const metrics = new UnleashClientMetrics();
const store = new EventEmitter();
const metrics = new UnleashClientMetrics(store);
metrics.addPayload({
appName,
@ -59,9 +62,10 @@ test.cb('data should expire', (t) => {
t.end();
});
test('addPayload', t => {
const metrics = new UnleashClientMetrics();
metrics.addPayload({
test('should listen to metrics from store', t => {
const store = new EventEmitter();
const metrics = new UnleashClientMetrics(store);
store.emit('metrics', {
appName,
instanceId,
bucket: {
@ -76,8 +80,6 @@ test('addPayload', t => {
},
});
t.truthy(metrics.clients[instanceId].appName === appName);
t.truthy(metrics.clients[instanceId].count === 123);
t.truthy(metrics.apps[appName].count === 123);
t.truthy(metrics.globalCount === 123);
@ -100,7 +102,6 @@ test('addPayload', t => {
},
});
t.truthy(metrics.clients[instanceId].count === 143);
t.truthy(metrics.globalCount === 143);
t.deepEqual(metrics.getTogglesMetrics().lastHour.toggleX, { yes: 133, no: 10 });
t.deepEqual(metrics.getTogglesMetrics().lastMinute.toggleX, { yes: 133, no: 10 });
@ -108,50 +109,65 @@ test('addPayload', t => {
metrics.destroy();
});
test('addBucket', t => {
const metrics = new UnleashClientMetrics();
metrics.addClient(appName, instanceId);
metrics.addBucket(appName, instanceId, {
start: new Date(),
stop: new Date(),
toggles: {
toggleX: {
yes: 123,
no: 0,
test('should build up list of seend toggles when new metrics arrives', t => {
const store = new EventEmitter();
const metrics = new UnleashClientMetrics(store);
store.emit('metrics', {
appName,
instanceId,
bucket: {
start: new Date(),
stop: new Date(),
toggles: {
toggleX: {
yes: 123,
no: 0,
},
toggleY: {
yes: 50,
no: 50,
},
},
},
});
t.truthy(metrics.clients[instanceId].count === 123);
t.truthy(metrics.globalCount === 123);
t.deepEqual(metrics.getTogglesMetrics().lastMinute.toggleX, { yes: 123, no: 0 });
const appToggles = metrics.getAppsWitToggles();
const togglesForApp = metrics.getSeenTogglesByAppName(appName);
t.truthy(appToggles.length === 1);
t.truthy(appToggles[0].seenToggles.length === 2);
t.truthy(appToggles[0].seenToggles.includes('toggleX'));
t.truthy(appToggles[0].seenToggles.includes('toggleY'));
t.truthy(togglesForApp.length === 2);
t.truthy(togglesForApp.includes('toggleX'));
t.truthy(togglesForApp.includes('toggleY'));
metrics.destroy();
});
test('addClient', t => {
const metrics = new UnleashClientMetrics();
metrics.addClient(appName, instanceId);
metrics.addClient(appName, instanceId, new Date());
t.truthy(metrics.clients[instanceId].count === 0);
t.truthy(metrics.globalCount === 0);
test('should handle a lot of toggles', t => {
const store = new EventEmitter();
const metrics = new UnleashClientMetrics(store);
const toggleCounts = {};
for (let i=0; i<100; i++) {
toggleCounts[`toggle${i}`] = {yes: i, no: i}
}
store.emit('metrics', {
appName,
instanceId,
bucket: {
start: new Date(),
stop: new Date(),
toggles: toggleCounts,
},
});
const seenToggles = metrics.getSeenTogglesByAppName(appName);
t.truthy(seenToggles.length === 100);
metrics.destroy();
});
test('addApp', t => {
const metrics = new UnleashClientMetrics();
metrics.addApp(appName, instanceId);
t.truthy(metrics.apps[appName].clients.length === 1);
metrics.addApp(appName, 'instanceId2');
t.truthy(metrics.apps[appName].clients.length === 2);
metrics.addApp('appName2', 'instanceId2');
t.truthy(metrics.apps.appName2.clients.length === 1);
metrics.addApp('appName2', instanceId);
t.truthy(metrics.apps.appName2.clients.length === 2);
metrics.destroy();
});
});

View File

@ -4,10 +4,10 @@ const Projection = require('./projection.js');
const TTLList = require('./ttl-list.js');
module.exports = class UnleashClientMetrics {
constructor () {
constructor (clientMetricsStore) {
this.globalCount = 0;
this.apps = {};
this.clients = {};
this.lastHourProjection = new Projection();
this.lastMinuteProjection = new Projection();
@ -15,6 +15,7 @@ module.exports = class UnleashClientMetrics {
this.lastHourList = new TTLList({
interval: 10000,
});
this.lastMinuteList = new TTLList({
interval: 10000,
expireType: 'minutes',
@ -31,18 +32,20 @@ module.exports = class UnleashClientMetrics {
this.lastMinuteProjection.substract(toggleName, toggles[toggleName]);
});
});
clientMetricsStore.on('metrics', (m) => this.addPayload(m));
}
toJSON () {
return JSON.stringify(this.getMetricsOverview(), null, 4);
getAppsWitToggles () {
const apps = [];
Object.keys(this.apps).forEach(appName => {
const seenToggles = Object.keys(this.apps[appName].seenToggles);
const metricsCount = this.apps[appName].count;
apps.push({appName, seenToggles, metricsCount})
});
return apps;
}
getMetricsOverview () {
return {
globalCount: this.globalCount,
apps: this.apps,
clients: this.clients,
};
getSeenTogglesByAppName(appName) {
return this.apps[appName] ? Object.keys(this.apps[appName].seenToggles) : [];
}
getTogglesMetrics () {
@ -53,16 +56,24 @@ module.exports = class UnleashClientMetrics {
}
addPayload (data) {
this.addClient(data.appName, data.instanceId);
this.addBucket(data.appName, data.instanceId, data.bucket);
const { appName, bucket } = data;
const app = this.getApp(appName)
this.addBucket(app, data.bucket);
}
addBucket (appName, instanceId, bucket) {
getApp(appName) {
this.apps[appName] = this.apps[appName] || {seenToggles: {}, count: 0};
return this.apps[appName];
}
addBucket (app, bucket) {
let count = 0;
// TODO stop should be createdAt
const { stop, toggles } = bucket;
Object.keys(toggles).forEach((n) => {
const toggleNames = Object.keys(toggles);
toggleNames.forEach((n) => {
const entry = toggles[n];
this.lastHourProjection.add(n, entry);
this.lastMinuteProjection.add(n, entry);
@ -72,49 +83,13 @@ module.exports = class UnleashClientMetrics {
this.lastHourList.add(toggles, stop);
this.lastMinuteList.add(toggles, stop);
this.addClientCount(appName, instanceId, count);
this.globalCount += count;
app.count += count;
this.addSeenToggles(app, toggleNames);
}
addClientCount (appName, instanceId, count) {
if (typeof count === 'number' && count > 0) {
this.globalCount += count;
if (this.clients[instanceId]) {
this.clients[instanceId].count += count;
}
if (this.apps[appName]) {
this.apps[appName].count += count;
}
}
}
addClient (appName, instanceId, started = new Date()) {
this.addApp(appName, instanceId);
if (instanceId) {
if (this.clients[instanceId]) {
this.clients[instanceId].ping = new Date();
} else {
this.clients[instanceId] = {
appName,
count: 0,
started,
init: new Date(),
ping: new Date(),
};
}
}
}
addApp (appName, instanceId) {
if (appName && !this.apps[appName]) {
this.apps[appName] = {
count: 0,
clients: [],
};
}
if (instanceId && !this.apps[appName].clients.includes(instanceId)) {
this.apps[appName].clients.push(instanceId);
}
addSeenToggles (app, toggleNames) {
toggleNames.forEach(t => app.seenToggles[t] = true);
}
destroy () {

View File

@ -1,43 +0,0 @@
'use strict';
const { EventEmitter } = require('events');
module.exports = class UnleashClientMetrics extends EventEmitter {
constructor (metricsDb, interval = 10000) {
super();
this.interval = interval;
this.db = metricsDb;
this.highestIdSeen = 0;
this.db.getMetricsLastHour().then(metrics => {
this.addMetrics(metrics);
this.startPoller();
this.emit('ready');
});
this.timer = null;
}
addMetrics (metrics) {
if (metrics && metrics.length > 0) {
this.highestIdSeen = metrics[metrics.length - 1].id;
}
this.emit('metrics', metrics);
}
startPoller () {
this.timer = setInterval(() => {
this.db.getNewMetrics(this.highestIdSeen)
.then(metrics => this.addMetrics(metrics));
}, this.interval);
this.timer.unref();
}
insert (metrics) {
return this.db.insert(metrics);
}
destroy () {
try {
clearTimeout(this.timer);
} catch (e) {}
}
};

View File

@ -1,66 +0,0 @@
'use strict';
const { test } = require('ava');
const MetricsService = require('./service');
const sinon = require('sinon');
function getMockDb () {
const list = [{ id: 2 }, { id: 3 }, { id: 4 }];
const db = {
getMetricsLastHour () {
return Promise.resolve([{ id: 1 }]);
},
getNewMetrics () {
return Promise.resolve([list.pop() || { id: 0 }]);
},
};
return {
db,
};
}
test.cb('should call database on startup', (t) => {
const mock = getMockDb();
const service = new MetricsService(mock.db);
t.plan(2);
service.on('metrics', ([metric]) => {
t.true(service.highestIdSeen === 1);
t.true(metric.id === 1);
t.end();
service.destroy();
});
});
test.cb('should poll for updates', (t) => {
const clock = sinon.useFakeTimers();
const mock = getMockDb();
const service = new MetricsService(mock.db, 100);
const metrics = [];
service.on('metrics', (_metrics) => {
_metrics.forEach(m => m && metrics.push(m));
});
t.true(metrics.length === 0);
service.on('ready', () => {
t.true(metrics.length === 1);
clock.tick(300);
clock.restore();
process.nextTick(() => {
t.true(metrics.length === 4);
t.true(metrics[0].id === 1);
t.true(metrics[1].id === 4);
t.true(metrics[2].id === 3);
t.true(metrics[3].id === 2);
service.destroy();
t.end();
});
});
});

View File

@ -27,10 +27,7 @@ test.cb('should slice off list', (t) => {
expireType: 'milliseconds',
});
// console.time('4');
// console.time('3');
// console.time('2');
// console.time('1');
list.add({ n: '1' }, moment().add(1, 'milliseconds'));
list.add({ n: '2' }, moment().add(50, 'milliseconds'));
list.add({ n: '3' }, moment().add(200, 'milliseconds'));

View File

@ -1,6 +1,7 @@
/* eslint camelcase: "off" */
'use strict';
const logger = require('../logger');
const COLUMNS = ['app_name', 'instance_id', 'client_ip', 'last_seen', 'created_at'];
const TABLE = 'client_instances';
@ -12,10 +13,24 @@ const mapRow = (row) => ({
createdAt: row.created_at,
});
const mapAppsRow = (row) => ({
appName: row.app_name,
createdAt: row.created_at,
});
class ClientInstanceStore {
constructor (db) {
this.db = db;
setTimeout(() => this._removeInstancesOlderThanTwoDays(), 10).unref();
setInterval(() => this._removeInstancesOlderThanTwoDays(), 24 * 61 * 60 * 1000).unref();
}
_removeInstancesOlderThanTwoDays () {
this.db(TABLE)
.whereRaw('created_at < now() - interval \'2 days\'')
.del()
.then((res) => logger.info(`Deleted ${res} instances`));
}
updateRow (details) {
@ -58,6 +73,24 @@ class ClientInstanceStore {
.orderBy('last_seen', 'desc')
.map(mapRow);
}
getByAppName (appName) {
return this.db
.select(COLUMNS)
.from(TABLE)
.where('app_name', appName)
.orderBy('last_seen', 'desc')
.map(mapRow);
}
getApplications () {
return this.db
.distinct('app_name')
.select(['app_name'])
.from(TABLE)
.orderBy('app_name', 'desc')
.map(mapRow);
}
};
module.exports = ClientInstanceStore;

View File

@ -0,0 +1,58 @@
'use strict';
const logger = require('../logger');
const METRICS_COLUMNS = ['id', 'created_at', 'metrics'];
const TABLE = 'client_metrics';
const mapRow = (row) => ({
id: row.id,
createdAt: row.created_at,
metrics: row.metrics,
});
class ClientMetricsDb {
constructor (db) {
this.db = db;
//Clear old metrics regulary
setTimeout(() => this.removeMetricsOlderThanOneHour(), 10).unref();
setInterval(() => this.removeMetricsOlderThanOneHour(), 60 * 1000).unref();
}
removeMetricsOlderThanOneHour () {
this.db(TABLE)
.whereRaw('created_at < now() - interval \'1 hour\'')
.del()
.then((res) => logger.info(`Deleted ${res} metrics`));
}
// Insert new client metrics
insert (metrics) {
return this.db(TABLE).insert({ metrics });
}
// Used at startup to load all metrics last week into memory!
getMetricsLastHour () {
return this.db
.select(METRICS_COLUMNS)
.from(TABLE)
.limit(2000)
.whereRaw('created_at > now() - interval \'1 hour\'')
.orderBy('created_at', 'asc')
.map(mapRow);
}
// Used to poll for new metrics
getNewMetrics (lastKnownId) {
return this.db
.select(METRICS_COLUMNS)
.from(TABLE)
.limit(1000)
.where('id', '>', lastKnownId)
.orderBy('created_at', 'asc')
.map(mapRow);
}
};
module.exports = ClientMetricsDb;

View File

@ -1,55 +1,53 @@
'use strict';
const logger = require('../logger');
const METRICS_COLUMNS = ['id', 'created_at', 'metrics'];
const TABLE = 'client_metrics';
const mapRow = (row) => ({
id: row.id,
createdAt: row.created_at,
metrics: row.metrics,
});
const { EventEmitter } = require('events');
class ClientMetricsStore {
const TEN_SECONDS = 10 * 1000;
constructor (db) {
this.db = db;
setTimeout(() => this._removeMetricsOlderThanOneHour(), 10).unref();
setInterval(() => this._removeMetricsOlderThanOneHour(), 60 * 60 * 1000).unref();
class ClientMetricsStore extends EventEmitter {
constructor (metricsDb, pollInterval = TEN_SECONDS) {
super();
this.metricsDb = metricsDb;
this.highestIdSeen = 0;
this.timer;
//Build internal state
metricsDb.getMetricsLastHour()
.then((metrics) => this._emitMetrics(metrics))
.then(() => this._startPoller(pollInterval))
.then(() => this.emit('ready'))
.catch((err) => logger.error(err));
}
_removeMetricsOlderThanOneHour () {
this.db(TABLE)
.whereRaw('created_at < now() - interval \'1 hour\'')
.del()
.then((res) => logger.info(`Deleted ${res} metrics`));
_startPoller (pollInterval) {
this.timer = setInterval(() => this._fetchNewAndEmit(), pollInterval);
this.timer.unref();
}
_fetchNewAndEmit() {
this.metricsDb.getNewMetrics(this.highestIdSeen)
.then((metrics) => this._emitMetrics(metrics))
}
_emitMetrics (metrics) {
if (metrics && metrics.length > 0) {
this.highestIdSeen = metrics[metrics.length - 1].id;
metrics.forEach(m => this.emit('metrics', m.metrics));
}
}
// Insert new client metrics
insert (metrics) {
return this.db(TABLE).insert({ metrics });
return this.metricsDb.insert(metrics)
}
// Used at startup to load all metrics last week into memory!
getMetricsLastHour () {
return this.db
.select(METRICS_COLUMNS)
.from(TABLE)
.limit(2000)
.whereRaw('created_at > now() - interval \'1 hour\'')
.orderBy('created_at', 'asc')
.map(mapRow);
}
// Used to poll for new metrics
getNewMetrics (lastKnownId) {
return this.db
.select(METRICS_COLUMNS)
.from(TABLE)
.limit(1000)
.where('id', '>', lastKnownId)
.orderBy('created_at', 'asc')
.map(mapRow);
destroy () {
try {
clearInterval(this.timer);
} catch (e) {}
}
};

View File

@ -0,0 +1,62 @@
'use strict';
const { test } = require('ava');
const ClientMetricStore = require('./client-metrics-store');
const sinon = require('sinon');
function getMockDb () {
const list = [{ id: 4, metrics: {appName: 'test'} }, { id: 3, metrics: {appName: 'test'} }, { id: 2, metrics: {appName: 'test'} }];
return {
getMetricsLastHour () {
return Promise.resolve([{ id: 1, metrics: {appName: 'test'} }]);
},
getNewMetrics (v) {
return Promise.resolve([list.pop() || { id: 0 }]);
}
};
}
test.cb('should call database on startup', (t) => {
const mock = getMockDb();
const store = new ClientMetricStore(mock);
t.plan(2);
store.on('metrics', (metrics) => {
t.true(store.highestIdSeen === 1);
t.true(metrics.appName === 'test');
store.destroy();
t.end();
});
});
test.cb('should poll for updates', (t) => {
const clock = sinon.useFakeTimers();
const mock = getMockDb();
const store = new ClientMetricStore(mock, 100);
const metrics = [];
store.on('metrics', (m) => metrics.push(m));
t.true(metrics.length === 0);
store.on('ready', () => {
t.true(metrics.length === 1);
clock.tick(300);
process.nextTick(() => {
t.true(metrics.length === 4);
t.true(store.highestIdSeen === 4);
store.destroy();
clock.restore();
t.end();
});
});
});

View File

@ -49,6 +49,14 @@ class ClientStrategyStore {
.from(TABLE)
.map(mapRow);
}
getByAppName (appName) {
return this.db
.select(COLUMNS)
.where('app_name', appName)
.from(TABLE)
.map(mapRow);
}
};
module.exports = ClientStrategyStore;

View File

@ -5,12 +5,14 @@ const EventStore = require('./event-store');
const FeatureToggleStore = require('./feature-toggle-store');
const StrategyStore = require('./strategy-store');
const ClientInstanceStore = require('./client-instance-store');
const ClientMetricsDb = require('./client-metrics-db');
const ClientMetricsStore = require('./client-metrics-store');
const ClientStrategyStore = require('./client-strategy-store');
module.exports.createStores = (config) => {
const db = createDb(config);
const eventStore = new EventStore(db);
const clientMetricsDb = new ClientMetricsDb(db);
return {
db,
@ -18,7 +20,7 @@ module.exports.createStores = (config) => {
featureToggleStore: new FeatureToggleStore(db, eventStore),
strategyStore: new StrategyStore(db, eventStore),
clientInstanceStore: new ClientInstanceStore(db),
clientMetricsStore: new ClientMetricsStore(db),
clientMetricsStore: new ClientMetricsStore(clientMetricsDb),
clientStrategyStore: new ClientStrategyStore(db),
};
};

View File

@ -2,10 +2,16 @@
const logger = require('../logger');
const ClientMetrics = require('../client-metrics');
const ClientMetricsService = require('../client-metrics/service');
const joi = require('joi');
const { clientMetricsSchema, clientRegisterSchema } = require('./metrics-schema');
/*
* TODO:
* - always catch errors and always return a response to client!
* - clean up and document uri endpoint
* - always json response (middleware?)
* - fix failing tests
*/
module.exports = function (app, config) {
const {
clientMetricsStore,
@ -13,30 +19,33 @@ module.exports = function (app, config) {
clientInstanceStore,
} = config.stores;
const metrics = new ClientMetrics();
const service = new ClientMetricsService(clientMetricsStore);
service.on('metrics', (entries) => {
entries.forEach((m) => {
metrics.addPayload(m.metrics);
});
const metrics = new ClientMetrics(clientMetricsStore);
app.get('/client/seen-toggles', (req, res) => {
const seenAppToggles = metrics.getAppsWitToggles();
res.json(seenAppToggles);
});
app.get('/metrics', (req, res) => {
res.json(metrics.getMetricsOverview());
});
app.get('/metrics/features', (req, res) => {
app.get('/metrics/feature-toggles', (req, res) => {
res.json(metrics.getTogglesMetrics());
});
app.post('/client/metrics', (req, res) => {
const data = req.body;
const clientIp = req.ip;
joi.validate(data, clientMetricsSchema, (err, cleaned) => {
if (err) {
return res.status(400).json(err);
}
service.insert(cleaned)
clientMetricsStore
.insert(cleaned)
.then(() => clientInstanceStore.insert({
appName: cleaned.appName,
instanceId: cleaned.instanceId,
clientIp,
}))
.catch(e => logger.error('Error inserting metrics data', e));
res.status(202).end();
@ -52,7 +61,8 @@ module.exports = function (app, config) {
return res.status(400).json(err);
}
clientStrategyStore.insert(cleaned.appName, cleaned.strategies)
clientStrategyStore
.insert(cleaned.appName, cleaned.strategies)
.then(() => clientInstanceStore.insert({
appName: cleaned.appName,
instanceId: cleaned.instanceId,
@ -66,12 +76,40 @@ module.exports = function (app, config) {
});
app.get('/client/strategies', (req, res) => {
clientStrategyStore.getAll().then(data => res.json(data));
const appName = req.query.appName;
if(appName) {
clientStrategyStore.getByAppName(appName)
.then(data => res.json(data))
.catch(err => logger.error(err));
} else {
clientStrategyStore.getAll()
.then(data => res.json(data))
.catch(err => logger.error(err));
}
});
app.get('/client/instances', (req, res) => {
clientInstanceStore.getAll()
.then(data => res.json(data))
app.get('/client/applications/', (req, res) => {
clientInstanceStore.getApplications()
.then(apps => {
const applications = apps.map(({appName}) => ({
appName: appName,
links: {
appDetails: `/api/client/applications/${appName}`
}
}))
res.json({applications})
})
.catch(err => logger.error(err));
});
app.get('/client/applications/:appName', (req, res) => {
const appName = req.params.appName;
const seenToggles = metrics.getSeenTogglesByAppName(appName);
Promise.all([
clientInstanceStore.getByAppName(appName),
clientStrategyStore.getByAppName(appName)
])
.then(([instances, strategies]) => res.json({appName, instances, strategies, seenToggles}))
.catch(err => logger.error(err));
});
};

View File

@ -78,6 +78,13 @@ function createClientInstance (stores) {
started: Date.now(),
interval: 10,
},
{
appName: 'demo-seed-2',
instanceId: 'test-2',
strategies: ['default'],
started: Date.now(),
interval: 10,
},
].map(client => stores.clientInstanceStore.insert(client));
}

View File

@ -51,14 +51,27 @@ test.serial('should get client strategies', async t => {
.then(destroy);
});
test.serial('should get client instances', async t => {
test.serial('should get application details', async t => {
const { request, destroy } = await setupApp('metrics_serial');
return request
.get('/api/client/instances')
.get('/api/client/applications/demo-seed')
.expect('Content-Type', /json/)
.expect((res) => {
t.true(res.status === 200);
t.true(res.body.length === 1);
t.true(res.body.appName === 'demo-seed');
t.true(res.body.instances.length === 1);
})
.then(destroy);
});
test.serial('should get list of applications', async t => {
const { request, destroy } = await setupApp('metrics_serial');
return request
.get('/api/client/applications')
.expect('Content-Type', /json/)
.expect((res) => {
t.true(res.status === 200);
t.true(res.body.applications.length === 2);
})
.then(destroy);
});

View File

@ -3,4 +3,5 @@
module.exports = () => ({
getMetricsLastHour: () => Promise.resolve([]),
insert: () => Promise.resolve(),
on: () => {}
});