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

View File

@ -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 {

View File

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

View File

@ -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/);
}); });

View File

@ -37,27 +37,31 @@ import {
} from './util/metrics'; } from './util/metrics';
import type { SchedulerService } from './services'; import type { SchedulerService } from './services';
import type { IClientMetricsEnv } from './features/metrics/client-metrics/client-metrics-store-v2-type'; import type { IClientMetricsEnv } from './features/metrics/client-metrics/client-metrics-store-v2-type';
import { DbMetricsMonitor } from './metrics-gauge';
export default class MetricsMonitor { export function registerPrometheusMetrics(
constructor() {}
async 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, const resolveEnvironmentType = async (
): Promise<void> { environment: string,
if (!config.server.serverMetrics) { cachedEnvironments: () => Promise<IEnvironment[]>,
return Promise.resolve(); ): Promise<string> => {
const environments = await cachedEnvironments();
const env = environments.find((e) => e.name === environment);
if (env) {
return env.type;
} else {
return 'unknown';
} }
};
const { eventStore, environmentStore } = stores; const { eventStore, environmentStore } = stores;
const { flagResolver } = config; const { flagResolver } = config;
const dbMetrics = new DbMetricsMonitor(config); const dbMetrics = instanceStatsService.dbMetrics;
const cachedEnvironments: () => Promise<IEnvironment[]> = memoizee( const cachedEnvironments: () => Promise<IEnvironment[]> = memoizee(
async () => environmentStore.getAll(), async () => environmentStore.getAll(),
@ -67,8 +71,6 @@ export default class MetricsMonitor {
}, },
); );
collectDefaultMetrics();
const requestDuration = createSummary({ const requestDuration = createSummary({
name: 'http_request_duration_milliseconds', name: 'http_request_duration_milliseconds',
help: 'App response time', help: 'App response time',
@ -118,14 +120,16 @@ export default class MetricsMonitor {
labelNames: ['toggle', 'active', 'appName'], labelNames: ['toggle', 'active', 'appName'],
}); });
// schedule and execute immediately dbMetrics.registerGaugeDbMetric({
await dbMetrics.registerGaugeDbMetric({
name: 'feature_toggles_total', name: 'feature_toggles_total',
help: 'Number of feature flags', help: 'Number of feature flags',
labelNames: ['version'], labelNames: ['version'],
query: () => instanceStatsService.getToggleCount(), query: () =>
stores.featureToggleStore.count({
archived: false,
}),
map: (value) => ({ value, labels: { version } }), map: (value) => ({ value, labels: { version } }),
})(); });
dbMetrics.registerGaugeDbMetric({ dbMetrics.registerGaugeDbMetric({
name: 'max_feature_environment_strategies', name: 'max_feature_environment_strategies',
@ -260,18 +264,28 @@ export default class MetricsMonitor {
help: 'Number of strategies', help: 'Number of strategies',
}); });
// execute immediately to get initial values dbMetrics.registerGaugeDbMetric({
await dbMetrics.registerGaugeDbMetric({
name: 'client_apps_total', name: 'client_apps_total',
help: 'Number of registered client apps aggregated by range by last seen', help: 'Number of registered client apps aggregated by range by last seen',
labelNames: ['range'], labelNames: ['range'],
query: () => instanceStatsService.getLabeledAppCounts(), query: async () => {
const [t7d, t30d, allTime] = await Promise.all([
stores.clientInstanceStore.getDistinctApplicationsCount(7),
stores.clientInstanceStore.getDistinctApplicationsCount(30),
stores.clientInstanceStore.getDistinctApplicationsCount(),
]);
return {
'7d': t7d,
'30d': t30d,
allTime,
};
},
map: (result) => map: (result) =>
Object.entries(result).map(([range, count]) => ({ Object.entries(result).map(([range, count]) => ({
value: count, value: count,
labels: { range }, labels: { range },
})), })),
})(); });
const samlEnabled = createGauge({ const samlEnabled = createGauge({
name: 'saml_enabled', name: 'saml_enabled',
@ -423,7 +437,307 @@ export default class MetricsMonitor {
labelNames: ['result', 'destination'], labelNames: ['result', 'destination'],
}); });
async function collectStaticCounters() { // register event listeners
eventBus.on(
events.EXCEEDS_LIMIT,
({ resource, limit }: { resource: string; limit: number }) => {
exceedsLimitErrorCounter.increment({ resource, limit });
},
);
eventBus.on(
events.STAGE_ENTERED,
(entered: { stage: string; feature: string }) => {
if (flagResolver.isEnabled('trackLifecycleMetrics')) {
logger.info(
`STAGE_ENTERED listened ${JSON.stringify(entered)}`,
);
}
featureLifecycleStageEnteredCounter.increment({
stage: entered.stage,
});
},
);
eventBus.on(
events.REQUEST_TIME,
({ path, method, time, statusCode, appName }) => {
requestDuration
.labels({
path,
method,
status: statusCode,
appName,
})
.observe(time);
},
);
eventBus.on(events.SCHEDULER_JOB_TIME, ({ jobId, time }) => {
schedulerDuration.labels(jobId).observe(time);
});
eventBus.on(events.FUNCTION_TIME, ({ functionName, className, time }) => {
functionDuration
.labels({
functionName,
className,
})
.observe(time);
});
eventBus.on(events.EVENTS_CREATED_BY_PROCESSED, ({ updated }) => {
eventCreatedByMigration.inc(updated);
});
eventBus.on(events.FEATURES_CREATED_BY_PROCESSED, ({ updated }) => {
featureCreatedByMigration.inc(updated);
});
eventBus.on(events.DB_TIME, ({ store, action, time }) => {
dbDuration
.labels({
store,
action,
})
.observe(time);
});
eventBus.on(events.PROXY_REPOSITORY_CREATED, () => {
proxyRepositoriesCreated.inc();
});
eventBus.on(events.FRONTEND_API_REPOSITORY_CREATED, () => {
frontendApiRepositoriesCreated.inc();
});
eventBus.on(events.PROXY_FEATURES_FOR_TOKEN_TIME, ({ duration }) => {
mapFeaturesForClientDuration.observe(duration);
});
events.onMetricEvent(
eventBus,
events.REQUEST_ORIGIN,
({ type, method, source }) => {
if (flagResolver.isEnabled('originMiddleware')) {
requestOriginCounter.increment({ type, method, source });
}
},
);
eventStore.on(FEATURE_CREATED, ({ featureName, project }) => {
featureFlagUpdateTotal.increment({
toggle: featureName,
project,
environment: 'n/a',
environmentType: 'n/a',
action: 'created',
});
});
eventStore.on(FEATURE_VARIANTS_UPDATED, ({ featureName, project }) => {
featureFlagUpdateTotal.increment({
toggle: featureName,
project,
environment: 'n/a',
environmentType: 'n/a',
action: 'updated',
});
});
eventStore.on(FEATURE_METADATA_UPDATED, ({ featureName, project }) => {
featureFlagUpdateTotal.increment({
toggle: featureName,
project,
environment: 'n/a',
environmentType: 'n/a',
action: 'updated',
});
});
eventStore.on(FEATURE_UPDATED, ({ featureName, project }) => {
featureFlagUpdateTotal.increment({
toggle: featureName,
project,
environment: 'default',
environmentType: 'production',
action: 'updated',
});
});
eventStore.on(
FEATURE_STRATEGY_ADD,
async ({ featureName, project, environment }) => {
const environmentType = await resolveEnvironmentType(
environment,
cachedEnvironments,
);
featureFlagUpdateTotal.increment({
toggle: featureName,
project,
environment,
environmentType,
action: 'updated',
});
},
);
eventStore.on(
FEATURE_STRATEGY_REMOVE,
async ({ featureName, project, environment }) => {
const environmentType = await resolveEnvironmentType(
environment,
cachedEnvironments,
);
featureFlagUpdateTotal.increment({
toggle: featureName,
project,
environment,
environmentType,
action: 'updated',
});
},
);
eventStore.on(
FEATURE_STRATEGY_UPDATE,
async ({ featureName, project, environment }) => {
const environmentType = await resolveEnvironmentType(
environment,
cachedEnvironments,
);
featureFlagUpdateTotal.increment({
toggle: featureName,
project,
environment,
environmentType,
action: 'updated',
});
},
);
eventStore.on(
FEATURE_ENVIRONMENT_DISABLED,
async ({ featureName, project, environment }) => {
const environmentType = await resolveEnvironmentType(
environment,
cachedEnvironments,
);
featureFlagUpdateTotal.increment({
toggle: featureName,
project,
environment,
environmentType,
action: 'updated',
});
},
);
eventStore.on(
FEATURE_ENVIRONMENT_ENABLED,
async ({ featureName, project, environment }) => {
const environmentType = await resolveEnvironmentType(
environment,
cachedEnvironments,
);
featureFlagUpdateTotal.increment({
toggle: featureName,
project,
environment,
environmentType,
action: 'updated',
});
},
);
eventStore.on(FEATURE_ARCHIVED, ({ featureName, project }) => {
featureFlagUpdateTotal.increment({
toggle: featureName,
project,
environment: 'n/a',
environmentType: 'n/a',
action: 'archived',
});
});
eventStore.on(FEATURE_REVIVED, ({ featureName, project }) => {
featureFlagUpdateTotal.increment({
toggle: featureName,
project,
environment: 'n/a',
environmentType: 'n/a',
action: 'revived',
});
});
eventStore.on(PROJECT_CREATED, () => {
projectActionsCounter.increment({ action: PROJECT_CREATED });
});
eventStore.on(PROJECT_ARCHIVED, () => {
projectActionsCounter.increment({ action: PROJECT_ARCHIVED });
});
eventStore.on(PROJECT_REVIVED, () => {
projectActionsCounter.increment({ action: PROJECT_REVIVED });
});
eventStore.on(PROJECT_DELETED, () => {
projectActionsCounter.increment({ action: PROJECT_DELETED });
});
const logger = config.getLogger('metrics.ts');
eventBus.on(CLIENT_METRICS, (metrics: IClientMetricsEnv[]) => {
try {
for (const metric of metrics) {
featureFlagUsageTotal.increment(
{
toggle: metric.featureName,
active: 'true',
appName: metric.appName,
},
metric.yes,
);
featureFlagUsageTotal.increment(
{
toggle: metric.featureName,
active: 'false',
appName: metric.appName,
},
metric.no,
);
}
} catch (e) {
logger.warn('Metrics registration failed', e);
}
});
eventStore.on(CLIENT_REGISTER, (heartbeatEvent: ISdkHeartbeat) => {
if (!heartbeatEvent.sdkName || !heartbeatEvent.sdkVersion) {
return;
}
if (flagResolver.isEnabled('extendedMetrics')) {
clientSdkVersionUsage.increment({
sdk_name: heartbeatEvent.sdkName,
sdk_version: heartbeatEvent.sdkVersion,
platform_name:
heartbeatEvent.metadata?.platformName ?? 'not-set',
platform_version:
heartbeatEvent.metadata?.platformVersion ?? 'not-set',
yggdrasil_version:
heartbeatEvent.metadata?.yggdrasilVersion ?? 'not-set',
spec_version: heartbeatEvent.metadata?.specVersion ?? 'not-set',
});
} else {
clientSdkVersionUsage.increment({
sdk_name: heartbeatEvent.sdkName,
sdk_version: heartbeatEvent.sdkVersion,
platform_name: 'not-set',
platform_version: 'not-set',
yggdrasil_version: 'not-set',
spec_version: 'not-set',
});
}
});
eventStore.on(PROJECT_ENVIRONMENT_REMOVED, ({ project }) => {
projectEnvironmentsDisabled.increment({ project_id: project });
});
eventBus.on(events.ADDON_EVENTS_HANDLED, ({ result, destination }) => {
addonEventsHandledCounter.increment({ result, destination });
});
// return an update function (temporarily) to allow for manual refresh
return {
collectStaticCounters: async () => {
try { try {
dbMetrics.refreshDbMetrics(); dbMetrics.refreshDbMetrics();
@ -691,329 +1005,60 @@ export default class MetricsMonitor {
config.rateLimiting.callSignalEndpointMaxPerSecond * 60, config.rateLimiting.callSignalEndpointMaxPerSecond * 60,
); );
} catch (e) {} } catch (e) {}
},
};
} }
export default class MetricsMonitor {
constructor() {}
async startMonitoring(
config: IUnleashConfig,
stores: IUnleashStores,
version: string,
eventBus: EventEmitter,
instanceStatsService: InstanceStatsService,
schedulerService: SchedulerService,
db: Knex,
): Promise<void> {
if (!config.server.serverMetrics) {
return Promise.resolve();
}
collectDefaultMetrics();
const { collectStaticCounters } = registerPrometheusMetrics(
config,
stores,
version,
eventBus,
instanceStatsService,
);
await this.registerPrometheusDbMetrics(
db,
eventBus,
stores.settingStore,
);
await schedulerService.schedule( await schedulerService.schedule(
collectStaticCounters.bind(this), collectStaticCounters.bind(this),
hoursToMilliseconds(2), hoursToMilliseconds(2),
'collectStaticCounters', 'collectStaticCounters',
); );
await schedulerService.schedule(
eventBus.on( async () =>
events.EXCEEDS_LIMIT, this.registerPoolMetrics.bind(this, db.client.pool, eventBus),
({ resource, limit }: { resource: string; limit: number }) => { minutesToMilliseconds(1),
exceedsLimitErrorCounter.increment({ resource, limit }); 'registerPoolMetrics',
}, 0, // no jitter
);
eventBus.on(
events.STAGE_ENTERED,
(entered: { stage: string; feature: string }) => {
if (flagResolver.isEnabled('trackLifecycleMetrics')) {
logger.info(
`STAGE_ENTERED listened ${JSON.stringify(entered)}`,
);
}
featureLifecycleStageEnteredCounter.increment({
stage: entered.stage,
});
},
);
eventBus.on(
events.REQUEST_TIME,
({ path, method, time, statusCode, appName }) => {
requestDuration
.labels({
path,
method,
status: statusCode,
appName,
})
.observe(time);
},
);
eventBus.on(events.SCHEDULER_JOB_TIME, ({ jobId, time }) => {
schedulerDuration.labels(jobId).observe(time);
});
eventBus.on(
events.FUNCTION_TIME,
({ functionName, className, time }) => {
functionDuration
.labels({
functionName,
className,
})
.observe(time);
},
);
eventBus.on(events.EVENTS_CREATED_BY_PROCESSED, ({ updated }) => {
eventCreatedByMigration.inc(updated);
});
eventBus.on(events.FEATURES_CREATED_BY_PROCESSED, ({ updated }) => {
featureCreatedByMigration.inc(updated);
});
eventBus.on(events.DB_TIME, ({ store, action, time }) => {
dbDuration
.labels({
store,
action,
})
.observe(time);
});
eventBus.on(events.PROXY_REPOSITORY_CREATED, () => {
proxyRepositoriesCreated.inc();
});
eventBus.on(events.FRONTEND_API_REPOSITORY_CREATED, () => {
frontendApiRepositoriesCreated.inc();
});
eventBus.on(events.PROXY_FEATURES_FOR_TOKEN_TIME, ({ duration }) => {
mapFeaturesForClientDuration.observe(duration);
});
events.onMetricEvent(
eventBus,
events.REQUEST_ORIGIN,
({ type, method, source }) => {
if (flagResolver.isEnabled('originMiddleware')) {
requestOriginCounter.increment({ type, method, source });
}
},
);
eventStore.on(FEATURE_CREATED, ({ featureName, project }) => {
featureFlagUpdateTotal.increment({
toggle: featureName,
project,
environment: 'n/a',
environmentType: 'n/a',
action: 'created',
});
});
eventStore.on(FEATURE_VARIANTS_UPDATED, ({ featureName, project }) => {
featureFlagUpdateTotal.increment({
toggle: featureName,
project,
environment: 'n/a',
environmentType: 'n/a',
action: 'updated',
});
});
eventStore.on(FEATURE_METADATA_UPDATED, ({ featureName, project }) => {
featureFlagUpdateTotal.increment({
toggle: featureName,
project,
environment: 'n/a',
environmentType: 'n/a',
action: 'updated',
});
});
eventStore.on(FEATURE_UPDATED, ({ featureName, project }) => {
featureFlagUpdateTotal.increment({
toggle: featureName,
project,
environment: 'default',
environmentType: 'production',
action: 'updated',
});
});
eventStore.on(
FEATURE_STRATEGY_ADD,
async ({ featureName, project, environment }) => {
const environmentType = await this.resolveEnvironmentType(
environment,
cachedEnvironments,
);
featureFlagUpdateTotal.increment({
toggle: featureName,
project,
environment,
environmentType,
action: 'updated',
});
},
);
eventStore.on(
FEATURE_STRATEGY_REMOVE,
async ({ featureName, project, environment }) => {
const environmentType = await this.resolveEnvironmentType(
environment,
cachedEnvironments,
);
featureFlagUpdateTotal.increment({
toggle: featureName,
project,
environment,
environmentType,
action: 'updated',
});
},
);
eventStore.on(
FEATURE_STRATEGY_UPDATE,
async ({ featureName, project, environment }) => {
const environmentType = await this.resolveEnvironmentType(
environment,
cachedEnvironments,
);
featureFlagUpdateTotal.increment({
toggle: featureName,
project,
environment,
environmentType,
action: 'updated',
});
},
);
eventStore.on(
FEATURE_ENVIRONMENT_DISABLED,
async ({ featureName, project, environment }) => {
const environmentType = await this.resolveEnvironmentType(
environment,
cachedEnvironments,
);
featureFlagUpdateTotal.increment({
toggle: featureName,
project,
environment,
environmentType,
action: 'updated',
});
},
);
eventStore.on(
FEATURE_ENVIRONMENT_ENABLED,
async ({ featureName, project, environment }) => {
const environmentType = await this.resolveEnvironmentType(
environment,
cachedEnvironments,
);
featureFlagUpdateTotal.increment({
toggle: featureName,
project,
environment,
environmentType,
action: 'updated',
});
},
);
eventStore.on(FEATURE_ARCHIVED, ({ featureName, project }) => {
featureFlagUpdateTotal.increment({
toggle: featureName,
project,
environment: 'n/a',
environmentType: 'n/a',
action: 'archived',
});
});
eventStore.on(FEATURE_REVIVED, ({ featureName, project }) => {
featureFlagUpdateTotal.increment({
toggle: featureName,
project,
environment: 'n/a',
environmentType: 'n/a',
action: 'revived',
});
});
eventStore.on(PROJECT_CREATED, () => {
projectActionsCounter.increment({ action: PROJECT_CREATED });
});
eventStore.on(PROJECT_ARCHIVED, () => {
projectActionsCounter.increment({ action: PROJECT_ARCHIVED });
});
eventStore.on(PROJECT_REVIVED, () => {
projectActionsCounter.increment({ action: PROJECT_REVIVED });
});
eventStore.on(PROJECT_DELETED, () => {
projectActionsCounter.increment({ action: PROJECT_DELETED });
});
const logger = config.getLogger('metrics.ts');
eventBus.on(CLIENT_METRICS, (metrics: IClientMetricsEnv[]) => {
try {
for (const metric of metrics) {
featureFlagUsageTotal.increment(
{
toggle: metric.featureName,
active: 'true',
appName: metric.appName,
},
metric.yes,
);
featureFlagUsageTotal.increment(
{
toggle: metric.featureName,
active: 'false',
appName: metric.appName,
},
metric.no,
);
}
} catch (e) {
logger.warn('Metrics registration failed', e);
}
});
eventStore.on(CLIENT_REGISTER, (heartbeatEvent: ISdkHeartbeat) => {
if (!heartbeatEvent.sdkName || !heartbeatEvent.sdkVersion) {
return;
}
if (flagResolver.isEnabled('extendedMetrics')) {
clientSdkVersionUsage.increment({
sdk_name: heartbeatEvent.sdkName,
sdk_version: heartbeatEvent.sdkVersion,
platform_name:
heartbeatEvent.metadata?.platformName ?? 'not-set',
platform_version:
heartbeatEvent.metadata?.platformVersion ?? 'not-set',
yggdrasil_version:
heartbeatEvent.metadata?.yggdrasilVersion ?? 'not-set',
spec_version:
heartbeatEvent.metadata?.specVersion ?? 'not-set',
});
} else {
clientSdkVersionUsage.increment({
sdk_name: heartbeatEvent.sdkName,
sdk_version: heartbeatEvent.sdkVersion,
platform_name: 'not-set',
platform_version: 'not-set',
yggdrasil_version: 'not-set',
spec_version: 'not-set',
});
}
});
eventStore.on(PROJECT_ENVIRONMENT_REMOVED, ({ project }) => {
projectEnvironmentsDisabled.increment({ project_id: project });
});
eventBus.on(events.ADDON_EVENTS_HANDLED, ({ result, destination }) => {
addonEventsHandledCounter.increment({ result, destination });
});
await this.configureDbMetrics(
db,
eventBus,
schedulerService,
stores.settingStore,
); );
return Promise.resolve(); return Promise.resolve();
} }
async configureDbMetrics( async registerPrometheusDbMetrics(
db: Knex, db: Knex,
eventBus: EventEmitter, eventBus: EventEmitter,
schedulerService: SchedulerService,
settingStore: ISettingStore, settingStore: ISettingStore,
): Promise<void> { ): Promise<void> {
if (db?.client) { if (db?.client) {
@ -1051,17 +1096,6 @@ export default class MetricsMonitor {
dbPoolPendingAcquires.set(data.pendingAcquires); dbPoolPendingAcquires.set(data.pendingAcquires);
}); });
await schedulerService.schedule(
async () =>
this.registerPoolMetrics.bind(
this,
db.client.pool,
eventBus,
),
minutesToMilliseconds(1),
'registerPoolMetrics',
0, // no jitter
);
const postgresVersion = await settingStore.postgresVersion(); const postgresVersion = await settingStore.postgresVersion();
const database_version = createGauge({ const database_version = createGauge({
name: 'postgres_version', name: 'postgres_version',
@ -1084,26 +1118,8 @@ export default class MetricsMonitor {
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
} catch (e) {} } catch (e) {}
} }
async resolveEnvironmentType(
environment: string,
cachedEnvironments: () => Promise<IEnvironment[]>,
): Promise<string> {
const environments = await cachedEnvironments();
const env = environments.find((e) => e.name === environment);
if (env) {
return env.type;
} else {
return 'unknown';
}
}
} }
export function createMetricsMonitor(): MetricsMonitor { export function createMetricsMonitor(): MetricsMonitor {
return new MetricsMonitor(); return new MetricsMonitor();
} }
module.exports = {
createMetricsMonitor,
};

View File

@ -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/)