2020-04-14 22:29:11 +02:00
|
|
|
/* eslint-disable no-param-reassign */
|
|
|
|
|
2016-10-27 13:13:51 +02:00
|
|
|
'use strict';
|
|
|
|
|
2016-11-04 16:16:55 +01:00
|
|
|
const Projection = require('./projection.js');
|
|
|
|
const TTLList = require('./ttl-list.js');
|
2021-01-18 12:32:19 +01:00
|
|
|
const appSchema = require('./metrics-schema');
|
|
|
|
const NotFoundError = require('../../error/notfound-error');
|
|
|
|
const { clientMetricsSchema } = require('./client-metrics-schema');
|
|
|
|
const { clientRegisterSchema } = require('./register-schema');
|
|
|
|
const { APPLICATION_CREATED } = require('../../event-type');
|
2016-11-04 16:16:55 +01:00
|
|
|
|
2020-12-17 19:22:30 +01:00
|
|
|
module.exports = class ClientMetricsService {
|
2021-01-18 12:32:19 +01:00
|
|
|
constructor(
|
|
|
|
{
|
|
|
|
clientMetricsStore,
|
|
|
|
strategyStore,
|
|
|
|
featureToggleStore,
|
|
|
|
clientApplicationsStore,
|
|
|
|
clientInstanceStore,
|
|
|
|
eventStore,
|
|
|
|
},
|
|
|
|
{ getLogger },
|
|
|
|
) {
|
2016-10-27 13:13:51 +02:00
|
|
|
this.globalCount = 0;
|
2016-11-04 16:16:55 +01:00
|
|
|
this.apps = {};
|
2021-01-18 12:32:19 +01:00
|
|
|
this.strategyStore = strategyStore;
|
|
|
|
this.toggleStore = featureToggleStore;
|
|
|
|
this.clientAppStore = clientApplicationsStore;
|
|
|
|
this.clientInstanceStore = clientInstanceStore;
|
|
|
|
this.clientMetricsStore = clientMetricsStore;
|
2016-11-07 09:13:19 +01:00
|
|
|
this.lastHourProjection = new Projection();
|
|
|
|
this.lastMinuteProjection = new Projection();
|
2021-01-18 12:32:19 +01:00
|
|
|
this.eventStore = eventStore;
|
2016-11-07 09:13:19 +01:00
|
|
|
|
|
|
|
this.lastHourList = new TTLList({
|
|
|
|
interval: 10000,
|
|
|
|
});
|
2021-01-18 12:32:19 +01:00
|
|
|
this.logger = getLogger('services/client-metrics/index.js');
|
2016-11-28 17:11:11 +01:00
|
|
|
|
2016-11-07 09:13:19 +01:00
|
|
|
this.lastMinuteList = new TTLList({
|
|
|
|
interval: 10000,
|
|
|
|
expireType: 'minutes',
|
|
|
|
expireAmount: 1,
|
|
|
|
});
|
|
|
|
|
2017-06-28 10:17:14 +02:00
|
|
|
this.lastHourList.on('expire', toggles => {
|
2016-11-04 16:16:55 +01:00
|
|
|
Object.keys(toggles).forEach(toggleName => {
|
2017-06-28 10:17:14 +02:00
|
|
|
this.lastHourProjection.substract(
|
|
|
|
toggleName,
|
2020-04-14 22:29:11 +02:00
|
|
|
this.createCountObject(toggles[toggleName]),
|
2017-06-28 10:17:14 +02:00
|
|
|
);
|
2016-11-07 09:13:19 +01:00
|
|
|
});
|
|
|
|
});
|
2017-06-28 10:17:14 +02:00
|
|
|
this.lastMinuteList.on('expire', toggles => {
|
2016-11-07 09:13:19 +01:00
|
|
|
Object.keys(toggles).forEach(toggleName => {
|
2017-06-28 10:17:14 +02:00
|
|
|
this.lastMinuteProjection.substract(
|
|
|
|
toggleName,
|
2020-04-14 22:29:11 +02:00
|
|
|
this.createCountObject(toggles[toggleName]),
|
2017-06-28 10:17:14 +02:00
|
|
|
);
|
2016-11-04 16:16:55 +01:00
|
|
|
});
|
|
|
|
});
|
2017-06-28 10:17:14 +02:00
|
|
|
clientMetricsStore.on('metrics', m => this.addPayload(m));
|
2016-10-27 13:13:51 +02:00
|
|
|
}
|
|
|
|
|
2021-01-18 12:32:19 +01:00
|
|
|
async registerClientMetrics(data, clientIp) {
|
|
|
|
const value = await clientMetricsSchema.validateAsync(data);
|
|
|
|
const toggleNames = Object.keys(value.bucket.toggles);
|
|
|
|
await this.toggleStore.lastSeenToggles(toggleNames);
|
|
|
|
await this.clientMetricsStore.insert(value);
|
|
|
|
await this.clientInstanceStore.insert({
|
|
|
|
appName: value.appName,
|
|
|
|
instanceId: value.instanceId,
|
|
|
|
clientIp,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async upsertApp(value, clientIp) {
|
|
|
|
try {
|
|
|
|
const app = await this.clientAppStore.getApplication(value.appName);
|
|
|
|
await this.updateRow(value, app);
|
|
|
|
} catch (error) {
|
|
|
|
if (error instanceof NotFoundError) {
|
|
|
|
await this.clientAppStore.insertNewRow(value);
|
|
|
|
await this.eventStore.store({
|
|
|
|
type: APPLICATION_CREATED,
|
|
|
|
createdBy: clientIp,
|
|
|
|
data: value,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async registerClient(data, clientIp) {
|
|
|
|
const value = await clientRegisterSchema.validateAsync(data);
|
|
|
|
value.clientIp = clientIp;
|
|
|
|
await this.upsertApp(value, clientIp);
|
|
|
|
await this.clientInstanceStore.insert(value);
|
|
|
|
this.logger.info(
|
|
|
|
`New client registration: appName=${value.appName}, instanceId=${value.instanceId}`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-06-28 10:17:14 +02:00
|
|
|
getAppsWithToggles() {
|
2016-11-28 17:11:11 +01:00
|
|
|
const apps = [];
|
|
|
|
Object.keys(this.apps).forEach(appName => {
|
|
|
|
const seenToggles = Object.keys(this.apps[appName].seenToggles);
|
|
|
|
const metricsCount = this.apps[appName].count;
|
2016-12-04 14:09:37 +01:00
|
|
|
apps.push({ appName, seenToggles, metricsCount });
|
2016-11-28 17:11:11 +01:00
|
|
|
});
|
|
|
|
return apps;
|
2016-10-27 13:13:51 +02:00
|
|
|
}
|
2020-04-14 22:29:11 +02:00
|
|
|
|
2017-06-28 10:17:14 +02:00
|
|
|
getSeenTogglesByAppName(appName) {
|
|
|
|
return this.apps[appName]
|
|
|
|
? Object.keys(this.apps[appName].seenToggles)
|
|
|
|
: [];
|
2016-10-27 13:13:51 +02:00
|
|
|
}
|
|
|
|
|
2021-01-18 12:32:19 +01:00
|
|
|
async getSeenApps() {
|
|
|
|
const seenApps = this.getSeenAppsPerToggle();
|
|
|
|
const applications = await this.clientAppStore.getApplications();
|
|
|
|
const metaData = applications.reduce((result, entry) => {
|
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
|
|
result[entry.appName] = entry;
|
|
|
|
return result;
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
Object.keys(seenApps).forEach(key => {
|
|
|
|
seenApps[key] = seenApps[key].map(entry => {
|
|
|
|
if (metaData[entry.appName]) {
|
|
|
|
return { ...entry, ...metaData[entry.appName] };
|
|
|
|
}
|
|
|
|
return entry;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
return seenApps;
|
|
|
|
}
|
|
|
|
|
|
|
|
async getApplications(query) {
|
|
|
|
return this.clientAppStore.getApplications(query);
|
|
|
|
}
|
|
|
|
|
|
|
|
async getApplication(appName) {
|
|
|
|
const seenToggles = this.getSeenTogglesByAppName(appName);
|
|
|
|
const [
|
|
|
|
application,
|
|
|
|
instances,
|
|
|
|
strategies,
|
|
|
|
features,
|
|
|
|
] = await Promise.all([
|
|
|
|
this.clientAppStore.getApplication(appName),
|
|
|
|
this.clientInstanceStore.getByAppName(appName),
|
|
|
|
this.strategyStore.getStrategies(),
|
|
|
|
this.toggleStore.getFeatures(),
|
|
|
|
]);
|
|
|
|
|
|
|
|
return {
|
|
|
|
appName: application.appName,
|
|
|
|
createdAt: application.createdAt,
|
|
|
|
description: application.description,
|
|
|
|
url: application.url,
|
|
|
|
color: application.color,
|
|
|
|
icon: application.icon,
|
|
|
|
strategies: application.strategies.map(name => {
|
|
|
|
const found = strategies.find(f => f.name === name);
|
|
|
|
return found || { name, notFound: true };
|
|
|
|
}),
|
|
|
|
instances,
|
|
|
|
seenToggles: seenToggles.map(name => {
|
|
|
|
const found = features.find(f => f.name === name);
|
|
|
|
return found || { name, notFound: true };
|
|
|
|
}),
|
|
|
|
links: {
|
|
|
|
self: `/api/applications/${application.appName}`,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-06-28 10:17:14 +02:00
|
|
|
getSeenAppsPerToggle() {
|
2016-12-05 13:53:53 +01:00
|
|
|
const toggles = {};
|
2016-12-05 13:27:08 +01:00
|
|
|
Object.keys(this.apps).forEach(appName => {
|
2017-11-11 08:43:08 +01:00
|
|
|
Object.keys(this.apps[appName].seenToggles).forEach(
|
|
|
|
seenToggleName => {
|
|
|
|
if (!toggles[seenToggleName]) {
|
|
|
|
toggles[seenToggleName] = [];
|
|
|
|
}
|
|
|
|
toggles[seenToggleName].push({ appName });
|
2020-04-14 22:29:11 +02:00
|
|
|
},
|
2017-11-11 08:43:08 +01:00
|
|
|
);
|
2016-12-05 13:27:08 +01:00
|
|
|
});
|
2016-12-05 13:53:53 +01:00
|
|
|
return toggles;
|
2016-12-05 13:27:08 +01:00
|
|
|
}
|
|
|
|
|
2017-06-28 10:17:14 +02:00
|
|
|
getTogglesMetrics() {
|
2016-11-07 09:13:19 +01:00
|
|
|
return {
|
|
|
|
lastHour: this.lastHourProjection.getProjection(),
|
|
|
|
lastMinute: this.lastMinuteProjection.getProjection(),
|
|
|
|
};
|
2016-11-04 16:16:55 +01:00
|
|
|
}
|
|
|
|
|
2017-06-28 10:17:14 +02:00
|
|
|
addPayload(data) {
|
2016-11-28 17:11:11 +01:00
|
|
|
const { appName, bucket } = data;
|
2016-12-04 14:09:37 +01:00
|
|
|
const app = this.getApp(appName);
|
|
|
|
this.addBucket(app, bucket);
|
2016-11-28 17:11:11 +01:00
|
|
|
}
|
|
|
|
|
2017-06-28 10:17:14 +02:00
|
|
|
getApp(appName) {
|
|
|
|
this.apps[appName] = this.apps[appName] || {
|
|
|
|
seenToggles: {},
|
|
|
|
count: 0,
|
|
|
|
};
|
2016-11-28 17:11:11 +01:00
|
|
|
return this.apps[appName];
|
2016-10-27 13:13:51 +02:00
|
|
|
}
|
|
|
|
|
2017-08-04 11:24:58 +02:00
|
|
|
createCountObject(entry) {
|
2020-04-14 22:29:11 +02:00
|
|
|
let yes = typeof entry.yes === 'number' ? entry.yes : 0;
|
|
|
|
let no = typeof entry.no === 'number' ? entry.no : 0;
|
2019-01-25 13:05:25 +01:00
|
|
|
|
|
|
|
if (entry.variants) {
|
|
|
|
Object.entries(entry.variants).forEach(([key, value]) => {
|
|
|
|
if (key === 'disabled') {
|
|
|
|
no += value;
|
|
|
|
} else {
|
|
|
|
yes += value;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-08-04 11:24:58 +02:00
|
|
|
return { yes, no };
|
|
|
|
}
|
|
|
|
|
2017-06-28 10:17:14 +02:00
|
|
|
addBucket(app, bucket) {
|
2016-10-27 13:13:51 +02:00
|
|
|
let count = 0;
|
2016-11-04 16:16:55 +01:00
|
|
|
// TODO stop should be createdAt
|
|
|
|
const { stop, toggles } = bucket;
|
2016-10-27 15:16:27 +02:00
|
|
|
|
2016-11-28 17:11:11 +01:00
|
|
|
const toggleNames = Object.keys(toggles);
|
|
|
|
|
2017-06-28 10:17:14 +02:00
|
|
|
toggleNames.forEach(n => {
|
2017-08-04 11:24:58 +02:00
|
|
|
const countObj = this.createCountObject(toggles[n]);
|
|
|
|
this.lastHourProjection.add(n, countObj);
|
|
|
|
this.lastMinuteProjection.add(n, countObj);
|
|
|
|
count += countObj.yes + countObj.no;
|
2016-10-27 13:13:51 +02:00
|
|
|
});
|
2016-11-04 16:16:55 +01:00
|
|
|
|
2016-11-07 09:13:19 +01:00
|
|
|
this.lastHourList.add(toggles, stop);
|
|
|
|
this.lastMinuteList.add(toggles, stop);
|
2016-11-04 16:16:55 +01:00
|
|
|
|
2016-11-28 17:11:11 +01:00
|
|
|
this.globalCount += count;
|
2016-12-27 21:03:50 +01:00
|
|
|
app.count += count;
|
2016-11-28 17:11:11 +01:00
|
|
|
this.addSeenToggles(app, toggleNames);
|
2016-10-27 13:13:51 +02:00
|
|
|
}
|
|
|
|
|
2017-06-28 10:17:14 +02:00
|
|
|
addSeenToggles(app, toggleNames) {
|
2016-12-04 14:09:37 +01:00
|
|
|
toggleNames.forEach(t => {
|
|
|
|
app.seenToggles[t] = true;
|
|
|
|
});
|
2016-10-27 13:13:51 +02:00
|
|
|
}
|
2016-11-13 18:14:29 +01:00
|
|
|
|
2021-01-18 12:32:19 +01:00
|
|
|
async deleteApplication(appName) {
|
|
|
|
await this.clientInstanceStore.deleteForApplication(appName);
|
|
|
|
await this.clientAppStore.deleteApplication(appName);
|
|
|
|
}
|
|
|
|
|
|
|
|
async createApplication(input) {
|
|
|
|
const applicationData = await appSchema.validateAsync(input);
|
|
|
|
await this.clientAppStore.upsert(applicationData);
|
|
|
|
}
|
|
|
|
|
2017-06-28 10:17:14 +02:00
|
|
|
destroy() {
|
2016-11-13 18:14:29 +01:00
|
|
|
this.lastHourList.destroy();
|
|
|
|
this.lastMinuteList.destroy();
|
|
|
|
}
|
2016-10-27 13:13:51 +02:00
|
|
|
};
|