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:
parent
f5cf84c8d3
commit
5edd0f879f
@ -4,11 +4,16 @@ import createStores from '../../../test/fixtures/store';
|
|||||||
import VersionService from '../../services/version-service';
|
import VersionService from '../../services/version-service';
|
||||||
import { createFakeGetActiveUsers } from './getActiveUsers';
|
import { createFakeGetActiveUsers } from './getActiveUsers';
|
||||||
import { createFakeGetProductionChanges } from './getProductionChanges';
|
import { createFakeGetProductionChanges } from './getProductionChanges';
|
||||||
|
import { registerPrometheusMetrics } from '../../metrics';
|
||||||
|
import { register } from 'prom-client';
|
||||||
|
import type { IClientInstanceStore } from '../../types';
|
||||||
let instanceStatsService: InstanceStatsService;
|
let instanceStatsService: InstanceStatsService;
|
||||||
let versionService: VersionService;
|
let versionService: VersionService;
|
||||||
|
let clientInstanceStore: IClientInstanceStore;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
register.clear();
|
||||||
|
|
||||||
const config = createTestConfig();
|
const config = createTestConfig();
|
||||||
const stores = createStores();
|
const stores = createStores();
|
||||||
versionService = new VersionService(
|
versionService = new VersionService(
|
||||||
@ -17,6 +22,7 @@ beforeEach(() => {
|
|||||||
createFakeGetActiveUsers(),
|
createFakeGetActiveUsers(),
|
||||||
createFakeGetProductionChanges(),
|
createFakeGetProductionChanges(),
|
||||||
);
|
);
|
||||||
|
clientInstanceStore = stores.clientInstanceStore;
|
||||||
instanceStatsService = new InstanceStatsService(
|
instanceStatsService = new InstanceStatsService(
|
||||||
stores,
|
stores,
|
||||||
config,
|
config,
|
||||||
@ -25,20 +31,25 @@ beforeEach(() => {
|
|||||||
createFakeGetProductionChanges(),
|
createFakeGetProductionChanges(),
|
||||||
);
|
);
|
||||||
|
|
||||||
jest.spyOn(instanceStatsService, 'refreshAppCountSnapshot');
|
registerPrometheusMetrics(
|
||||||
jest.spyOn(instanceStatsService, 'getLabeledAppCounts');
|
config,
|
||||||
|
stores,
|
||||||
|
undefined as unknown as string,
|
||||||
|
config.eventBus,
|
||||||
|
instanceStatsService,
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.spyOn(clientInstanceStore, 'getDistinctApplicationsCount');
|
||||||
jest.spyOn(instanceStatsService, 'getStats');
|
jest.spyOn(instanceStatsService, 'getStats');
|
||||||
|
|
||||||
// validate initial state without calls to these methods
|
|
||||||
expect(instanceStatsService.refreshAppCountSnapshot).toHaveBeenCalledTimes(
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
expect(instanceStatsService.getStats).toHaveBeenCalledTimes(0);
|
expect(instanceStatsService.getStats).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('get snapshot should not call getStats', async () => {
|
test('get snapshot should not call getStats', async () => {
|
||||||
await instanceStatsService.refreshAppCountSnapshot();
|
await instanceStatsService.dbMetrics.refreshDbMetrics();
|
||||||
expect(instanceStatsService.getLabeledAppCounts).toHaveBeenCalledTimes(1);
|
expect(
|
||||||
|
clientInstanceStore.getDistinctApplicationsCount,
|
||||||
|
).toHaveBeenCalledTimes(3);
|
||||||
expect(instanceStatsService.getStats).toHaveBeenCalledTimes(0);
|
expect(instanceStatsService.getStats).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
// subsequent calls to getStatsSnapshot don't call getStats
|
// 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
|
// 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 () => {
|
test('before the snapshot is refreshed we can still get the appCount', async () => {
|
||||||
expect(instanceStatsService.refreshAppCountSnapshot).toHaveBeenCalledTimes(
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
expect(instanceStatsService.getAppCountSnapshot('7d')).toBeUndefined();
|
expect(instanceStatsService.getAppCountSnapshot('7d')).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
@ -29,6 +29,7 @@ import { CUSTOM_ROOT_ROLE_TYPE } from '../../util';
|
|||||||
import type { GetActiveUsers } from './getActiveUsers';
|
import type { GetActiveUsers } from './getActiveUsers';
|
||||||
import type { ProjectModeCount } from '../project/project-store';
|
import type { ProjectModeCount } from '../project/project-store';
|
||||||
import type { GetProductionChanges } from './getProductionChanges';
|
import type { GetProductionChanges } from './getProductionChanges';
|
||||||
|
import { DbMetricsMonitor } from '../../metrics-gauge';
|
||||||
|
|
||||||
export type TimeRange = 'allTime' | '30d' | '7d';
|
export type TimeRange = 'allTime' | '30d' | '7d';
|
||||||
|
|
||||||
@ -115,6 +116,8 @@ export class InstanceStatsService {
|
|||||||
|
|
||||||
private featureStrategiesReadModel: IFeatureStrategiesReadModel;
|
private featureStrategiesReadModel: IFeatureStrategiesReadModel;
|
||||||
|
|
||||||
|
dbMetrics: DbMetricsMonitor;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
{
|
{
|
||||||
featureToggleStore,
|
featureToggleStore,
|
||||||
@ -178,37 +181,20 @@ export class InstanceStatsService {
|
|||||||
this.clientMetricsStore = clientMetricsStoreV2;
|
this.clientMetricsStore = clientMetricsStoreV2;
|
||||||
this.flagResolver = flagResolver;
|
this.flagResolver = flagResolver;
|
||||||
this.featureStrategiesReadModel = featureStrategiesReadModel;
|
this.featureStrategiesReadModel = featureStrategiesReadModel;
|
||||||
|
this.dbMetrics = new DbMetricsMonitor({ getLogger });
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshAppCountSnapshot(): Promise<
|
async fromPrometheus(
|
||||||
Partial<{ [key in TimeRange]: number }>
|
name: string,
|
||||||
> {
|
labels?: Record<string, string | number>,
|
||||||
try {
|
): Promise<number> {
|
||||||
this.appCount = await this.getLabeledAppCounts();
|
return (await this.dbMetrics.findValue(name, labels)) ?? 0;
|
||||||
return this.appCount;
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.warn(
|
|
||||||
'Unable to retrieve statistics. This will be retried',
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
'7d': 0,
|
|
||||||
'30d': 0,
|
|
||||||
allTime: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getProjectModeCount(): Promise<ProjectModeCount[]> {
|
getProjectModeCount(): Promise<ProjectModeCount[]> {
|
||||||
return this.projectStore.getProjectModeCounts();
|
return this.projectStore.getProjectModeCounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
getToggleCount(): Promise<number> {
|
|
||||||
return this.featureToggleStore.count({
|
|
||||||
archived: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getArchivedToggleCount(): Promise<number> {
|
getArchivedToggleCount(): Promise<number> {
|
||||||
return this.featureToggleStore.count({
|
return this.featureToggleStore.count({
|
||||||
archived: true,
|
archived: true,
|
||||||
@ -263,7 +249,7 @@ export class InstanceStatsService {
|
|||||||
maxConstraintValues,
|
maxConstraintValues,
|
||||||
maxConstraints,
|
maxConstraints,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.getToggleCount(),
|
this.fromPrometheus('feature_toggles_total'),
|
||||||
this.getArchivedToggleCount(),
|
this.getArchivedToggleCount(),
|
||||||
this.userStore.count(),
|
this.userStore.count(),
|
||||||
this.userStore.countServiceAccounts(),
|
this.userStore.countServiceAccounts(),
|
||||||
@ -280,7 +266,7 @@ export class InstanceStatsService {
|
|||||||
this.strategyStore.count(),
|
this.strategyStore.count(),
|
||||||
this.hasSAML(),
|
this.hasSAML(),
|
||||||
this.hasOIDC(),
|
this.hasOIDC(),
|
||||||
this.appCount ? this.appCount : this.refreshAppCountSnapshot(),
|
this.clientAppCounts(),
|
||||||
this.eventStore.deprecatedFilteredCount({
|
this.eventStore.deprecatedFilteredCount({
|
||||||
type: FEATURES_EXPORTED,
|
type: FEATURES_EXPORTED,
|
||||||
}),
|
}),
|
||||||
@ -329,20 +315,19 @@ export class InstanceStatsService {
|
|||||||
maxConstraints: maxConstraints?.count ?? 0,
|
maxConstraints: maxConstraints?.count ?? 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
async clientAppCounts(): Promise<Partial<{ [key in TimeRange]: number }>> {
|
||||||
async getLabeledAppCounts(): Promise<
|
this.appCount = {
|
||||||
Partial<{ [key in TimeRange]: number }>
|
'7d': await this.fromPrometheus('client_apps_total', {
|
||||||
> {
|
range: '7d',
|
||||||
const [t7d, t30d, allTime] = await Promise.all([
|
}),
|
||||||
this.clientInstanceStore.getDistinctApplicationsCount(7),
|
'30d': await this.fromPrometheus('client_apps_total', {
|
||||||
this.clientInstanceStore.getDistinctApplicationsCount(30),
|
range: '30d',
|
||||||
this.clientInstanceStore.getDistinctApplicationsCount(),
|
}),
|
||||||
]);
|
allTime: await this.fromPrometheus('client_apps_total', {
|
||||||
return {
|
range: 'allTime',
|
||||||
'7d': t7d,
|
}),
|
||||||
'30d': t30d,
|
|
||||||
allTime,
|
|
||||||
};
|
};
|
||||||
|
return this.appCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAppCountSnapshot(range: TimeRange): number | undefined {
|
getAppCountSnapshot(range: TimeRange): number | undefined {
|
||||||
|
@ -64,10 +64,11 @@ export class DbMetricsMonitor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
refreshDbMetrics = async () => {
|
refreshDbMetrics = async () => {
|
||||||
const tasks = Array.from(this.updaters.values()).map(
|
const tasks = Array.from(this.updaters.entries()).map(
|
||||||
(updater) => updater.task,
|
([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();
|
await task();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -212,6 +212,7 @@ test('should collect metrics for function timings', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should collect metrics for feature flag size', async () => {
|
test('should collect metrics for feature flag size', async () => {
|
||||||
|
await statsService.dbMetrics.refreshDbMetrics();
|
||||||
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/);
|
||||||
});
|
});
|
||||||
@ -222,7 +223,7 @@ test('should collect metrics for archived feature flag size', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should collect metrics for total client apps', async () => {
|
test('should collect metrics for total client apps', async () => {
|
||||||
await statsService.refreshAppCountSnapshot();
|
await statsService.dbMetrics.refreshDbMetrics();
|
||||||
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/);
|
||||||
});
|
});
|
||||||
|
1452
src/lib/metrics.ts
1452
src/lib/metrics.ts
File diff suppressed because it is too large
Load Diff
@ -6,6 +6,7 @@ import {
|
|||||||
import getLogger from '../../../fixtures/no-logger';
|
import getLogger from '../../../fixtures/no-logger';
|
||||||
import type { IUnleashStores } from '../../../../lib/types';
|
import type { IUnleashStores } from '../../../../lib/types';
|
||||||
import { ApiTokenType } from '../../../../lib/types/models/api-token';
|
import { ApiTokenType } from '../../../../lib/types/models/api-token';
|
||||||
|
import { registerPrometheusMetrics } from '../../../../lib/metrics';
|
||||||
|
|
||||||
let app: IUnleashTest;
|
let app: IUnleashTest;
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
@ -26,6 +27,14 @@ beforeAll(async () => {
|
|||||||
},
|
},
|
||||||
db.rawDatabase,
|
db.rawDatabase,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
registerPrometheusMetrics(
|
||||||
|
app.config,
|
||||||
|
stores,
|
||||||
|
undefined as unknown as string,
|
||||||
|
app.config.eventBus,
|
||||||
|
app.services.instanceStatsService,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@ -39,6 +48,8 @@ test('should return instance statistics', async () => {
|
|||||||
createdByUserId: 9999,
|
createdByUserId: 9999,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await app.services.instanceStatsService.dbMetrics.refreshDbMetrics();
|
||||||
|
|
||||||
return app.request
|
return app.request
|
||||||
.get('/api/admin/instance-admin/statistics')
|
.get('/api/admin/instance-admin/statistics')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
|
Loading…
Reference in New Issue
Block a user