1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-09-10 17:53:36 +02:00

Split metric registration from refresh, use existing prometheus metrics for stats

This commit is contained in:
Gastón Fournier 2024-10-17 19:12:48 +02:00
parent f5cf84c8d3
commit 5edd0f879f
No known key found for this signature in database
GPG Key ID: AF45428626E17A8E
6 changed files with 797 additions and 773 deletions

View File

@ -4,11 +4,16 @@ import createStores from '../../../test/fixtures/store';
import VersionService from '../../services/version-service';
import { createFakeGetActiveUsers } from './getActiveUsers';
import { createFakeGetProductionChanges } from './getProductionChanges';
import { registerPrometheusMetrics } from '../../metrics';
import { register } from 'prom-client';
import type { IClientInstanceStore } from '../../types';
let instanceStatsService: InstanceStatsService;
let versionService: VersionService;
let clientInstanceStore: IClientInstanceStore;
beforeEach(() => {
register.clear();
const config = createTestConfig();
const stores = createStores();
versionService = new VersionService(
@ -17,6 +22,7 @@ beforeEach(() => {
createFakeGetActiveUsers(),
createFakeGetProductionChanges(),
);
clientInstanceStore = stores.clientInstanceStore;
instanceStatsService = new InstanceStatsService(
stores,
config,
@ -25,20 +31,25 @@ beforeEach(() => {
createFakeGetProductionChanges(),
);
jest.spyOn(instanceStatsService, 'refreshAppCountSnapshot');
jest.spyOn(instanceStatsService, 'getLabeledAppCounts');
registerPrometheusMetrics(
config,
stores,
undefined as unknown as string,
config.eventBus,
instanceStatsService,
);
jest.spyOn(clientInstanceStore, 'getDistinctApplicationsCount');
jest.spyOn(instanceStatsService, 'getStats');
// validate initial state without calls to these methods
expect(instanceStatsService.refreshAppCountSnapshot).toHaveBeenCalledTimes(
0,
);
expect(instanceStatsService.getStats).toHaveBeenCalledTimes(0);
});
test('get snapshot should not call getStats', async () => {
await instanceStatsService.refreshAppCountSnapshot();
expect(instanceStatsService.getLabeledAppCounts).toHaveBeenCalledTimes(1);
await instanceStatsService.dbMetrics.refreshDbMetrics();
expect(
clientInstanceStore.getDistinctApplicationsCount,
).toHaveBeenCalledTimes(3);
expect(instanceStatsService.getStats).toHaveBeenCalledTimes(0);
// subsequent calls to getStatsSnapshot don't call getStats
@ -51,12 +62,11 @@ test('get snapshot should not call getStats', async () => {
]);
}
// after querying the stats snapshot no call to getStats should be issued
expect(instanceStatsService.getLabeledAppCounts).toHaveBeenCalledTimes(1);
expect(
clientInstanceStore.getDistinctApplicationsCount,
).toHaveBeenCalledTimes(3);
});
test('before the snapshot is refreshed we can still get the appCount', async () => {
expect(instanceStatsService.refreshAppCountSnapshot).toHaveBeenCalledTimes(
0,
);
expect(instanceStatsService.getAppCountSnapshot('7d')).toBeUndefined();
});

View File

@ -29,6 +29,7 @@ import { CUSTOM_ROOT_ROLE_TYPE } from '../../util';
import type { GetActiveUsers } from './getActiveUsers';
import type { ProjectModeCount } from '../project/project-store';
import type { GetProductionChanges } from './getProductionChanges';
import { DbMetricsMonitor } from '../../metrics-gauge';
export type TimeRange = 'allTime' | '30d' | '7d';
@ -115,6 +116,8 @@ export class InstanceStatsService {
private featureStrategiesReadModel: IFeatureStrategiesReadModel;
dbMetrics: DbMetricsMonitor;
constructor(
{
featureToggleStore,
@ -178,37 +181,20 @@ export class InstanceStatsService {
this.clientMetricsStore = clientMetricsStoreV2;
this.flagResolver = flagResolver;
this.featureStrategiesReadModel = featureStrategiesReadModel;
this.dbMetrics = new DbMetricsMonitor({ getLogger });
}
async refreshAppCountSnapshot(): Promise<
Partial<{ [key in TimeRange]: number }>
> {
try {
this.appCount = await this.getLabeledAppCounts();
return this.appCount;
} catch (error) {
this.logger.warn(
'Unable to retrieve statistics. This will be retried',
error,
);
return {
'7d': 0,
'30d': 0,
allTime: 0,
};
}
async fromPrometheus(
name: string,
labels?: Record<string, string | number>,
): Promise<number> {
return (await this.dbMetrics.findValue(name, labels)) ?? 0;
}
getProjectModeCount(): Promise<ProjectModeCount[]> {
return this.projectStore.getProjectModeCounts();
}
getToggleCount(): Promise<number> {
return this.featureToggleStore.count({
archived: false,
});
}
getArchivedToggleCount(): Promise<number> {
return this.featureToggleStore.count({
archived: true,
@ -263,7 +249,7 @@ export class InstanceStatsService {
maxConstraintValues,
maxConstraints,
] = await Promise.all([
this.getToggleCount(),
this.fromPrometheus('feature_toggles_total'),
this.getArchivedToggleCount(),
this.userStore.count(),
this.userStore.countServiceAccounts(),
@ -280,7 +266,7 @@ export class InstanceStatsService {
this.strategyStore.count(),
this.hasSAML(),
this.hasOIDC(),
this.appCount ? this.appCount : this.refreshAppCountSnapshot(),
this.clientAppCounts(),
this.eventStore.deprecatedFilteredCount({
type: FEATURES_EXPORTED,
}),
@ -329,20 +315,19 @@ export class InstanceStatsService {
maxConstraints: maxConstraints?.count ?? 0,
};
}
async getLabeledAppCounts(): Promise<
Partial<{ [key in TimeRange]: number }>
> {
const [t7d, t30d, allTime] = await Promise.all([
this.clientInstanceStore.getDistinctApplicationsCount(7),
this.clientInstanceStore.getDistinctApplicationsCount(30),
this.clientInstanceStore.getDistinctApplicationsCount(),
]);
return {
'7d': t7d,
'30d': t30d,
allTime,
async clientAppCounts(): Promise<Partial<{ [key in TimeRange]: number }>> {
this.appCount = {
'7d': await this.fromPrometheus('client_apps_total', {
range: '7d',
}),
'30d': await this.fromPrometheus('client_apps_total', {
range: '30d',
}),
allTime: await this.fromPrometheus('client_apps_total', {
range: 'allTime',
}),
};
return this.appCount;
}
getAppCountSnapshot(range: TimeRange): number | undefined {

View File

@ -64,10 +64,11 @@ export class DbMetricsMonitor {
}
refreshDbMetrics = async () => {
const tasks = Array.from(this.updaters.values()).map(
(updater) => updater.task,
const tasks = Array.from(this.updaters.entries()).map(
([name, updater]) => ({ name, task: updater.task }),
);
for (const task of tasks) {
for (const { name, task } of tasks) {
this.log.debug(`Refreshing metric ${name}`);
await task();
}
};

View File

@ -212,6 +212,7 @@ test('should collect metrics for function timings', async () => {
});
test('should collect metrics for feature flag size', async () => {
await statsService.dbMetrics.refreshDbMetrics();
const metrics = await prometheusRegister.metrics();
expect(metrics).toMatch(/feature_toggles_total\{version="(.*)"\} 0/);
});
@ -222,7 +223,7 @@ test('should collect metrics for archived feature flag size', async () => {
});
test('should collect metrics for total client apps', async () => {
await statsService.refreshAppCountSnapshot();
await statsService.dbMetrics.refreshDbMetrics();
const metrics = await prometheusRegister.metrics();
expect(metrics).toMatch(/client_apps_total\{range="(.*)"\} 0/);
});

File diff suppressed because it is too large Load Diff

View File

@ -6,6 +6,7 @@ import {
import getLogger from '../../../fixtures/no-logger';
import type { IUnleashStores } from '../../../../lib/types';
import { ApiTokenType } from '../../../../lib/types/models/api-token';
import { registerPrometheusMetrics } from '../../../../lib/metrics';
let app: IUnleashTest;
let db: ITestDb;
@ -26,6 +27,14 @@ beforeAll(async () => {
},
db.rawDatabase,
);
registerPrometheusMetrics(
app.config,
stores,
undefined as unknown as string,
app.config.eventBus,
app.services.instanceStatsService,
);
});
afterAll(async () => {
@ -39,6 +48,8 @@ test('should return instance statistics', async () => {
createdByUserId: 9999,
});
await app.services.instanceStatsService.dbMetrics.refreshDbMetrics();
return app.request
.get('/api/admin/instance-admin/statistics')
.expect('Content-Type', /json/)