diff --git a/src/lib/metrics-gauge.ts b/src/lib/metrics-gauge.ts index c17e0d784e94..0c6bf06755b2 100644 --- a/src/lib/metrics-gauge.ts +++ b/src/lib/metrics-gauge.ts @@ -37,17 +37,39 @@ export class DbMetricsMonitor { return Array.isArray(value) ? value : [value]; } + private async fetch( + definition: GaugeDefinition, + ) { + const result = await definition.query(); + if ( + result !== undefined && + result !== null && + (!Array.isArray(result) || result.length > 0) + ) { + const resultArray = this.asArray(definition.map(result)); + resultArray + .filter((r) => !Number.isInteger(r.value)) + .forEach((r) => { + this.log.warn( + `Invalid value for ${definition.name}: ${r.value}. Value must be an integer.`, + ); + }); + return resultArray.filter((r) => Number.isInteger(r.value)); + } + return []; + } + registerGaugeDbMetric( definition: GaugeDefinition, ): Task { const gauge = createGauge(definition); const task = async () => { try { - const result = await definition.query(); - if (result !== null && result !== undefined) { - const results = this.asArray(definition.map(result)); + const results = await this.fetch(definition); + if (results.length > 0) { gauge.reset(); for (const r of results) { + // when r.value is zero, we are writing a zero value to the gauge which might not be what we want in some cases if (r.labels) { gauge.labels(r.labels).set(r.value); } else { diff --git a/src/lib/metrics.ts b/src/lib/metrics.ts index 126471710977..186def36e3be 100644 --- a/src/lib/metrics.ts +++ b/src/lib/metrics.ts @@ -219,20 +219,54 @@ export function registerPrometheusMetrics( }), }); - const maxConstraintsPerStrategy = createGauge({ + dbMetrics.registerGaugeDbMetric({ name: 'max_strategy_constraints', help: 'Maximum number of constraints used on a single strategy', labelNames: ['feature', 'environment'], + query: () => + stores.featureStrategiesReadModel.getMaxConstraintsPerStrategy(), + map: (result) => ({ + value: result.count, + labels: { + environment: result.environment, + feature: result.feature, + }, + }), }); - const largestProjectEnvironment = createGauge({ + + dbMetrics.registerGaugeDbMetric({ name: 'largest_project_environment_size', help: 'The largest project environment size (bytes) based on strategies, constraints, variants and parameters', labelNames: ['project', 'environment'], + query: () => + stores.largestResourcesReadModel.getLargestProjectEnvironments(1), + map: (results) => { + const result = results[0]; + return { + value: result.size, + labels: { + project: result.project, + environment: result.environment, + }, + }; + }, }); - const largestFeatureEnvironment = createGauge({ + dbMetrics.registerGaugeDbMetric({ name: 'largest_feature_environment_size', help: 'The largest feature environment size (bytes) base on strategies, constraints, variants and parameters', labelNames: ['feature', 'environment'], + query: () => + stores.largestResourcesReadModel.getLargestFeatureEnvironments(1), + map: (results) => { + const result = results[0]; + return { + value: result.size, + labels: { + feature: result.feature, + environment: result.environment, + }, + }; + }, }); const featureTogglesArchivedTotal = createGauge({ @@ -473,27 +507,76 @@ export function registerPrometheusMetrics( help: 'Duration of mapFeaturesForClient function', }); - const featureLifecycleStageDuration = createGauge({ + dbMetrics.registerGaugeDbMetric({ name: 'feature_lifecycle_stage_duration', labelNames: ['stage', 'project_id'], help: 'Duration of feature lifecycle stages', + query: () => stores.featureLifecycleReadModel.getAllWithStageDuration(), + map: (result) => + result.map((stageResult) => ({ + value: stageResult.duration, + labels: { + project_id: stageResult.project, + stage: stageResult.stage, + }, + })), }); - const onboardingDuration = createGauge({ + dbMetrics.registerGaugeDbMetric({ name: 'onboarding_duration', labelNames: ['event'], help: 'firstLogin, secondLogin, firstFeatureFlag, firstPreLive, firstLive from first user creation', + query: () => + flagResolver.isEnabled('onboardingMetrics') + ? stores.onboardingReadModel.getInstanceOnboardingMetrics() + : Promise.resolve({}), + map: (result) => + Object.keys(result) + .filter((key) => Number.isInteger(result[key])) + .map((key) => ({ + value: result[key], + labels: { + event: key, + }, + })), }); - const projectOnboardingDuration = createGauge({ + + dbMetrics.registerGaugeDbMetric({ name: 'project_onboarding_duration', labelNames: ['event', 'project'], help: 'firstFeatureFlag, firstPreLive, firstLive from project creation', + query: () => + flagResolver.isEnabled('onboardingMetrics') + ? stores.onboardingReadModel.getProjectsOnboardingMetrics() + : Promise.resolve([]), + map: (projectsOnboardingMetrics) => + projectsOnboardingMetrics.flatMap( + ({ project, ...projectMetrics }) => + Object.keys(projectMetrics) + .filter((key) => Number.isInteger(projectMetrics[key])) + .map((key) => ({ + value: projectMetrics[key], + labels: { + event: key, + project, + }, + })), + ), }); - const featureLifecycleStageCountByProject = createGauge({ + dbMetrics.registerGaugeDbMetric({ name: 'feature_lifecycle_stage_count_by_project', help: 'Count features in a given stage by project id', labelNames: ['stage', 'project_id'], + query: () => stores.featureLifecycleReadModel.getStageCountByProject(), + map: (result) => + result.map((stageResult) => ({ + value: stageResult.count, + labels: { + project_id: stageResult.project, + stage: stageResult.stage, + }, + })), }); const featureLifecycleStageEnteredCounter = createCounter({ @@ -858,34 +941,6 @@ export function registerPrometheusMetrics( collectDbMetrics: dbMetrics.refreshDbMetrics, collectStaticCounters: async () => { try { - const [ - maxConstraintsPerStrategyResult, - stageCountByProjectResult, - stageDurationByProject, - largestProjectEnvironments, - largestFeatureEnvironments, - deprecatedTokens, - instanceOnboardingMetrics, - projectsOnboardingMetrics, - ] = await Promise.all([ - stores.featureStrategiesReadModel.getMaxConstraintsPerStrategy(), - stores.featureLifecycleReadModel.getStageCountByProject(), - stores.featureLifecycleReadModel.getAllWithStageDuration(), - stores.largestResourcesReadModel.getLargestProjectEnvironments( - 1, - ), - stores.largestResourcesReadModel.getLargestFeatureEnvironments( - 1, - ), - stores.apiTokenStore.countDeprecatedTokens(), - flagResolver.isEnabled('onboardingMetrics') - ? stores.onboardingReadModel.getInstanceOnboardingMetrics() - : Promise.resolve({}), - flagResolver.isEnabled('onboardingMetrics') - ? stores.onboardingReadModel.getProjectsOnboardingMetrics() - : Promise.resolve([]), - ]); - featureTogglesArchivedTotal.reset(); featureTogglesArchivedTotal.set( await instanceStatsService.getArchivedToggleCount(), @@ -899,25 +954,6 @@ export function registerPrometheusMetrics( await instanceStatsService.countServiceAccounts(), ); - stageDurationByProject.forEach((stage) => { - featureLifecycleStageDuration - .labels({ - stage: stage.stage, - project_id: stage.project, - }) - .set(stage.duration); - }); - - featureLifecycleStageCountByProject.reset(); - stageCountByProjectResult.forEach((stageResult) => - featureLifecycleStageCountByProject - .labels({ - project_id: stageResult.project, - stage: stageResult.stage, - }) - .set(stageResult.count), - ); - apiTokens.reset(); for (const [ @@ -927,6 +963,8 @@ export function registerPrometheusMetrics( apiTokens.labels({ type }).set(value); } + const deprecatedTokens = + await stores.apiTokenStore.countDeprecatedTokens(); orphanedTokensTotal.reset(); orphanedTokensTotal.set(deprecatedTokens.orphanedTokens); @@ -939,60 +977,6 @@ export function registerPrometheusMetrics( legacyTokensActive.reset(); legacyTokensActive.set(deprecatedTokens.activeLegacyTokens); - if (maxConstraintsPerStrategyResult) { - maxConstraintsPerStrategy.reset(); - maxConstraintsPerStrategy - .labels({ - environment: - maxConstraintsPerStrategyResult.environment, - feature: maxConstraintsPerStrategyResult.feature, - }) - .set(maxConstraintsPerStrategyResult.count); - } - - if (largestProjectEnvironments.length > 0) { - const projectEnvironment = largestProjectEnvironments[0]; - largestProjectEnvironment.reset(); - largestProjectEnvironment - .labels({ - project: projectEnvironment.project, - environment: projectEnvironment.environment, - }) - .set(projectEnvironment.size); - } - - if (largestFeatureEnvironments.length > 0) { - const featureEnvironment = largestFeatureEnvironments[0]; - largestFeatureEnvironment.reset(); - largestFeatureEnvironment - .labels({ - feature: featureEnvironment.feature, - environment: featureEnvironment.environment, - }) - .set(featureEnvironment.size); - } - - Object.keys(instanceOnboardingMetrics).forEach((key) => { - if (Number.isInteger(instanceOnboardingMetrics[key])) { - onboardingDuration - .labels({ - event: key, - }) - .set(instanceOnboardingMetrics[key]); - } - }); - projectsOnboardingMetrics.forEach( - ({ project, ...projectMetrics }) => { - Object.keys(projectMetrics).forEach((key) => { - if (Number.isInteger(projectMetrics[key])) { - projectOnboardingDuration - .labels({ event: key, project }) - .set(projectMetrics[key]); - } - }); - }, - ); - for (const [resource, limit] of Object.entries( config.resourceLimits, )) {