mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-28 00:06:53 +01:00
c17a1980a2
This simplifies stores to just be storage interaction, they no longer react to events. Controllers now call services and awaits the result from the call. When the service calls are returned the database is updated. This simplifies testing dramatically, cause you know that your state is updated when returned from a call, rather than hoping the store has picked up the event (which really was a command) and reacted to it. Events are still emitted from eventStore, so other parts of the app can react to events as they're being sent out. As part of the move to services, we now also emit an application-created event when we see a new client application. Fixes: #685 Fixes: #595
276 lines
8.7 KiB
JavaScript
276 lines
8.7 KiB
JavaScript
/* eslint-disable no-param-reassign */
|
|
|
|
'use strict';
|
|
|
|
const Projection = require('./projection.js');
|
|
const TTLList = require('./ttl-list.js');
|
|
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');
|
|
|
|
module.exports = class ClientMetricsService {
|
|
constructor(
|
|
{
|
|
clientMetricsStore,
|
|
strategyStore,
|
|
featureToggleStore,
|
|
clientApplicationsStore,
|
|
clientInstanceStore,
|
|
eventStore,
|
|
},
|
|
{ getLogger },
|
|
) {
|
|
this.globalCount = 0;
|
|
this.apps = {};
|
|
this.strategyStore = strategyStore;
|
|
this.toggleStore = featureToggleStore;
|
|
this.clientAppStore = clientApplicationsStore;
|
|
this.clientInstanceStore = clientInstanceStore;
|
|
this.clientMetricsStore = clientMetricsStore;
|
|
this.lastHourProjection = new Projection();
|
|
this.lastMinuteProjection = new Projection();
|
|
this.eventStore = eventStore;
|
|
|
|
this.lastHourList = new TTLList({
|
|
interval: 10000,
|
|
});
|
|
this.logger = getLogger('services/client-metrics/index.js');
|
|
|
|
this.lastMinuteList = new TTLList({
|
|
interval: 10000,
|
|
expireType: 'minutes',
|
|
expireAmount: 1,
|
|
});
|
|
|
|
this.lastHourList.on('expire', toggles => {
|
|
Object.keys(toggles).forEach(toggleName => {
|
|
this.lastHourProjection.substract(
|
|
toggleName,
|
|
this.createCountObject(toggles[toggleName]),
|
|
);
|
|
});
|
|
});
|
|
this.lastMinuteList.on('expire', toggles => {
|
|
Object.keys(toggles).forEach(toggleName => {
|
|
this.lastMinuteProjection.substract(
|
|
toggleName,
|
|
this.createCountObject(toggles[toggleName]),
|
|
);
|
|
});
|
|
});
|
|
clientMetricsStore.on('metrics', m => this.addPayload(m));
|
|
}
|
|
|
|
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}`,
|
|
);
|
|
}
|
|
|
|
getAppsWithToggles() {
|
|
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;
|
|
}
|
|
|
|
getSeenTogglesByAppName(appName) {
|
|
return this.apps[appName]
|
|
? Object.keys(this.apps[appName].seenToggles)
|
|
: [];
|
|
}
|
|
|
|
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}`,
|
|
},
|
|
};
|
|
}
|
|
|
|
getSeenAppsPerToggle() {
|
|
const toggles = {};
|
|
Object.keys(this.apps).forEach(appName => {
|
|
Object.keys(this.apps[appName].seenToggles).forEach(
|
|
seenToggleName => {
|
|
if (!toggles[seenToggleName]) {
|
|
toggles[seenToggleName] = [];
|
|
}
|
|
toggles[seenToggleName].push({ appName });
|
|
},
|
|
);
|
|
});
|
|
return toggles;
|
|
}
|
|
|
|
getTogglesMetrics() {
|
|
return {
|
|
lastHour: this.lastHourProjection.getProjection(),
|
|
lastMinute: this.lastMinuteProjection.getProjection(),
|
|
};
|
|
}
|
|
|
|
addPayload(data) {
|
|
const { appName, bucket } = data;
|
|
const app = this.getApp(appName);
|
|
this.addBucket(app, bucket);
|
|
}
|
|
|
|
getApp(appName) {
|
|
this.apps[appName] = this.apps[appName] || {
|
|
seenToggles: {},
|
|
count: 0,
|
|
};
|
|
return this.apps[appName];
|
|
}
|
|
|
|
createCountObject(entry) {
|
|
let yes = typeof entry.yes === 'number' ? entry.yes : 0;
|
|
let no = typeof entry.no === 'number' ? entry.no : 0;
|
|
|
|
if (entry.variants) {
|
|
Object.entries(entry.variants).forEach(([key, value]) => {
|
|
if (key === 'disabled') {
|
|
no += value;
|
|
} else {
|
|
yes += value;
|
|
}
|
|
});
|
|
}
|
|
|
|
return { yes, no };
|
|
}
|
|
|
|
addBucket(app, bucket) {
|
|
let count = 0;
|
|
// TODO stop should be createdAt
|
|
const { stop, toggles } = bucket;
|
|
|
|
const toggleNames = Object.keys(toggles);
|
|
|
|
toggleNames.forEach(n => {
|
|
const countObj = this.createCountObject(toggles[n]);
|
|
this.lastHourProjection.add(n, countObj);
|
|
this.lastMinuteProjection.add(n, countObj);
|
|
count += countObj.yes + countObj.no;
|
|
});
|
|
|
|
this.lastHourList.add(toggles, stop);
|
|
this.lastMinuteList.add(toggles, stop);
|
|
|
|
this.globalCount += count;
|
|
app.count += count;
|
|
this.addSeenToggles(app, toggleNames);
|
|
}
|
|
|
|
addSeenToggles(app, toggleNames) {
|
|
toggleNames.forEach(t => {
|
|
app.seenToggles[t] = true;
|
|
});
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
destroy() {
|
|
this.lastHourList.destroy();
|
|
this.lastMinuteList.destroy();
|
|
}
|
|
};
|