1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-10-18 20:09:08 +02:00
unleash.unleash/lib/services/client-metrics/index.js
Christopher Kolstad c17a1980a2
Add service layer
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
2021-01-21 10:59:19 +01:00

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();
}
};