1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-04 00:18:40 +01:00

chore: reimplementation of app stats (#6155)

## About the changes
App stats is mainly used to cap the number of applications reported to
Unleash based on the last 7 days information:
cc2ccb1134/src/lib/middleware/response-time-metrics.ts (L24-L28)

Instead of getting all stats, just calculate appCount statistics

Use scheduler service instead of setInterval
This commit is contained in:
Gastón Fournier 2024-02-08 17:15:42 +01:00 committed by GitHub
parent 4a4196c66a
commit fa3352786a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 92 additions and 97 deletions

View File

@ -25,32 +25,38 @@ beforeEach(() => {
createFakeGetProductionChanges(), createFakeGetProductionChanges(),
); );
jest.spyOn(instanceStatsService, 'refreshStatsSnapshot'); jest.spyOn(instanceStatsService, 'refreshAppCountSnapshot');
jest.spyOn(instanceStatsService, 'getLabeledAppCounts');
jest.spyOn(instanceStatsService, 'getStats'); jest.spyOn(instanceStatsService, 'getStats');
// validate initial state without calls to these methods // validate initial state without calls to these methods
expect(instanceStatsService.refreshStatsSnapshot).toBeCalledTimes(0); expect(instanceStatsService.refreshAppCountSnapshot).toHaveBeenCalledTimes(
expect(instanceStatsService.getStats).toBeCalledTimes(0); 0,
);
expect(instanceStatsService.getStats).toHaveBeenCalledTimes(0);
}); });
test('get snapshot should not call getStats', async () => { test('get snapshot should not call getStats', async () => {
await instanceStatsService.refreshStatsSnapshot(); await instanceStatsService.refreshAppCountSnapshot();
expect(instanceStatsService.getStats).toBeCalledTimes(1); expect(instanceStatsService.getLabeledAppCounts).toHaveBeenCalledTimes(1);
expect(instanceStatsService.getStats).toHaveBeenCalledTimes(0);
// subsequent calls to getStatsSnapshot don't call getStats // subsequent calls to getStatsSnapshot don't call getStats
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
const stats = instanceStatsService.getStatsSnapshot(); const { clientApps } = await instanceStatsService.getStats();
expect(stats?.clientApps).toStrictEqual([ expect(clientApps).toStrictEqual([
{ range: 'allTime', count: 0 }, { count: 0, range: '7d' },
{ range: '30d', count: 0 }, { count: 0, range: '30d' },
{ range: '7d', count: 0 }, { count: 0, range: 'allTime' },
]); ]);
} }
// after querying the stats snapshot no call to getStats should be issued // after querying the stats snapshot no call to getStats should be issued
expect(instanceStatsService.getStats).toBeCalledTimes(1); expect(instanceStatsService.getLabeledAppCounts).toHaveBeenCalledTimes(1);
}); });
test('before the snapshot is refreshed we can still get the appCount', async () => { test('before the snapshot is refreshed we can still get the appCount', async () => {
expect(instanceStatsService.refreshStatsSnapshot).toBeCalledTimes(0); expect(instanceStatsService.refreshAppCountSnapshot).toHaveBeenCalledTimes(
0,
);
expect(instanceStatsService.getAppCountSnapshot('7d')).toBeUndefined(); expect(instanceStatsService.getAppCountSnapshot('7d')).toBeUndefined();
}); });

View File

@ -99,8 +99,6 @@ export class InstanceStatsService {
private clientMetricsStore: IClientMetricsStoreV2; private clientMetricsStore: IClientMetricsStoreV2;
private snapshot?: InstanceStats;
private appCount?: Partial<{ [key in TimeRange]: number }>; private appCount?: Partial<{ [key in TimeRange]: number }>;
private getActiveUsers: GetActiveUsers; private getActiveUsers: GetActiveUsers;
@ -165,19 +163,22 @@ export class InstanceStatsService {
this.clientMetricsStore = clientMetricsStoreV2; this.clientMetricsStore = clientMetricsStoreV2;
} }
async refreshStatsSnapshot(): Promise<void> { async refreshAppCountSnapshot(): Promise<
Partial<{ [key in TimeRange]: number }>
> {
try { try {
this.snapshot = await this.getStats(); this.appCount = await this.getLabeledAppCounts();
const appCountReplacement = {}; return this.appCount;
this.snapshot.clientApps?.forEach((appCount) => {
appCountReplacement[appCount.range] = appCount.count;
});
this.appCount = appCountReplacement;
} catch (error) { } catch (error) {
this.logger.warn( this.logger.warn(
'Unable to retrieve statistics. This will be retried', 'Unable to retrieve statistics. This will be retried',
error, error,
); );
return {
'7d': 0,
'30d': 0,
allTime: 0,
};
} }
} }
@ -251,7 +252,7 @@ export class InstanceStatsService {
this.strategyStore.count(), this.strategyStore.count(),
this.hasSAML(), this.hasSAML(),
this.hasOIDC(), this.hasOIDC(),
this.getLabeledAppCounts(), this.appCount ? this.appCount : this.refreshAppCountSnapshot(),
this.eventStore.filteredCount({ type: FEATURES_EXPORTED }), this.eventStore.filteredCount({ type: FEATURES_EXPORTED }),
this.eventStore.filteredCount({ type: FEATURES_IMPORTED }), this.eventStore.filteredCount({ type: FEATURES_IMPORTED }),
this.getProductionChanges(), this.getProductionChanges(),
@ -279,7 +280,10 @@ export class InstanceStatsService {
strategies, strategies,
SAMLenabled, SAMLenabled,
OIDCenabled, OIDCenabled,
clientApps, clientApps: Object.entries(clientApps).map(([range, count]) => ({
range: range as TimeRange,
count,
})),
featureExports, featureExports,
featureImports, featureImports,
productionChanges, productionChanges,
@ -287,34 +291,19 @@ export class InstanceStatsService {
}; };
} }
getStatsSnapshot(): InstanceStats | undefined {
return this.snapshot;
}
async getLabeledAppCounts(): Promise< async getLabeledAppCounts(): Promise<
{ range: TimeRange; count: number }[] Partial<{ [key in TimeRange]: number }>
> { > {
return [ const [t7d, t30d, allTime] = await Promise.all([
{ this.clientInstanceStore.getDistinctApplicationsCount(7),
range: 'allTime', this.clientInstanceStore.getDistinctApplicationsCount(30),
count: this.clientInstanceStore.getDistinctApplicationsCount(),
await this.clientInstanceStore.getDistinctApplicationsCount(), ]);
}, return {
{ '7d': t7d,
range: '30d', '30d': t30d,
count: allTime,
await this.clientInstanceStore.getDistinctApplicationsCount( };
30,
),
},
{
range: '7d',
count:
await this.clientInstanceStore.getDistinctApplicationsCount(
7,
),
},
];
} }
getAppCountSnapshot(range: TimeRange): number | undefined { getAppCountSnapshot(range: TimeRange): number | undefined {

View File

@ -57,9 +57,9 @@ export const scheduleServices = async (
); );
schedulerService.schedule( schedulerService.schedule(
instanceStatsService.refreshStatsSnapshot.bind(instanceStatsService), instanceStatsService.refreshAppCountSnapshot.bind(instanceStatsService),
minutesToMilliseconds(5), minutesToMilliseconds(5),
'refreshStatsSnapshot', 'refreshAppCountSnapshot',
); );
schedulerService.schedule( schedulerService.schedule(

View File

@ -17,6 +17,8 @@ import { createFakeGetActiveUsers } from './features/instance-stats/getActiveUse
import { createFakeGetProductionChanges } from './features/instance-stats/getProductionChanges'; import { createFakeGetProductionChanges } from './features/instance-stats/getProductionChanges';
import { IEnvironmentStore, IUnleashStores } from './types'; import { IEnvironmentStore, IUnleashStores } from './types';
import FakeEnvironmentStore from './features/project-environments/fake-environment-store'; import FakeEnvironmentStore from './features/project-environments/fake-environment-store';
import { SchedulerService } from './services';
import noLogger from '../test/fixtures/no-logger';
const monitor = createMetricsMonitor(); const monitor = createMetricsMonitor();
const eventBus = new EventEmitter(); const eventBus = new EventEmitter();
@ -25,7 +27,8 @@ let eventStore: IEventStore;
let environmentStore: IEnvironmentStore; let environmentStore: IEnvironmentStore;
let statsService: InstanceStatsService; let statsService: InstanceStatsService;
let stores: IUnleashStores; let stores: IUnleashStores;
beforeAll(() => { let schedulerService: SchedulerService;
beforeAll(async () => {
const config = createTestConfig({ const config = createTestConfig({
server: { server: {
serverMetrics: true, serverMetrics: true,
@ -49,6 +52,14 @@ beforeAll(() => {
createFakeGetProductionChanges(), createFakeGetProductionChanges(),
); );
schedulerService = new SchedulerService(
noLogger,
{
isMaintenanceMode: () => Promise.resolve(false),
},
eventBus,
);
const db = { const db = {
client: { client: {
pool: { pool: {
@ -61,19 +72,21 @@ beforeAll(() => {
}, },
}, },
}; };
// @ts-ignore - We don't want a full knex implementation for our tests, it's enough that it actually yields the numbers we want.
monitor.startMonitoring( await monitor.startMonitoring(
config, config,
stores, stores,
'4.0.0', '4.0.0',
eventBus, eventBus,
statsService, statsService,
//@ts-ignore schedulerService,
// @ts-ignore - We don't want a full knex implementation for our tests, it's enough that it actually yields the numbers we want.
db, db,
); );
}); });
afterAll(() => {
monitor.stopMonitoring(); afterAll(async () => {
schedulerService.stop();
}); });
test('should collect metrics for requests', async () => { test('should collect metrics for requests', async () => {
@ -160,17 +173,12 @@ test('should collect metrics for db query timings', async () => {
}); });
test('should collect metrics for feature toggle size', async () => { test('should collect metrics for feature toggle size', async () => {
await new Promise((done) => {
setTimeout(done, 10);
});
const metrics = await prometheusRegister.metrics(); const metrics = await prometheusRegister.metrics();
expect(metrics).toMatch(/feature_toggles_total\{version="(.*)"\} 0/); expect(metrics).toMatch(/feature_toggles_total\{version="(.*)"\} 0/);
}); });
test('should collect metrics for total client apps', async () => { test('should collect metrics for total client apps', async () => {
await new Promise((done) => { await statsService.refreshAppCountSnapshot();
setTimeout(done, 10);
});
const metrics = await prometheusRegister.metrics(); const metrics = await prometheusRegister.metrics();
expect(metrics).toMatch(/client_apps_total\{range="(.*)"\} 0/); expect(metrics).toMatch(/client_apps_total\{range="(.*)"\} 0/);
}); });

View File

@ -26,23 +26,18 @@ import { InstanceStatsService } from './features/instance-stats/instance-stats-s
import { ValidatedClientMetrics } from './features/metrics/shared/schema'; import { ValidatedClientMetrics } from './features/metrics/shared/schema';
import { IEnvironment } from './types'; import { IEnvironment } from './types';
import { createCounter, createGauge, createSummary } from './util/metrics'; import { createCounter, createGauge, createSummary } from './util/metrics';
import { SchedulerService } from './services';
export default class MetricsMonitor { export default class MetricsMonitor {
timer?: NodeJS.Timeout; constructor() {}
poolMetricsTimer?: NodeJS.Timeout; async startMonitoring(
constructor() {
this.timer = undefined;
this.poolMetricsTimer = undefined;
}
startMonitoring(
config: IUnleashConfig, config: IUnleashConfig,
stores: IUnleashStores, stores: IUnleashStores,
version: string, version: string,
eventBus: EventEmitter, eventBus: EventEmitter,
instanceStatsService: InstanceStatsService, instanceStatsService: InstanceStatsService,
schedulerService: SchedulerService,
db: Knex, db: Knex,
): Promise<void> { ): Promise<void> {
if (!config.server.serverMetrics) { if (!config.server.serverMetrics) {
@ -317,10 +312,8 @@ export default class MetricsMonitor {
oidcEnabled.set(stats.OIDCenabled ? 1 : 0); oidcEnabled.set(stats.OIDCenabled ? 1 : 0);
clientAppsTotal.reset(); clientAppsTotal.reset();
stats.clientApps.forEach((clientStat) => stats.clientApps.forEach(({ range, count }) =>
clientAppsTotal clientAppsTotal.labels({ range }).set(count),
.labels({ range: clientStat.range })
.set(clientStat.count),
); );
rateLimits.reset(); rateLimits.reset();
@ -361,13 +354,12 @@ export default class MetricsMonitor {
} catch (e) {} } catch (e) {}
} }
process.nextTick(() => { await schedulerService.schedule(
collectStaticCounters(); collectStaticCounters.bind(this),
this.timer = setInterval( hoursToMilliseconds(2),
() => collectStaticCounters(), 'collectStaticCounters',
hoursToMilliseconds(2), 0, // no jitter
).unref(); );
});
eventBus.on( eventBus.on(
events.REQUEST_TIME, events.REQUEST_TIME,
@ -548,17 +540,16 @@ export default class MetricsMonitor {
} }
}); });
this.configureDbMetrics(db, eventBus); await this.configureDbMetrics(db, eventBus, schedulerService);
return Promise.resolve(); return Promise.resolve();
} }
stopMonitoring(): void { async configureDbMetrics(
clearInterval(this.timer); db: Knex,
clearInterval(this.poolMetricsTimer); eventBus: EventEmitter,
} schedulerService: SchedulerService,
): Promise<void> {
configureDbMetrics(db: Knex, eventBus: EventEmitter): void {
if (db?.client) { if (db?.client) {
const dbPoolMin = createGauge({ const dbPoolMin = createGauge({
name: 'db_pool_min', name: 'db_pool_min',
@ -594,12 +585,12 @@ export default class MetricsMonitor {
dbPoolPendingAcquires.set(data.pendingAcquires); dbPoolPendingAcquires.set(data.pendingAcquires);
}); });
this.registerPoolMetrics(db.client.pool, eventBus); await schedulerService.schedule(
this.poolMetricsTimer = setInterval( this.registerPoolMetrics.bind(this, db.client.pool, eventBus),
() => this.registerPoolMetrics(db.client.pool, eventBus),
minutesToMilliseconds(1), minutesToMilliseconds(1),
'registerPoolMetrics',
0, // no jitter
); );
this.poolMetricsTimer.unref();
} }
} }

View File

@ -60,7 +60,6 @@ async function createApp(
await stopServer(); await stopServer();
} }
services.schedulerService.stop(); services.schedulerService.stop();
metricsMonitor.stopMonitoring();
services.addonService.destroy(); services.addonService.destroy();
await db.destroy(); await db.destroy();
}; };
@ -77,6 +76,7 @@ async function createApp(
serverVersion, serverVersion,
config.eventBus, config.eventBus,
services.instanceStatsService, services.instanceStatsService,
services.schedulerService,
db, db,
); );
const unleash: Omit<IUnleash, 'stop'> = { const unleash: Omit<IUnleash, 'stop'> = {

View File

@ -28,7 +28,7 @@ export default class FakeClientInstanceStore implements IClientInstanceStore {
} }
setLastSeen(): Promise<void> { setLastSeen(): Promise<void> {
return; return Promise.resolve();
} }
async getBySdkName(sdkName: string): Promise<IClientInstance[]> { async getBySdkName(sdkName: string): Promise<IClientInstance[]> {
@ -84,7 +84,8 @@ export default class FakeClientInstanceStore implements IClientInstanceStore {
} }
async getDistinctApplicationsCount(): Promise<number> { async getDistinctApplicationsCount(): Promise<number> {
return this.getDistinctApplications().then((apps) => apps.length); const apps = await this.getDistinctApplications();
return apps.length;
} }
async insert(details: INewClientInstance): Promise<void> { async insert(details: INewClientInstance): Promise<void> {