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 { 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();
|
||||
});
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
@ -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/);
|
||||
});
|
||||
|
@ -37,27 +37,31 @@ import {
|
||||
} from './util/metrics';
|
||||
import type { SchedulerService } from './services';
|
||||
import type { IClientMetricsEnv } from './features/metrics/client-metrics/client-metrics-store-v2-type';
|
||||
import { DbMetricsMonitor } from './metrics-gauge';
|
||||
|
||||
export default class MetricsMonitor {
|
||||
constructor() {}
|
||||
|
||||
async startMonitoring(
|
||||
export function registerPrometheusMetrics(
|
||||
config: IUnleashConfig,
|
||||
stores: IUnleashStores,
|
||||
version: string,
|
||||
eventBus: EventEmitter,
|
||||
instanceStatsService: InstanceStatsService,
|
||||
schedulerService: SchedulerService,
|
||||
db: Knex,
|
||||
): Promise<void> {
|
||||
if (!config.server.serverMetrics) {
|
||||
return Promise.resolve();
|
||||
) {
|
||||
const resolveEnvironmentType = async (
|
||||
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';
|
||||
}
|
||||
};
|
||||
|
||||
const { eventStore, environmentStore } = stores;
|
||||
const { flagResolver } = config;
|
||||
const dbMetrics = new DbMetricsMonitor(config);
|
||||
const dbMetrics = instanceStatsService.dbMetrics;
|
||||
|
||||
const cachedEnvironments: () => Promise<IEnvironment[]> = memoizee(
|
||||
async () => environmentStore.getAll(),
|
||||
@ -67,8 +71,6 @@ export default class MetricsMonitor {
|
||||
},
|
||||
);
|
||||
|
||||
collectDefaultMetrics();
|
||||
|
||||
const requestDuration = createSummary({
|
||||
name: 'http_request_duration_milliseconds',
|
||||
help: 'App response time',
|
||||
@ -118,14 +120,16 @@ export default class MetricsMonitor {
|
||||
labelNames: ['toggle', 'active', 'appName'],
|
||||
});
|
||||
|
||||
// schedule and execute immediately
|
||||
await dbMetrics.registerGaugeDbMetric({
|
||||
dbMetrics.registerGaugeDbMetric({
|
||||
name: 'feature_toggles_total',
|
||||
help: 'Number of feature flags',
|
||||
labelNames: ['version'],
|
||||
query: () => instanceStatsService.getToggleCount(),
|
||||
query: () =>
|
||||
stores.featureToggleStore.count({
|
||||
archived: false,
|
||||
}),
|
||||
map: (value) => ({ value, labels: { version } }),
|
||||
})();
|
||||
});
|
||||
|
||||
dbMetrics.registerGaugeDbMetric({
|
||||
name: 'max_feature_environment_strategies',
|
||||
@ -260,18 +264,28 @@ export default class MetricsMonitor {
|
||||
help: 'Number of strategies',
|
||||
});
|
||||
|
||||
// execute immediately to get initial values
|
||||
await dbMetrics.registerGaugeDbMetric({
|
||||
dbMetrics.registerGaugeDbMetric({
|
||||
name: 'client_apps_total',
|
||||
help: 'Number of registered client apps aggregated by range by last seen',
|
||||
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) =>
|
||||
Object.entries(result).map(([range, count]) => ({
|
||||
value: count,
|
||||
labels: { range },
|
||||
})),
|
||||
})();
|
||||
});
|
||||
|
||||
const samlEnabled = createGauge({
|
||||
name: 'saml_enabled',
|
||||
@ -423,7 +437,307 @@ export default class MetricsMonitor {
|
||||
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 {
|
||||
dbMetrics.refreshDbMetrics();
|
||||
|
||||
@ -691,329 +1005,60 @@ export default class MetricsMonitor {
|
||||
config.rateLimiting.callSignalEndpointMaxPerSecond * 60,
|
||||
);
|
||||
} 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(
|
||||
collectStaticCounters.bind(this),
|
||||
hoursToMilliseconds(2),
|
||||
'collectStaticCounters',
|
||||
);
|
||||
|
||||
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 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,
|
||||
await schedulerService.schedule(
|
||||
async () =>
|
||||
this.registerPoolMetrics.bind(this, db.client.pool, eventBus),
|
||||
minutesToMilliseconds(1),
|
||||
'registerPoolMetrics',
|
||||
0, // no jitter
|
||||
);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async configureDbMetrics(
|
||||
async registerPrometheusDbMetrics(
|
||||
db: Knex,
|
||||
eventBus: EventEmitter,
|
||||
schedulerService: SchedulerService,
|
||||
settingStore: ISettingStore,
|
||||
): Promise<void> {
|
||||
if (db?.client) {
|
||||
@ -1051,17 +1096,6 @@ export default class MetricsMonitor {
|
||||
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 database_version = createGauge({
|
||||
name: 'postgres_version',
|
||||
@ -1084,26 +1118,8 @@ export default class MetricsMonitor {
|
||||
// eslint-disable-next-line no-empty
|
||||
} 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 {
|
||||
return new MetricsMonitor();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createMetricsMonitor,
|
||||
};
|
||||
|
@ -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/)
|
||||
|
Loading…
Reference in New Issue
Block a user