mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-31 13:47:02 +02:00
chore: Convert client metrics controller to typescript (#831)
Co-authored-by: Ivar Conradi Østhus <ivarconr@gmail.com>
This commit is contained in:
parent
00f2c7312d
commit
2f013bacbf
@ -1,4 +1,5 @@
|
|||||||
'use strict';
|
import { Knex } from 'knex';
|
||||||
|
import { Logger, LogProvider } from '../logger';
|
||||||
|
|
||||||
const METRICS_COLUMNS = ['id', 'created_at', 'metrics'];
|
const METRICS_COLUMNS = ['id', 'created_at', 'metrics'];
|
||||||
const TABLE = 'client_metrics';
|
const TABLE = 'client_metrics';
|
||||||
@ -11,18 +12,27 @@ const mapRow = row => ({
|
|||||||
metrics: row.metrics,
|
metrics: row.metrics,
|
||||||
});
|
});
|
||||||
|
|
||||||
class ClientMetricsDb {
|
export interface IClientMetric {
|
||||||
constructor(db, getLogger) {
|
id: number;
|
||||||
this.db = db;
|
createdAt: Date;
|
||||||
|
metrics: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ClientMetricsDb {
|
||||||
|
private readonly logger: Logger;
|
||||||
|
|
||||||
|
private readonly timer: NodeJS.Timeout;
|
||||||
|
|
||||||
|
constructor(private db: Knex, getLogger: LogProvider) {
|
||||||
this.logger = getLogger('client-metrics-db.js');
|
this.logger = getLogger('client-metrics-db.js');
|
||||||
|
|
||||||
// Clear old metrics regulary
|
// Clear old metrics regularly
|
||||||
const clearer = () => this.removeMetricsOlderThanOneHour();
|
const clearer = () => this.removeMetricsOlderThanOneHour();
|
||||||
setTimeout(clearer, 10).unref();
|
setTimeout(clearer, 10).unref();
|
||||||
this.timer = setInterval(clearer, ONE_MINUTE).unref();
|
this.timer = setInterval(clearer, ONE_MINUTE).unref();
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeMetricsOlderThanOneHour() {
|
async removeMetricsOlderThanOneHour(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const rows = await this.db(TABLE)
|
const rows = await this.db(TABLE)
|
||||||
.whereRaw("created_at < now() - interval '1 hour'")
|
.whereRaw("created_at < now() - interval '1 hour'")
|
||||||
@ -36,12 +46,12 @@ class ClientMetricsDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Insert new client metrics
|
// Insert new client metrics
|
||||||
async insert(metrics) {
|
async insert(metrics: IClientMetric): Promise<void> {
|
||||||
return this.db(TABLE).insert({ metrics });
|
return this.db(TABLE).insert({ metrics });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used at startup to load all metrics last week into memory!
|
// Used at startup to load all metrics last week into memory!
|
||||||
async getMetricsLastHour() {
|
async getMetricsLastHour(): Promise<IClientMetric[]> {
|
||||||
try {
|
try {
|
||||||
const result = await this.db
|
const result = await this.db
|
||||||
.select(METRICS_COLUMNS)
|
.select(METRICS_COLUMNS)
|
||||||
@ -57,7 +67,7 @@ class ClientMetricsDb {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Used to poll for new metrics
|
// Used to poll for new metrics
|
||||||
async getNewMetrics(lastKnownId) {
|
async getNewMetrics(lastKnownId: number): Promise<IClientMetric[]> {
|
||||||
try {
|
try {
|
||||||
const res = await this.db
|
const res = await this.db
|
||||||
.select(METRICS_COLUMNS)
|
.select(METRICS_COLUMNS)
|
||||||
@ -72,9 +82,7 @@ class ClientMetricsDb {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy(): void {
|
||||||
clearInterval(this.timer);
|
clearInterval(this.timer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ClientMetricsDb;
|
|
@ -1,8 +1,7 @@
|
|||||||
'use strict';
|
import EventEmitter from 'events';
|
||||||
|
|
||||||
const { EventEmitter } = require('events');
|
import { ClientMetricsStore } from './client-metrics-store';
|
||||||
const ClientMetricStore = require('./client-metrics-store');
|
import getLogger from '../../test/fixtures/no-logger';
|
||||||
const getLogger = require('../../test/fixtures/no-logger');
|
|
||||||
|
|
||||||
function getMockDb() {
|
function getMockDb() {
|
||||||
const list = [
|
const list = [
|
||||||
@ -28,7 +27,7 @@ test('should call database on startup', done => {
|
|||||||
jest.useFakeTimers('modern');
|
jest.useFakeTimers('modern');
|
||||||
const mock = getMockDb();
|
const mock = getMockDb();
|
||||||
const ee = new EventEmitter();
|
const ee = new EventEmitter();
|
||||||
const store = new ClientMetricStore(mock, ee, getLogger);
|
const store = new ClientMetricsStore(mock as any, ee, getLogger);
|
||||||
|
|
||||||
jest.runAllTicks();
|
jest.runAllTicks();
|
||||||
|
|
||||||
@ -49,7 +48,7 @@ test('should start poller even if initial database fetch fails', done => {
|
|||||||
const mock = getMockDb();
|
const mock = getMockDb();
|
||||||
mock.getMetricsLastHour = () => Promise.reject(new Error('oops'));
|
mock.getMetricsLastHour = () => Promise.reject(new Error('oops'));
|
||||||
const ee = new EventEmitter();
|
const ee = new EventEmitter();
|
||||||
const store = new ClientMetricStore(mock, ee, getLogger, 100);
|
const store = new ClientMetricsStore(mock as any, ee, getLogger, 100);
|
||||||
jest.runAllTicks();
|
jest.runAllTicks();
|
||||||
|
|
||||||
const metrics = [];
|
const metrics = [];
|
||||||
@ -74,7 +73,7 @@ test('should poll for updates', done => {
|
|||||||
jest.useFakeTimers('modern');
|
jest.useFakeTimers('modern');
|
||||||
const mock = getMockDb();
|
const mock = getMockDb();
|
||||||
const ee = new EventEmitter();
|
const ee = new EventEmitter();
|
||||||
const store = new ClientMetricStore(mock, ee, getLogger, 100);
|
const store = new ClientMetricsStore(mock as any, ee, getLogger, 100);
|
||||||
jest.runAllTicks();
|
jest.runAllTicks();
|
||||||
|
|
||||||
const metrics = [];
|
const metrics = [];
|
@ -1,17 +1,31 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { EventEmitter } = require('events');
|
import EventEmitter from 'events';
|
||||||
const metricsHelper = require('../util/metrics-helper');
|
import { ClientMetricsDb, IClientMetric } from './client-metrics-db';
|
||||||
const { DB_TIME } = require('../metric-events');
|
import { Logger, LogProvider } from '../logger';
|
||||||
|
import metricsHelper from '../util/metrics-helper';
|
||||||
|
import { DB_TIME } from '../metric-events';
|
||||||
|
|
||||||
const TEN_SECONDS = 10 * 1000;
|
const TEN_SECONDS = 10 * 1000;
|
||||||
|
|
||||||
class ClientMetricsStore extends EventEmitter {
|
export class ClientMetricsStore extends EventEmitter {
|
||||||
constructor(metricsDb, eventBus, getLogger, pollInterval = TEN_SECONDS) {
|
private logger: Logger;
|
||||||
|
|
||||||
|
highestIdSeen = 0;
|
||||||
|
|
||||||
|
private startTimer: Function;
|
||||||
|
|
||||||
|
private timer: NodeJS.Timeout;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private metricsDb: ClientMetricsDb,
|
||||||
|
eventBus: EventEmitter,
|
||||||
|
getLogger: LogProvider,
|
||||||
|
pollInterval = TEN_SECONDS,
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
this.logger = getLogger('client-metrics-store.js');
|
this.logger = getLogger('client-metrics-store.js');
|
||||||
this.metricsDb = metricsDb;
|
this.metricsDb = metricsDb;
|
||||||
this.eventBus = eventBus;
|
|
||||||
this.highestIdSeen = 0;
|
this.highestIdSeen = 0;
|
||||||
|
|
||||||
this.startTimer = action =>
|
this.startTimer = action =>
|
||||||
@ -25,7 +39,7 @@ class ClientMetricsStore extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async _init(pollInterval) {
|
async _init(pollInterval: number): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const metrics = await this.metricsDb.getMetricsLastHour();
|
const metrics = await this.metricsDb.getMetricsLastHour();
|
||||||
this._emitMetrics(metrics);
|
this._emitMetrics(metrics);
|
||||||
@ -36,18 +50,18 @@ class ClientMetricsStore extends EventEmitter {
|
|||||||
this.emit('ready');
|
this.emit('ready');
|
||||||
}
|
}
|
||||||
|
|
||||||
_startPoller(pollInterval) {
|
_startPoller(pollInterval: number): void {
|
||||||
this.timer = setInterval(() => this._fetchNewAndEmit(), pollInterval);
|
this.timer = setInterval(() => this._fetchNewAndEmit(), pollInterval);
|
||||||
this.timer.unref();
|
this.timer.unref();
|
||||||
}
|
}
|
||||||
|
|
||||||
_fetchNewAndEmit() {
|
_fetchNewAndEmit(): void {
|
||||||
this.metricsDb
|
this.metricsDb
|
||||||
.getNewMetrics(this.highestIdSeen)
|
.getNewMetrics(this.highestIdSeen)
|
||||||
.then(metrics => this._emitMetrics(metrics));
|
.then(metrics => this._emitMetrics(metrics));
|
||||||
}
|
}
|
||||||
|
|
||||||
_emitMetrics(metrics) {
|
_emitMetrics(metrics: IClientMetric[]): void {
|
||||||
if (metrics && metrics.length > 0) {
|
if (metrics && metrics.length > 0) {
|
||||||
this.highestIdSeen = metrics[metrics.length - 1].id;
|
this.highestIdSeen = metrics[metrics.length - 1].id;
|
||||||
metrics.forEach(m => this.emit('metrics', m.metrics));
|
metrics.forEach(m => this.emit('metrics', m.metrics));
|
||||||
@ -55,7 +69,7 @@ class ClientMetricsStore extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Insert new client metrics
|
// Insert new client metrics
|
||||||
async insert(metrics) {
|
async insert(metrics: IClientMetric): Promise<void> {
|
||||||
const stopTimer = this.startTimer('insert');
|
const stopTimer = this.startTimer('insert');
|
||||||
|
|
||||||
await this.metricsDb.insert(metrics);
|
await this.metricsDb.insert(metrics);
|
||||||
@ -63,10 +77,8 @@ class ClientMetricsStore extends EventEmitter {
|
|||||||
stopTimer();
|
stopTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy(): void {
|
||||||
clearInterval(this.timer);
|
clearInterval(this.timer);
|
||||||
this.metricsDb.destroy();
|
this.metricsDb.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ClientMetricsStore;
|
|
@ -21,7 +21,7 @@ interface IEventTable {
|
|||||||
tags: [];
|
tags: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ICreateEvent {
|
export interface ICreateEvent {
|
||||||
type: string;
|
type: string;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
data?: any;
|
data?: any;
|
||||||
|
@ -11,8 +11,8 @@ import FeatureToggleStore from './feature-toggle-store';
|
|||||||
import FeatureTypeStore from './feature-type-store';
|
import FeatureTypeStore from './feature-type-store';
|
||||||
import StrategyStore from './strategy-store';
|
import StrategyStore from './strategy-store';
|
||||||
import ClientInstanceStore from './client-instance-store';
|
import ClientInstanceStore from './client-instance-store';
|
||||||
import ClientMetricsDb from './client-metrics-db';
|
import { ClientMetricsDb } from './client-metrics-db';
|
||||||
import ClientMetricsStore from './client-metrics-store';
|
import { ClientMetricsStore } from './client-metrics-store';
|
||||||
import ClientApplicationsStore from './client-applications-store';
|
import ClientApplicationsStore from './client-applications-store';
|
||||||
import ContextFieldStore from './context-field-store';
|
import ContextFieldStore from './context-field-store';
|
||||||
import SettingStore from './setting-store';
|
import SettingStore from './setting-store';
|
||||||
|
@ -106,7 +106,10 @@ class MetricsController extends Controller {
|
|||||||
|
|
||||||
async getApplications(req: Request, res: Response): Promise<void> {
|
async getApplications(req: Request, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const applications = await this.metrics.getApplications(req.query);
|
const query = req.query.strategyName
|
||||||
|
? { strategyName: req.query.strategyName as string }
|
||||||
|
: {};
|
||||||
|
const applications = await this.metrics.getApplications(query);
|
||||||
res.json({ applications });
|
res.json({ applications });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleErrors(res, this.logger, err);
|
handleErrors(res, this.logger, err);
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
'use strict';
|
import joi from 'joi';
|
||||||
|
|
||||||
const joi = require('joi');
|
|
||||||
|
|
||||||
const countSchema = joi
|
const countSchema = joi
|
||||||
.object()
|
.object()
|
||||||
@ -19,7 +17,7 @@ const countSchema = joi
|
|||||||
variants: joi.object().pattern(joi.string(), joi.number().min(0)),
|
variants: joi.object().pattern(joi.string(), joi.number().min(0)),
|
||||||
});
|
});
|
||||||
|
|
||||||
const clientMetricsSchema = joi
|
export const clientMetricsSchema = joi
|
||||||
.object()
|
.object()
|
||||||
.options({ stripUnknown: true })
|
.options({ stripUnknown: true })
|
||||||
.keys({
|
.keys({
|
||||||
@ -34,5 +32,3 @@ const clientMetricsSchema = joi
|
|||||||
toggles: joi.object().pattern(/.*/, countSchema),
|
toggles: joi.object().pattern(/.*/, countSchema),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = { clientMetricsSchema };
|
|
@ -1,21 +1,27 @@
|
|||||||
'use strict';
|
import EventEmitter from 'events';
|
||||||
|
import moment from 'moment';
|
||||||
const moment = require('moment');
|
import ClientMetricsService, { IClientApp } from './index';
|
||||||
|
import getLogger from '../../../test/fixtures/no-logger';
|
||||||
const { EventEmitter } = require('events');
|
|
||||||
const UnleashClientMetrics = require('./index');
|
|
||||||
|
|
||||||
const appName = 'appName';
|
const appName = 'appName';
|
||||||
const instanceId = 'instanceId';
|
const instanceId = 'instanceId';
|
||||||
|
|
||||||
const getLogger = require('../../../test/fixtures/no-logger');
|
const createMetricsService = cms =>
|
||||||
|
new ClientMetricsService(
|
||||||
|
{
|
||||||
|
clientMetricsStore: cms,
|
||||||
|
strategyStore: null,
|
||||||
|
featureToggleStore: null,
|
||||||
|
clientApplicationsStore: null,
|
||||||
|
clientInstanceStore: null,
|
||||||
|
eventStore: null,
|
||||||
|
},
|
||||||
|
{ getLogger },
|
||||||
|
);
|
||||||
|
|
||||||
test('should work without state', () => {
|
test('should work without state', () => {
|
||||||
const clientMetricsStore = new EventEmitter();
|
const clientMetricsStore = new EventEmitter();
|
||||||
const metrics = new UnleashClientMetrics(
|
const metrics = createMetricsService(clientMetricsStore);
|
||||||
{ clientMetricsStore },
|
|
||||||
{ getLogger },
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(metrics.getAppsWithToggles()).toBeTruthy();
|
expect(metrics.getAppsWithToggles()).toBeTruthy();
|
||||||
expect(metrics.getTogglesMetrics()).toBeTruthy();
|
expect(metrics.getTogglesMetrics()).toBeTruthy();
|
||||||
@ -27,10 +33,7 @@ test('data should expire', () => {
|
|||||||
jest.useFakeTimers('modern');
|
jest.useFakeTimers('modern');
|
||||||
|
|
||||||
const clientMetricsStore = new EventEmitter();
|
const clientMetricsStore = new EventEmitter();
|
||||||
const metrics = new UnleashClientMetrics(
|
const metrics = createMetricsService(clientMetricsStore);
|
||||||
{ clientMetricsStore },
|
|
||||||
{ getLogger },
|
|
||||||
);
|
|
||||||
|
|
||||||
metrics.addPayload({
|
metrics.addPayload({
|
||||||
appName,
|
appName,
|
||||||
@ -58,22 +61,20 @@ test('data should expire', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
jest.advanceTimersByTime(60 * 1000);
|
jest.advanceTimersByTime(60 * 1000);
|
||||||
expect(lastMinExpires === 1).toBe(true);
|
expect(lastMinExpires).toBe(1);
|
||||||
expect(lastHourExpires === 0).toBe(true);
|
expect(lastHourExpires).toBe(0);
|
||||||
|
|
||||||
jest.advanceTimersByTime(60 * 60 * 1000);
|
jest.advanceTimersByTime(60 * 60 * 1000);
|
||||||
expect(lastMinExpires === 1).toBe(true);
|
expect(lastMinExpires).toBe(1);
|
||||||
expect(lastHourExpires === 1).toBe(true);
|
expect(lastHourExpires).toBe(1);
|
||||||
|
|
||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
|
metrics.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should listen to metrics from store', () => {
|
test('should listen to metrics from store', () => {
|
||||||
const clientMetricsStore = new EventEmitter();
|
const clientMetricsStore = new EventEmitter();
|
||||||
const metrics = new UnleashClientMetrics(
|
const metrics = createMetricsService(clientMetricsStore);
|
||||||
{ clientMetricsStore },
|
|
||||||
{ getLogger },
|
|
||||||
);
|
|
||||||
clientMetricsStore.emit('metrics', {
|
clientMetricsStore.emit('metrics', {
|
||||||
appName,
|
appName,
|
||||||
instanceId,
|
instanceId,
|
||||||
@ -89,8 +90,8 @@ test('should listen to metrics from store', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(metrics.apps[appName].count === 123).toBeTruthy();
|
expect(metrics.apps[appName].count).toBe(123);
|
||||||
expect(metrics.globalCount === 123).toBeTruthy();
|
expect(metrics.globalCount).toBe(123);
|
||||||
|
|
||||||
expect(metrics.getTogglesMetrics().lastHour.toggleX).toEqual({
|
expect(metrics.getTogglesMetrics().lastHour.toggleX).toEqual({
|
||||||
yes: 123,
|
yes: 123,
|
||||||
@ -116,7 +117,7 @@ test('should listen to metrics from store', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(metrics.globalCount === 143).toBeTruthy();
|
expect(metrics.globalCount).toBe(143);
|
||||||
expect(metrics.getTogglesMetrics().lastHour.toggleX).toEqual({
|
expect(metrics.getTogglesMetrics().lastHour.toggleX).toEqual({
|
||||||
yes: 133,
|
yes: 133,
|
||||||
no: 10,
|
no: 10,
|
||||||
@ -131,10 +132,7 @@ test('should listen to metrics from store', () => {
|
|||||||
|
|
||||||
test('should build up list of seen toggles when new metrics arrives', () => {
|
test('should build up list of seen toggles when new metrics arrives', () => {
|
||||||
const clientMetricsStore = new EventEmitter();
|
const clientMetricsStore = new EventEmitter();
|
||||||
const metrics = new UnleashClientMetrics(
|
const metrics = createMetricsService(clientMetricsStore);
|
||||||
{ clientMetricsStore },
|
|
||||||
{ getLogger },
|
|
||||||
);
|
|
||||||
clientMetricsStore.emit('metrics', {
|
clientMetricsStore.emit('metrics', {
|
||||||
appName,
|
appName,
|
||||||
instanceId,
|
instanceId,
|
||||||
@ -157,12 +155,12 @@ test('should build up list of seen toggles when new metrics arrives', () => {
|
|||||||
const appToggles = metrics.getAppsWithToggles();
|
const appToggles = metrics.getAppsWithToggles();
|
||||||
const togglesForApp = metrics.getSeenTogglesByAppName(appName);
|
const togglesForApp = metrics.getSeenTogglesByAppName(appName);
|
||||||
|
|
||||||
expect(appToggles).toHaveLength(1);
|
expect(appToggles.length).toBe(1);
|
||||||
expect(appToggles[0].seenToggles).toHaveLength(2);
|
expect(appToggles[0].seenToggles.length).toBe(2);
|
||||||
expect(appToggles[0].seenToggles).toContain('toggleX');
|
expect(appToggles[0].seenToggles).toContain('toggleX');
|
||||||
expect(appToggles[0].seenToggles).toContain('toggleY');
|
expect(appToggles[0].seenToggles).toContain('toggleY');
|
||||||
|
|
||||||
expect(togglesForApp).toHaveLength(2);
|
expect(togglesForApp.length === 2);
|
||||||
expect(togglesForApp).toContain('toggleX');
|
expect(togglesForApp).toContain('toggleX');
|
||||||
expect(togglesForApp).toContain('toggleY');
|
expect(togglesForApp).toContain('toggleY');
|
||||||
metrics.destroy();
|
metrics.destroy();
|
||||||
@ -170,10 +168,7 @@ test('should build up list of seen toggles when new metrics arrives', () => {
|
|||||||
|
|
||||||
test('should handle a lot of toggles', () => {
|
test('should handle a lot of toggles', () => {
|
||||||
const clientMetricsStore = new EventEmitter();
|
const clientMetricsStore = new EventEmitter();
|
||||||
const metrics = new UnleashClientMetrics(
|
const metrics = createMetricsService(clientMetricsStore);
|
||||||
{ clientMetricsStore },
|
|
||||||
{ getLogger },
|
|
||||||
);
|
|
||||||
|
|
||||||
const toggleCounts = {};
|
const toggleCounts = {};
|
||||||
for (let i = 0; i < 100; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
@ -192,7 +187,7 @@ test('should handle a lot of toggles', () => {
|
|||||||
|
|
||||||
const seenToggles = metrics.getSeenTogglesByAppName(appName);
|
const seenToggles = metrics.getSeenTogglesByAppName(appName);
|
||||||
|
|
||||||
expect(seenToggles).toHaveLength(100);
|
expect(seenToggles.length).toBe(100);
|
||||||
metrics.destroy();
|
metrics.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -200,10 +195,7 @@ test('should have correct values for lastMinute', () => {
|
|||||||
jest.useFakeTimers('modern');
|
jest.useFakeTimers('modern');
|
||||||
|
|
||||||
const clientMetricsStore = new EventEmitter();
|
const clientMetricsStore = new EventEmitter();
|
||||||
const metrics = new UnleashClientMetrics(
|
const metrics = createMetricsService(clientMetricsStore);
|
||||||
{ clientMetricsStore },
|
|
||||||
{ getLogger },
|
|
||||||
);
|
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const input = [
|
const input = [
|
||||||
@ -253,7 +245,7 @@ test('should have correct values for lastMinute', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const seenToggles = metrics.getSeenTogglesByAppName(appName);
|
const seenToggles = metrics.getSeenTogglesByAppName(appName);
|
||||||
expect(seenToggles.length === 1).toBeTruthy();
|
expect(seenToggles.length).toBe(1);
|
||||||
|
|
||||||
// metrics.se
|
// metrics.se
|
||||||
let c = metrics.getTogglesMetrics();
|
let c = metrics.getTogglesMetrics();
|
||||||
@ -275,10 +267,7 @@ test('should have correct values for lastHour', () => {
|
|||||||
jest.useFakeTimers('modern');
|
jest.useFakeTimers('modern');
|
||||||
|
|
||||||
const clientMetricsStore = new EventEmitter();
|
const clientMetricsStore = new EventEmitter();
|
||||||
const metrics = new UnleashClientMetrics(
|
const metrics = createMetricsService(clientMetricsStore);
|
||||||
{ clientMetricsStore },
|
|
||||||
{ getLogger },
|
|
||||||
);
|
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const input = [
|
const input = [
|
||||||
@ -322,7 +311,7 @@ test('should have correct values for lastHour', () => {
|
|||||||
|
|
||||||
const seenToggles = metrics.getSeenTogglesByAppName(appName);
|
const seenToggles = metrics.getSeenTogglesByAppName(appName);
|
||||||
|
|
||||||
expect(seenToggles.length === 1).toBeTruthy();
|
expect(seenToggles.length).toBe(1);
|
||||||
|
|
||||||
// metrics.se
|
// metrics.se
|
||||||
let c = metrics.getTogglesMetrics();
|
let c = metrics.getTogglesMetrics();
|
||||||
@ -358,10 +347,7 @@ test('should have correct values for lastHour', () => {
|
|||||||
|
|
||||||
test('should not fail when toggle metrics is missing yes/no field', () => {
|
test('should not fail when toggle metrics is missing yes/no field', () => {
|
||||||
const clientMetricsStore = new EventEmitter();
|
const clientMetricsStore = new EventEmitter();
|
||||||
const metrics = new UnleashClientMetrics(
|
const metrics = createMetricsService(clientMetricsStore);
|
||||||
{ clientMetricsStore },
|
|
||||||
{ getLogger },
|
|
||||||
);
|
|
||||||
clientMetricsStore.emit('metrics', {
|
clientMetricsStore.emit('metrics', {
|
||||||
appName,
|
appName,
|
||||||
instanceId,
|
instanceId,
|
||||||
@ -403,20 +389,27 @@ test('should not fail when toggle metrics is missing yes/no field', () => {
|
|||||||
|
|
||||||
test('Multiple registrations of same appname and instanceid within same time period should only cause one registration', async () => {
|
test('Multiple registrations of same appname and instanceid within same time period should only cause one registration', async () => {
|
||||||
jest.useFakeTimers('modern');
|
jest.useFakeTimers('modern');
|
||||||
const clientMetricsStore = new EventEmitter();
|
const clientMetricsStore: any = new EventEmitter();
|
||||||
const appStoreSpy = jest.fn();
|
const appStoreSpy = jest.fn();
|
||||||
const bulkSpy = jest.fn();
|
const bulkSpy = jest.fn();
|
||||||
const clientApplicationsStore = {
|
const clientApplicationsStore: any = {
|
||||||
bulkUpsert: appStoreSpy,
|
bulkUpsert: appStoreSpy,
|
||||||
};
|
};
|
||||||
const clientInstanceStore = {
|
const clientInstanceStore: any = {
|
||||||
bulkUpsert: bulkSpy,
|
bulkUpsert: bulkSpy,
|
||||||
};
|
};
|
||||||
const clientMetrics = new UnleashClientMetrics(
|
const clientMetrics = new ClientMetricsService(
|
||||||
{ clientMetricsStore, clientApplicationsStore, clientInstanceStore },
|
{
|
||||||
|
clientMetricsStore,
|
||||||
|
strategyStore: null,
|
||||||
|
featureToggleStore: null,
|
||||||
|
clientApplicationsStore,
|
||||||
|
clientInstanceStore,
|
||||||
|
eventStore: null,
|
||||||
|
},
|
||||||
{ getLogger },
|
{ getLogger },
|
||||||
);
|
);
|
||||||
const client1 = {
|
const client1: IClientApp = {
|
||||||
appName: 'test_app',
|
appName: 'test_app',
|
||||||
instanceId: 'ava',
|
instanceId: 'ava',
|
||||||
strategies: [{ name: 'defaullt' }],
|
strategies: [{ name: 'defaullt' }],
|
||||||
@ -427,30 +420,42 @@ test('Multiple registrations of same appname and instanceid within same time per
|
|||||||
await clientMetrics.registerClient(client1, '127.0.0.1');
|
await clientMetrics.registerClient(client1, '127.0.0.1');
|
||||||
await clientMetrics.registerClient(client1, '127.0.0.1');
|
await clientMetrics.registerClient(client1, '127.0.0.1');
|
||||||
await clientMetrics.registerClient(client1, '127.0.0.1');
|
await clientMetrics.registerClient(client1, '127.0.0.1');
|
||||||
jest.advanceTimersByTime(7 * 1000);
|
await jest.advanceTimersByTime(7 * 1000);
|
||||||
|
|
||||||
expect(appStoreSpy).toHaveBeenCalledTimes(1);
|
expect(appStoreSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(bulkSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
const registrations = appStoreSpy.mock.calls[0][0];
|
const registrations = appStoreSpy.mock.calls[0][0];
|
||||||
expect(registrations).toHaveLength(1);
|
|
||||||
|
expect(registrations.length).toBe(1);
|
||||||
expect(registrations[0].appName).toBe(client1.appName);
|
expect(registrations[0].appName).toBe(client1.appName);
|
||||||
expect(registrations[0].instanceId).toBe(client1.instanceId);
|
expect(registrations[0].instanceId).toBe(client1.instanceId);
|
||||||
expect(registrations[0].started).toBe(client1.started);
|
expect(registrations[0].started).toBe(client1.started);
|
||||||
expect(registrations[0].interval).toBe(client1.interval);
|
expect(registrations[0].interval).toBe(client1.interval);
|
||||||
|
|
||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Multiple unique clients causes multiple registrations', async () => {
|
test('Multiple unique clients causes multiple registrations', async () => {
|
||||||
jest.useFakeTimers('modern');
|
jest.useFakeTimers('modern');
|
||||||
const clientMetricsStore = new EventEmitter();
|
const clientMetricsStore: any = new EventEmitter();
|
||||||
const appStoreSpy = jest.fn();
|
const appStoreSpy = jest.fn();
|
||||||
const bulkSpy = jest.fn();
|
const bulkSpy = jest.fn();
|
||||||
const clientApplicationsStore = {
|
const clientApplicationsStore: any = {
|
||||||
bulkUpsert: appStoreSpy,
|
bulkUpsert: appStoreSpy,
|
||||||
};
|
};
|
||||||
const clientInstanceStore = {
|
const clientInstanceStore: any = {
|
||||||
bulkUpsert: bulkSpy,
|
bulkUpsert: bulkSpy,
|
||||||
};
|
};
|
||||||
const clientMetrics = new UnleashClientMetrics(
|
const clientMetrics = new ClientMetricsService(
|
||||||
{ clientMetricsStore, clientApplicationsStore, clientInstanceStore },
|
{
|
||||||
|
clientMetricsStore,
|
||||||
|
strategyStore: null,
|
||||||
|
featureToggleStore: null,
|
||||||
|
clientApplicationsStore,
|
||||||
|
clientInstanceStore,
|
||||||
|
eventStore: null,
|
||||||
|
},
|
||||||
{ getLogger },
|
{ getLogger },
|
||||||
);
|
);
|
||||||
const client1 = {
|
const client1 = {
|
||||||
@ -473,70 +478,92 @@ test('Multiple unique clients causes multiple registrations', async () => {
|
|||||||
await clientMetrics.registerClient(client2, '127.0.0.1');
|
await clientMetrics.registerClient(client2, '127.0.0.1');
|
||||||
await clientMetrics.registerClient(client2, '127.0.0.1');
|
await clientMetrics.registerClient(client2, '127.0.0.1');
|
||||||
await clientMetrics.registerClient(client2, '127.0.0.1');
|
await clientMetrics.registerClient(client2, '127.0.0.1');
|
||||||
jest.advanceTimersByTime(7 * 1000);
|
await jest.advanceTimersByTime(7 * 1000);
|
||||||
|
|
||||||
expect(appStoreSpy).toHaveBeenCalledTimes(1);
|
expect(appStoreSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(bulkSpy).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
const registrations = appStoreSpy.mock.calls[0][0];
|
const registrations = appStoreSpy.mock.calls[0][0];
|
||||||
expect(registrations).toHaveLength(2);
|
|
||||||
|
expect(registrations.length).toBe(2);
|
||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
test('Same client registered outside of dedup interval will be registered twice', async () => {
|
test('Same client registered outside of dedup interval will be registered twice', async () => {
|
||||||
jest.useFakeTimers('modern');
|
jest.useFakeTimers('modern');
|
||||||
const clientMetricsStore = new EventEmitter();
|
const clientMetricsStore: any = new EventEmitter();
|
||||||
const appStoreSpy = jest.fn();
|
const appStoreSpy = jest.fn();
|
||||||
const bulkSpy = jest.fn();
|
const bulkSpy = jest.fn();
|
||||||
const clientApplicationsStore = {
|
const clientApplicationsStore: any = {
|
||||||
bulkUpsert: appStoreSpy,
|
bulkUpsert: appStoreSpy,
|
||||||
};
|
};
|
||||||
const clientInstanceStore = {
|
const clientInstanceStore: any = {
|
||||||
bulkUpsert: bulkSpy,
|
bulkUpsert: bulkSpy,
|
||||||
};
|
};
|
||||||
|
|
||||||
const bulkInterval = 2000;
|
const bulkInterval = 2000;
|
||||||
const clientMetrics = new UnleashClientMetrics(
|
|
||||||
{ clientMetricsStore, clientApplicationsStore, clientInstanceStore },
|
const clientMetrics = new ClientMetricsService(
|
||||||
{ getLogger, bulkInterval },
|
{
|
||||||
|
clientMetricsStore,
|
||||||
|
strategyStore: null,
|
||||||
|
featureToggleStore: null,
|
||||||
|
clientApplicationsStore,
|
||||||
|
clientInstanceStore,
|
||||||
|
eventStore: null,
|
||||||
|
},
|
||||||
|
{ getLogger },
|
||||||
|
bulkInterval,
|
||||||
);
|
);
|
||||||
const client1 = {
|
const client1 = {
|
||||||
appName: 'test_app',
|
appName: 'test_app',
|
||||||
instanceId: 'client1',
|
instanceId: 'client1',
|
||||||
strategies: [{ name: 'default' }],
|
strategies: [{ name: 'defaullt' }],
|
||||||
started: new Date(),
|
started: new Date(),
|
||||||
interval: 10,
|
interval: 10,
|
||||||
};
|
};
|
||||||
await clientMetrics.registerClient(client1, '127.0.0.1');
|
await clientMetrics.registerClient(client1, '127.0.0.1');
|
||||||
await clientMetrics.registerClient(client1, '127.0.0.1');
|
await clientMetrics.registerClient(client1, '127.0.0.1');
|
||||||
await clientMetrics.registerClient(client1, '127.0.0.1');
|
await clientMetrics.registerClient(client1, '127.0.0.1');
|
||||||
jest.advanceTimersByTime(3 * 1000);
|
await jest.advanceTimersByTime(3 * 1000);
|
||||||
await clientMetrics.registerClient(client1, '127.0.0.1');
|
await clientMetrics.registerClient(client1, '127.0.0.1');
|
||||||
await clientMetrics.registerClient(client1, '127.0.0.1');
|
await clientMetrics.registerClient(client1, '127.0.0.1');
|
||||||
await clientMetrics.registerClient(client1, '127.0.0.1');
|
await clientMetrics.registerClient(client1, '127.0.0.1');
|
||||||
jest.advanceTimersByTime(3 * 1000);
|
await jest.advanceTimersByTime(3 * 1000);
|
||||||
expect(appStoreSpy).toHaveBeenCalledTimes(2);
|
expect(appStoreSpy).toHaveBeenCalledTimes(2);
|
||||||
const firstRegistrations = appStoreSpy.mock.calls[0][0];
|
expect(bulkSpy).toHaveBeenCalledTimes(2);
|
||||||
const secondRegistrations = appStoreSpy.mock.calls[1][0];
|
|
||||||
expect(firstRegistrations[0].appName).toBe(secondRegistrations[0].appName);
|
const firstRegistrations = appStoreSpy.mock.calls[0][0][0];
|
||||||
expect(firstRegistrations[0].instanceId).toBe(
|
const secondRegistrations = appStoreSpy.mock.calls[1][0][0];
|
||||||
secondRegistrations[0].instanceId,
|
|
||||||
);
|
expect(firstRegistrations.appName).toBe(secondRegistrations.appName);
|
||||||
|
expect(firstRegistrations.instanceId).toBe(secondRegistrations.instanceId);
|
||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('No registrations during a time period will not call stores', async () => {
|
test('No registrations during a time period will not call stores', async () => {
|
||||||
jest.useFakeTimers('modern');
|
jest.useFakeTimers('modern');
|
||||||
const clientMetricsStore = new EventEmitter();
|
const clientMetricsStore: any = new EventEmitter();
|
||||||
const appStoreSpy = jest.fn();
|
const appStoreSpy = jest.fn();
|
||||||
const bulkSpy = jest.fn();
|
const bulkSpy = jest.fn();
|
||||||
const clientApplicationsStore = {
|
const clientApplicationsStore: any = {
|
||||||
bulkUpsert: appStoreSpy,
|
bulkUpsert: appStoreSpy,
|
||||||
};
|
};
|
||||||
const clientInstanceStore = {
|
const clientInstanceStore: any = {
|
||||||
bulkUpsert: bulkSpy,
|
bulkUpsert: bulkSpy,
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const metrics = new UnleashClientMetrics(
|
const clientMetrics = new ClientMetricsService(
|
||||||
{ clientMetricsStore, clientApplicationsStore, clientInstanceStore },
|
{
|
||||||
|
clientMetricsStore,
|
||||||
|
strategyStore: null,
|
||||||
|
featureToggleStore: null,
|
||||||
|
clientApplicationsStore,
|
||||||
|
clientInstanceStore,
|
||||||
|
eventStore: null,
|
||||||
|
},
|
||||||
{ getLogger },
|
{ getLogger },
|
||||||
);
|
);
|
||||||
jest.advanceTimersByTime(6 * 1000);
|
await jest.advanceTimersByTime(6 * 1000);
|
||||||
expect(appStoreSpy).toHaveBeenCalledTimes(0);
|
expect(appStoreSpy).toHaveBeenCalledTimes(0);
|
||||||
expect(bulkSpy).toHaveBeenCalledTimes(0);
|
expect(bulkSpy).toHaveBeenCalledTimes(0);
|
||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
@ -1,54 +1,148 @@
|
|||||||
/* eslint-disable no-param-reassign */
|
/* eslint-disable no-param-reassign */
|
||||||
|
import EventStore, { ICreateEvent } from '../../db/event-store';
|
||||||
|
import StrategyStore from '../../db/strategy-store';
|
||||||
|
import ClientApplicationsDb from '../../db/client-applications-store';
|
||||||
|
import ClientInstanceStore from '../../db/client-instance-store';
|
||||||
|
import { ClientMetricsStore } from '../../db/client-metrics-store';
|
||||||
|
import FeatureToggleStore from '../../db/feature-toggle-store';
|
||||||
|
import { LogProvider } from '../../logger';
|
||||||
|
import { applicationSchema } from './metrics-schema';
|
||||||
|
import { Projection } from './projection';
|
||||||
|
import { clientMetricsSchema } from './client-metrics-schema';
|
||||||
|
import { APPLICATION_CREATED } from '../../types/events';
|
||||||
|
import { IApplication, IYesNoCount } from './models';
|
||||||
|
import { IUnleashStores } from '../../types/stores';
|
||||||
|
import { IUnleashConfig } from '../../types/option';
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const Projection = require('./projection');
|
|
||||||
const TTLList = require('./ttl-list');
|
const TTLList = require('./ttl-list');
|
||||||
const appSchema = require('./metrics-schema');
|
|
||||||
const { clientMetricsSchema } = require('./client-metrics-schema');
|
|
||||||
const { clientRegisterSchema } = require('./register-schema');
|
const { clientRegisterSchema } = require('./register-schema');
|
||||||
const { APPLICATION_CREATED } = require('../../types/events');
|
|
||||||
|
|
||||||
const FIVE_SECONDS = 5 * 1000;
|
const FIVE_SECONDS = 5 * 1000;
|
||||||
const FIVE_MINUTES = 5 * 60 * 1000;
|
const FIVE_MINUTES = 5 * 60 * 1000;
|
||||||
|
|
||||||
module.exports = class ClientMetricsService {
|
export interface IClientApp {
|
||||||
|
appName: string;
|
||||||
|
instanceId: string;
|
||||||
|
clientIp?: string;
|
||||||
|
seenToggles?: string[];
|
||||||
|
metricsCount?: number;
|
||||||
|
strategies?: string[] | Record<string, string>[];
|
||||||
|
bucket?: any;
|
||||||
|
count?: number;
|
||||||
|
started?: number | Date;
|
||||||
|
interval?: number;
|
||||||
|
icon?: string;
|
||||||
|
description?: string;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAppFeature {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
type: string;
|
||||||
|
project: string;
|
||||||
|
enabled: boolean;
|
||||||
|
stale: boolean;
|
||||||
|
strategies: any;
|
||||||
|
variants: any[];
|
||||||
|
createdAt: Date;
|
||||||
|
lastSeenAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IApplicationQuery {
|
||||||
|
strategyName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAppName {
|
||||||
|
appName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMetricCounts {
|
||||||
|
yes?: number;
|
||||||
|
no?: number;
|
||||||
|
variants?: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMetricsBucket {
|
||||||
|
start: Date;
|
||||||
|
stop: Date;
|
||||||
|
toggles: IMetricCounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ClientMetricsService {
|
||||||
|
globalCount = 0;
|
||||||
|
|
||||||
|
apps = {};
|
||||||
|
|
||||||
|
lastHourProjection = new Projection();
|
||||||
|
|
||||||
|
lastMinuteProjection = new Projection();
|
||||||
|
|
||||||
|
lastHourList = new TTLList({
|
||||||
|
interval: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger = null;
|
||||||
|
|
||||||
|
lastMinuteList = new TTLList({
|
||||||
|
interval: 10000,
|
||||||
|
expireType: 'minutes',
|
||||||
|
expireAmount: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
seenClients: Record<string, IClientApp> = {};
|
||||||
|
|
||||||
|
private timers: NodeJS.Timeout[] = [];
|
||||||
|
|
||||||
|
private clientMetricsStore: ClientMetricsStore;
|
||||||
|
|
||||||
|
private strategyStore: StrategyStore;
|
||||||
|
|
||||||
|
private featureToggleStore: FeatureToggleStore;
|
||||||
|
|
||||||
|
private clientApplicationsStore: ClientApplicationsDb;
|
||||||
|
|
||||||
|
private clientInstanceStore: ClientInstanceStore;
|
||||||
|
|
||||||
|
private eventStore: EventStore;
|
||||||
|
|
||||||
|
private getLogger: LogProvider;
|
||||||
|
|
||||||
|
private bulkInterval: number;
|
||||||
|
|
||||||
|
private announcementInterval: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
{
|
{
|
||||||
clientMetricsStore,
|
clientMetricsStore,
|
||||||
strategyStore,
|
strategyStore,
|
||||||
featureToggleStore,
|
featureToggleStore,
|
||||||
clientApplicationsStore,
|
|
||||||
clientInstanceStore,
|
clientInstanceStore,
|
||||||
|
clientApplicationsStore,
|
||||||
eventStore,
|
eventStore,
|
||||||
},
|
}: Pick<IUnleashStores,
|
||||||
{
|
| 'clientMetricsStore'
|
||||||
getLogger,
|
| 'strategyStore'
|
||||||
bulkInterval = FIVE_SECONDS,
|
| 'featureToggleStore'
|
||||||
announcementInterval: appAnnouncementInterval = FIVE_MINUTES,
|
| 'clientApplicationsStore'
|
||||||
},
|
| 'clientInstanceStore'
|
||||||
|
| 'eventStore'
|
||||||
|
>,
|
||||||
|
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
||||||
|
bulkInterval = FIVE_SECONDS,
|
||||||
|
announcementInterval = FIVE_MINUTES,
|
||||||
) {
|
) {
|
||||||
this.globalCount = 0;
|
|
||||||
this.apps = {};
|
|
||||||
this.strategyStore = strategyStore;
|
|
||||||
this.toggleStore = featureToggleStore;
|
|
||||||
this.clientAppStore = clientApplicationsStore;
|
|
||||||
this.clientInstanceStore = clientInstanceStore;
|
|
||||||
this.clientMetricsStore = clientMetricsStore;
|
this.clientMetricsStore = clientMetricsStore;
|
||||||
this.lastHourProjection = new Projection();
|
this.strategyStore = strategyStore;
|
||||||
this.lastMinuteProjection = new Projection();
|
this.featureToggleStore = featureToggleStore;
|
||||||
|
this.clientApplicationsStore = clientApplicationsStore;
|
||||||
|
this.clientInstanceStore = clientInstanceStore;
|
||||||
this.eventStore = eventStore;
|
this.eventStore = eventStore;
|
||||||
|
|
||||||
this.lastHourList = new TTLList({
|
this.logger = getLogger('/services/client-metrics/index.ts');
|
||||||
interval: 10000,
|
|
||||||
});
|
|
||||||
this.logger = getLogger('services/client-metrics/index.ts');
|
|
||||||
|
|
||||||
this.lastMinuteList = new TTLList({
|
this.bulkInterval = bulkInterval;
|
||||||
interval: 10000,
|
this.announcementInterval = announcementInterval;
|
||||||
expireType: 'minutes',
|
|
||||||
expireAmount: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.lastHourList.on('expire', toggles => {
|
this.lastHourList.on('expire', toggles => {
|
||||||
Object.keys(toggles).forEach(toggleName => {
|
Object.keys(toggles).forEach(toggleName => {
|
||||||
@ -66,21 +160,26 @@ module.exports = class ClientMetricsService {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.seenClients = {};
|
|
||||||
this.bulkAddTimer = setInterval(() => this.bulkAdd(), bulkInterval);
|
this.timers.push(
|
||||||
this.bulkAddTimer.unref();
|
setInterval(() => this.bulkAdd(), this.bulkInterval).unref(),
|
||||||
this.announceTimer = setInterval(
|
);
|
||||||
() => this.announceUnannounced(),
|
this.timers.push(
|
||||||
appAnnouncementInterval,
|
setInterval(
|
||||||
|
() => this.announceUnannounced(),
|
||||||
|
this.announcementInterval,
|
||||||
|
).unref(),
|
||||||
);
|
);
|
||||||
this.announceTimer.unref();
|
|
||||||
clientMetricsStore.on('metrics', m => this.addPayload(m));
|
clientMetricsStore.on('metrics', m => this.addPayload(m));
|
||||||
}
|
}
|
||||||
|
|
||||||
async registerClientMetrics(data, clientIp) {
|
async registerClientMetrics(
|
||||||
|
data: IClientApp,
|
||||||
|
clientIp: string,
|
||||||
|
): Promise<void> {
|
||||||
const value = await clientMetricsSchema.validateAsync(data);
|
const value = await clientMetricsSchema.validateAsync(data);
|
||||||
const toggleNames = Object.keys(value.bucket.toggles);
|
const toggleNames = Object.keys(value.bucket.toggles);
|
||||||
await this.toggleStore.lastSeenToggles(toggleNames);
|
await this.featureToggleStore.lastSeenToggles(toggleNames);
|
||||||
await this.clientMetricsStore.insert(value);
|
await this.clientMetricsStore.insert(value);
|
||||||
await this.clientInstanceStore.insert({
|
await this.clientInstanceStore.insert({
|
||||||
appName: value.appName,
|
appName: value.appName,
|
||||||
@ -89,40 +188,36 @@ module.exports = class ClientMetricsService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async announceUnannounced() {
|
async announceUnannounced(): Promise<void> {
|
||||||
if (this.clientAppStore) {
|
if (this.clientApplicationsStore) {
|
||||||
try {
|
const appsToAnnounce = await this.clientApplicationsStore.setUnannouncedToAnnounced();
|
||||||
const appsToAnnounce = await this.clientAppStore.setUnannouncedToAnnounced();
|
if (appsToAnnounce.length > 0) {
|
||||||
if (appsToAnnounce.length > 0) {
|
const events = appsToAnnounce.map(app => ({
|
||||||
const events = appsToAnnounce.map(app => ({
|
type: APPLICATION_CREATED,
|
||||||
type: APPLICATION_CREATED,
|
createdBy: app.createdBy || 'unknown',
|
||||||
createdBy: app.createdBy || 'unknown',
|
data: app,
|
||||||
data: app,
|
}));
|
||||||
}));
|
await this.eventStore.batchStore(events);
|
||||||
await this.eventStore.batchStore(events);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.warn(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async registerClient(data, clientIp) {
|
async registerClient(data: IClientApp, clientIp: string): Promise<void> {
|
||||||
const value = await clientRegisterSchema.validateAsync(data);
|
const value = await clientRegisterSchema.validateAsync(data);
|
||||||
value.clientIp = clientIp;
|
value.clientIp = clientIp;
|
||||||
value.createdBy = clientIp;
|
value.createdBy = clientIp;
|
||||||
this.seenClients[this.clientKey(value)] = value;
|
this.seenClients[this.clientKey(value)] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
clientKey(client) {
|
clientKey(client: IClientApp): string {
|
||||||
return `${client.appName}_${client.instanceId}`;
|
return `${client.appName}_${client.instanceId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkAdd() {
|
async bulkAdd(): Promise<void> {
|
||||||
if (
|
if (
|
||||||
this &&
|
this &&
|
||||||
this.seenClients &&
|
this.seenClients &&
|
||||||
this.clientAppStore &&
|
this.clientApplicationsStore &&
|
||||||
this.clientInstanceStore
|
this.clientInstanceStore
|
||||||
) {
|
) {
|
||||||
const uniqueRegistrations = Object.values(this.seenClients);
|
const uniqueRegistrations = Object.values(this.seenClients);
|
||||||
@ -135,7 +230,7 @@ module.exports = class ClientMetricsService {
|
|||||||
this.seenClients = {};
|
this.seenClients = {};
|
||||||
try {
|
try {
|
||||||
if (uniqueRegistrations.length > 0) {
|
if (uniqueRegistrations.length > 0) {
|
||||||
await this.clientAppStore.bulkUpsert(uniqueApps);
|
await this.clientApplicationsStore.bulkUpsert(uniqueApps);
|
||||||
await this.clientInstanceStore.bulkUpsert(
|
await this.clientInstanceStore.bulkUpsert(
|
||||||
uniqueRegistrations,
|
uniqueRegistrations,
|
||||||
);
|
);
|
||||||
@ -146,7 +241,7 @@ module.exports = class ClientMetricsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appToEvent(app) {
|
appToEvent(app: IClientApp): ICreateEvent {
|
||||||
return {
|
return {
|
||||||
type: APPLICATION_CREATED,
|
type: APPLICATION_CREATED,
|
||||||
createdBy: app.clientIp,
|
createdBy: app.clientIp,
|
||||||
@ -154,7 +249,7 @@ module.exports = class ClientMetricsService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getAppsWithToggles() {
|
getAppsWithToggles(): IClientApp[] {
|
||||||
const apps = [];
|
const apps = [];
|
||||||
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);
|
||||||
@ -164,15 +259,15 @@ module.exports = class ClientMetricsService {
|
|||||||
return apps;
|
return apps;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSeenTogglesByAppName(appName) {
|
getSeenTogglesByAppName(appName: string): string[] {
|
||||||
return this.apps[appName]
|
return this.apps[appName]
|
||||||
? Object.keys(this.apps[appName].seenToggles)
|
? Object.keys(this.apps[appName].seenToggles)
|
||||||
: [];
|
: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSeenApps() {
|
async getSeenApps(): Promise<Record<string, IApplication[]>> {
|
||||||
const seenApps = this.getSeenAppsPerToggle();
|
const seenApps = this.getSeenAppsPerToggle();
|
||||||
const applications = await this.clientAppStore.getApplications();
|
const applications: IApplication[] = await this.clientApplicationsStore.getApplications();
|
||||||
const metaData = applications.reduce((result, entry) => {
|
const metaData = applications.reduce((result, entry) => {
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
result[entry.appName] = entry;
|
result[entry.appName] = entry;
|
||||||
@ -190,11 +285,13 @@ module.exports = class ClientMetricsService {
|
|||||||
return seenApps;
|
return seenApps;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getApplications(query) {
|
async getApplications(
|
||||||
return this.clientAppStore.getApplications(query);
|
query: IApplicationQuery,
|
||||||
|
): Promise<Record<string, IApplication>> {
|
||||||
|
return this.clientApplicationsStore.getApplications(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getApplication(appName) {
|
async getApplication(appName: string): Promise<IApplication> {
|
||||||
const seenToggles = this.getSeenTogglesByAppName(appName);
|
const seenToggles = this.getSeenTogglesByAppName(appName);
|
||||||
const [
|
const [
|
||||||
application,
|
application,
|
||||||
@ -202,10 +299,10 @@ module.exports = class ClientMetricsService {
|
|||||||
strategies,
|
strategies,
|
||||||
features,
|
features,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.clientAppStore.getApplication(appName),
|
this.clientApplicationsStore.getApplication(appName),
|
||||||
this.clientInstanceStore.getByAppName(appName),
|
this.clientInstanceStore.getByAppName(appName),
|
||||||
this.strategyStore.getStrategies(),
|
this.strategyStore.getStrategies(),
|
||||||
this.toggleStore.getFeatures(),
|
this.featureToggleStore.getFeatures(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -230,7 +327,7 @@ module.exports = class ClientMetricsService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getSeenAppsPerToggle() {
|
getSeenAppsPerToggle(): Record<string, IApplication[]> {
|
||||||
const toggles = {};
|
const toggles = {};
|
||||||
Object.keys(this.apps).forEach(appName => {
|
Object.keys(this.apps).forEach(appName => {
|
||||||
Object.keys(this.apps[appName].seenToggles).forEach(
|
Object.keys(this.apps[appName].seenToggles).forEach(
|
||||||
@ -245,20 +342,20 @@ module.exports = class ClientMetricsService {
|
|||||||
return toggles;
|
return toggles;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTogglesMetrics() {
|
getTogglesMetrics(): Record<string, Record<string, IYesNoCount>> {
|
||||||
return {
|
return {
|
||||||
lastHour: this.lastHourProjection.getProjection(),
|
lastHour: this.lastHourProjection.getProjection(),
|
||||||
lastMinute: this.lastMinuteProjection.getProjection(),
|
lastMinute: this.lastMinuteProjection.getProjection(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
addPayload(data) {
|
addPayload(data: IClientApp): void {
|
||||||
const { appName, bucket } = data;
|
const { appName, bucket } = data;
|
||||||
const app = this.getApp(appName);
|
const app = this.getApp(appName);
|
||||||
this.addBucket(app, bucket);
|
this.addBucket(app, bucket);
|
||||||
}
|
}
|
||||||
|
|
||||||
getApp(appName) {
|
getApp(appName: string): IClientApp {
|
||||||
this.apps[appName] = this.apps[appName] || {
|
this.apps[appName] = this.apps[appName] || {
|
||||||
seenToggles: {},
|
seenToggles: {},
|
||||||
count: 0,
|
count: 0,
|
||||||
@ -266,7 +363,7 @@ module.exports = class ClientMetricsService {
|
|||||||
return this.apps[appName];
|
return this.apps[appName];
|
||||||
}
|
}
|
||||||
|
|
||||||
createCountObject(entry) {
|
createCountObject(entry: IMetricCounts): IYesNoCount {
|
||||||
let yes = typeof entry.yes === 'number' ? entry.yes : 0;
|
let yes = typeof entry.yes === 'number' ? entry.yes : 0;
|
||||||
let no = typeof entry.no === 'number' ? entry.no : 0;
|
let no = typeof entry.no === 'number' ? entry.no : 0;
|
||||||
|
|
||||||
@ -283,7 +380,7 @@ module.exports = class ClientMetricsService {
|
|||||||
return { yes, no };
|
return { yes, no };
|
||||||
}
|
}
|
||||||
|
|
||||||
addBucket(app, bucket) {
|
addBucket(app: IClientApp, bucket: IMetricsBucket): void {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
// TODO stop should be createdAt
|
// TODO stop should be createdAt
|
||||||
const { stop, toggles } = bucket;
|
const { stop, toggles } = bucket;
|
||||||
@ -305,26 +402,25 @@ module.exports = class ClientMetricsService {
|
|||||||
this.addSeenToggles(app, toggleNames);
|
this.addSeenToggles(app, toggleNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
addSeenToggles(app, toggleNames) {
|
addSeenToggles(app: IClientApp, toggleNames: string[]): void {
|
||||||
toggleNames.forEach(t => {
|
toggleNames.forEach(t => {
|
||||||
app.seenToggles[t] = true;
|
app.seenToggles[t] = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteApplication(appName) {
|
async deleteApplication(appName: string): Promise<void> {
|
||||||
await this.clientInstanceStore.deleteForApplication(appName);
|
await this.clientInstanceStore.deleteForApplication(appName);
|
||||||
await this.clientAppStore.deleteApplication(appName);
|
await this.clientApplicationsStore.deleteApplication(appName);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createApplication(input) {
|
async createApplication(input: IApplication): Promise<void> {
|
||||||
const applicationData = await appSchema.validateAsync(input);
|
const applicationData = await applicationSchema.validateAsync(input);
|
||||||
await this.clientAppStore.upsert(applicationData);
|
await this.clientApplicationsStore.upsert(applicationData);
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy(): void {
|
||||||
this.lastHourList.destroy();
|
this.lastHourList.destroy();
|
||||||
this.lastMinuteList.destroy();
|
this.lastMinuteList.destroy();
|
||||||
clearInterval(this.announceTimer);
|
this.timers.forEach(clearInterval);
|
||||||
clearInterval(this.bulkAddTimer);
|
|
||||||
}
|
}
|
||||||
};
|
}
|
@ -1,8 +1,6 @@
|
|||||||
'use strict';
|
import joi from 'joi';
|
||||||
|
|
||||||
const joi = require('joi');
|
export const applicationSchema = joi
|
||||||
|
|
||||||
const applicationSchema = joi
|
|
||||||
.object()
|
.object()
|
||||||
.options({ stripUnknown: false })
|
.options({ stripUnknown: false })
|
||||||
.keys({
|
.keys({
|
||||||
@ -29,5 +27,3 @@ const applicationSchema = joi
|
|||||||
.allow('')
|
.allow('')
|
||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = applicationSchema;
|
|
27
src/lib/services/client-metrics/models.ts
Normal file
27
src/lib/services/client-metrics/models.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
export interface IYesNoCount {
|
||||||
|
yes: number;
|
||||||
|
no: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAppInstance {
|
||||||
|
appName: string;
|
||||||
|
instanceId: string;
|
||||||
|
sdkVersion: string;
|
||||||
|
clientIp: string;
|
||||||
|
lastSeen: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IApplication {
|
||||||
|
appName: string;
|
||||||
|
sdkVersion?: string;
|
||||||
|
strategies?: string[] | any[];
|
||||||
|
description?: string;
|
||||||
|
url?: string;
|
||||||
|
color?: string;
|
||||||
|
icon?: string;
|
||||||
|
createdAt: Date;
|
||||||
|
instances?: IAppInstance;
|
||||||
|
seenToggles: Record<string, any>;
|
||||||
|
links: Record<string, string>;
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
'use strict';
|
import { Projection } from './projection';
|
||||||
|
|
||||||
const Projection = require('./projection');
|
|
||||||
|
|
||||||
test('should return set empty if missing', () => {
|
test('should return set empty if missing', () => {
|
||||||
const projection = new Projection();
|
const projection = new Projection();
|
@ -1,15 +1,13 @@
|
|||||||
'use strict';
|
import { IYesNoCount } from './models';
|
||||||
|
|
||||||
module.exports = class Projection {
|
export class Projection {
|
||||||
constructor() {
|
store: Record<string, IYesNoCount> = {};
|
||||||
this.store = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
getProjection() {
|
getProjection(): Record<string, IYesNoCount> {
|
||||||
return this.store;
|
return this.store;
|
||||||
}
|
}
|
||||||
|
|
||||||
add(name, countObj) {
|
add(name: string, countObj: IYesNoCount): void {
|
||||||
if (this.store[name]) {
|
if (this.store[name]) {
|
||||||
this.store[name].yes += countObj.yes;
|
this.store[name].yes += countObj.yes;
|
||||||
this.store[name].no += countObj.no;
|
this.store[name].no += countObj.no;
|
||||||
@ -21,7 +19,7 @@ module.exports = class Projection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
substract(name, countObj) {
|
substract(name: string, countObj: IYesNoCount): void {
|
||||||
if (this.store[name]) {
|
if (this.store[name]) {
|
||||||
this.store[name].yes -= countObj.yes;
|
this.store[name].yes -= countObj.yes;
|
||||||
this.store[name].no -= countObj.no;
|
this.store[name].no -= countObj.no;
|
||||||
@ -32,4 +30,4 @@ module.exports = class Projection {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
@ -21,7 +21,6 @@ import { IUnleashStores } from '../types/stores';
|
|||||||
import PasswordUndefinedError from '../error/password-undefined';
|
import PasswordUndefinedError from '../error/password-undefined';
|
||||||
import EventStore from '../db/event-store';
|
import EventStore from '../db/event-store';
|
||||||
import { USER_UPDATED, USER_CREATED, USER_DELETED } from '../types/events';
|
import { USER_UPDATED, USER_CREATED, USER_DELETED } from '../types/events';
|
||||||
import { IRole } from '../db/access-store';
|
|
||||||
|
|
||||||
const systemUser = new User({ id: -1, username: 'system' });
|
const systemUser = new User({ id: -1, username: 'system' });
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import FeatureTypeStore from '../db/feature-type-store';
|
|||||||
import StrategyStore from '../db/strategy-store';
|
import StrategyStore from '../db/strategy-store';
|
||||||
import ClientApplicationsDb from '../db/client-applications-store';
|
import ClientApplicationsDb from '../db/client-applications-store';
|
||||||
import ClientInstanceStore from '../db/client-instance-store';
|
import ClientInstanceStore from '../db/client-instance-store';
|
||||||
import ClientMetricsStore from '../db/client-metrics-store';
|
import { ClientMetricsStore } from '../db/client-metrics-store';
|
||||||
import FeatureToggleStore from '../db/feature-toggle-store';
|
import FeatureToggleStore from '../db/feature-toggle-store';
|
||||||
import ContextFieldStore from '../db/context-field-store';
|
import ContextFieldStore from '../db/context-field-store';
|
||||||
import SettingStore from '../db/setting-store';
|
import SettingStore from '../db/setting-store';
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
|
import ClientMetricsService, {
|
||||||
|
IClientApp,
|
||||||
|
} from '../../../lib/services/client-metrics';
|
||||||
|
|
||||||
const faker = require('faker');
|
const faker = require('faker');
|
||||||
const dbInit = require('../helpers/database-init');
|
const dbInit = require('../helpers/database-init');
|
||||||
const getLogger = require('../../fixtures/no-logger');
|
const getLogger = require('../../fixtures/no-logger');
|
||||||
const ClientMetricsService = require('../../../lib/services/client-metrics');
|
|
||||||
const { APPLICATION_CREATED } = require('../../../lib/types/events');
|
const { APPLICATION_CREATED } = require('../../../lib/types/events');
|
||||||
|
|
||||||
let stores;
|
let stores;
|
||||||
@ -11,11 +14,12 @@ let clientMetricsService;
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('client_metrics_service_serial', getLogger);
|
db = await dbInit('client_metrics_service_serial', getLogger);
|
||||||
stores = db.stores;
|
stores = db.stores;
|
||||||
clientMetricsService = new ClientMetricsService(stores, {
|
clientMetricsService = new ClientMetricsService(
|
||||||
getLogger,
|
stores,
|
||||||
bulkInterval: 500,
|
{ getLogger },
|
||||||
announcementInterval: 2000,
|
500,
|
||||||
});
|
2000,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@ -24,7 +28,7 @@ afterAll(async () => {
|
|||||||
});
|
});
|
||||||
test('Apps registered should be announced', async () => {
|
test('Apps registered should be announced', async () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
const clientRegistration = {
|
const clientRegistration: IClientApp = {
|
||||||
appName: faker.internet.domainName(),
|
appName: faker.internet.domainName(),
|
||||||
instanceId: faker.datatype.uuid(),
|
instanceId: faker.datatype.uuid(),
|
||||||
strategies: ['default'],
|
strategies: ['default'],
|
Loading…
Reference in New Issue
Block a user