diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 0a6c897e26..1da459eb91 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -47,6 +47,7 @@ import { ProjectOwnersReadModel } from '../features/project/project-owners-read- import { FeatureLifecycleStore } from '../features/feature-lifecycle/feature-lifecycle-store'; import { ProjectFlagCreatorsReadModel } from '../features/project/project-flag-creators-read-model'; import { FeatureStrategiesReadModel } from '../features/feature-toggle/feature-strategies-read-model'; +import { FeatureLifecycleReadModel } from '../features/feature-lifecycle/feature-lifecycle-read-model'; export const createStores = ( config: IUnleashConfig, @@ -161,6 +162,10 @@ export const createStores = ( projectFlagCreatorsReadModel: new ProjectFlagCreatorsReadModel(db), featureLifecycleStore: new FeatureLifecycleStore(db), featureStrategiesReadModel: new FeatureStrategiesReadModel(db), + featureLifecycleReadModel: new FeatureLifecycleReadModel( + db, + config.flagResolver, + ), }; }; diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle-read-model.test.ts b/src/lib/features/feature-lifecycle/feature-lifecycle-read-model.test.ts index 7ea9fbd27e..e76813afc6 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle-read-model.test.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle-read-model.test.ts @@ -4,15 +4,25 @@ import { FeatureLifecycleReadModel } from './feature-lifecycle-read-model'; import type { IFeatureLifecycleStore } from './feature-lifecycle-store-type'; import type { IFeatureLifecycleReadModel } from './feature-lifecycle-read-model-type'; import type { IFeatureToggleStore } from '../feature-toggle/types/feature-toggle-store-type'; +import type { IFlagResolver } from '../../types'; let db: ITestDb; let featureLifeycycleReadModel: IFeatureLifecycleReadModel; let featureLifecycleStore: IFeatureLifecycleStore; let featureToggleStore: IFeatureToggleStore; +const alwaysOnFlagResolver = { + isEnabled() { + return true; + }, +} as unknown as IFlagResolver; + beforeAll(async () => { db = await dbInit('feature_lifecycle_read_model', getLogger); - featureLifeycycleReadModel = new FeatureLifecycleReadModel(db.rawDatabase); + featureLifeycycleReadModel = new FeatureLifecycleReadModel( + db.rawDatabase, + alwaysOnFlagResolver, + ); featureLifecycleStore = db.stores.featureLifecycleStore; featureToggleStore = db.stores.featureToggleStore; }); diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle-read-model.ts b/src/lib/features/feature-lifecycle/feature-lifecycle-read-model.ts index 4cb14b4f21..bd3df36177 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle-read-model.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle-read-model.ts @@ -5,7 +5,11 @@ import type { StageCountByProject, } from './feature-lifecycle-read-model-type'; import { getCurrentStage } from './get-current-stage'; -import type { IFeatureLifecycleStage, StageName } from '../../types'; +import type { + IFeatureLifecycleStage, + IFlagResolver, + StageName, +} from '../../types'; type DBType = { feature: string; @@ -17,11 +21,18 @@ type DBType = { export class FeatureLifecycleReadModel implements IFeatureLifecycleReadModel { private db: Db; - constructor(db: Db) { + private flagResolver: IFlagResolver; + + constructor(db: Db, flagResolver: IFlagResolver) { this.db = db; + this.flagResolver = flagResolver; } async getStageCount(): Promise { + if (!this.flagResolver.isEnabled('featureLifecycleMetrics')) { + return []; + } + const { rows } = await this.db.raw(` SELECT stage, @@ -47,6 +58,10 @@ export class FeatureLifecycleReadModel implements IFeatureLifecycleReadModel { } async getStageCountByProject(): Promise { + if (!this.flagResolver.isEnabled('featureLifecycleMetrics')) { + return []; + } + const { rows } = await this.db.raw(` SELECT f.project, diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts b/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts index b3da7cf49d..ddbe30f18d 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts @@ -46,7 +46,10 @@ beforeAll(async () => { eventStore = db.stores.eventStore; eventBus = app.config.eventBus; featureLifecycleService = app.services.featureLifecycleService; - featureLifecycleReadModel = new FeatureLifecycleReadModel(db.rawDatabase); + featureLifecycleReadModel = new FeatureLifecycleReadModel( + db.rawDatabase, + app.config.flagResolver, + ); featureLifecycleStore = db.stores.featureLifecycleStore; await app.request diff --git a/src/lib/features/feature-toggle/createFeatureToggleService.ts b/src/lib/features/feature-toggle/createFeatureToggleService.ts index 7f755f59b8..733ad391e4 100644 --- a/src/lib/features/feature-toggle/createFeatureToggleService.ts +++ b/src/lib/features/feature-toggle/createFeatureToggleService.ts @@ -124,7 +124,10 @@ export const createFeatureToggleService = ( const dependentFeaturesReadModel = new DependentFeaturesReadModel(db); - const featureLifecycleReadModel = new FeatureLifecycleReadModel(db); + const featureLifecycleReadModel = new FeatureLifecycleReadModel( + db, + config.flagResolver, + ); const dependentFeaturesService = createDependentFeaturesService(config)(db); diff --git a/src/lib/metrics.ts b/src/lib/metrics.ts index 67c904361e..0a7da71e4c 100644 --- a/src/lib/metrics.ts +++ b/src/lib/metrics.ts @@ -285,6 +285,12 @@ export default class MetricsMonitor { help: 'Duration of feature lifecycle stages', }); + const stageCountByProject = createGauge({ + name: 'stage_count_by_project', + help: 'Count features in a given stage by project id', + labelNames: ['stage', 'project_id'], + }); + const projectEnvironmentsDisabled = createCounter({ name: 'project_environments_disabled', help: 'How many "environment disabled" events we have received for each project', @@ -299,11 +305,13 @@ export default class MetricsMonitor { maxEnvironmentStrategies, maxConstraintValuesResult, maxConstraintsPerStrategyResult, + stageCountByProjectResult, ] = await Promise.all([ stores.featureStrategiesReadModel.getMaxFeatureStrategies(), stores.featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies(), stores.featureStrategiesReadModel.getMaxConstraintValues(), stores.featureStrategiesReadModel.getMaxConstraintsPerStrategy(), + stores.featureLifecycleReadModel.getStageCountByProject(), ]); featureFlagsTotal.reset(); @@ -327,6 +335,16 @@ export default class MetricsMonitor { .observe(stage.duration); }); + stageCountByProject.reset(); + stageCountByProjectResult.forEach((stageResult) => + stageCountByProject + .labels({ + project_id: stageResult.project, + stage: stageResult.stage, + }) + .set(stageResult.count), + ); + apiTokens.reset(); for (const [type, value] of stats.apiTokens) { diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index 0350a9c2a0..d2c739edd7 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -160,7 +160,7 @@ export const createServices = ( ? new DependentFeaturesReadModel(db) : new FakeDependentFeaturesReadModel(); const featureLifecycleReadModel = db - ? new FeatureLifecycleReadModel(db) + ? new FeatureLifecycleReadModel(db, config.flagResolver) : new FakeFeatureLifecycleReadModel(); const segmentReadModel = db ? new SegmentReadModel(db) diff --git a/src/lib/types/stores.ts b/src/lib/types/stores.ts index 1e59813cda..aeba11153d 100644 --- a/src/lib/types/stores.ts +++ b/src/lib/types/stores.ts @@ -44,6 +44,7 @@ import { IProjectOwnersReadModel } from '../features/project/project-owners-read import { IFeatureLifecycleStore } from '../features/feature-lifecycle/feature-lifecycle-store-type'; import { IProjectFlagCreatorsReadModel } from '../features/project/project-flag-creators-read-model.type'; import { IFeatureStrategiesReadModel } from '../features/feature-toggle/types/feature-strategies-read-model-type'; +import { IFeatureLifecycleReadModel } from '../features/feature-lifecycle/feature-lifecycle-read-model-type'; export interface IUnleashStores { accessStore: IAccessStore; @@ -92,6 +93,7 @@ export interface IUnleashStores { projectFlagCreatorsReadModel: IProjectFlagCreatorsReadModel; featureLifecycleStore: IFeatureLifecycleStore; featureStrategiesReadModel: IFeatureStrategiesReadModel; + featureLifecycleReadModel: IFeatureLifecycleReadModel; } export { @@ -139,4 +141,5 @@ export { IFeatureLifecycleStore, IProjectFlagCreatorsReadModel, IFeatureStrategiesReadModel, + IFeatureLifecycleReadModel, }; diff --git a/src/test/fixtures/store.ts b/src/test/fixtures/store.ts index 8297832201..9a8c72dbc3 100644 --- a/src/test/fixtures/store.ts +++ b/src/test/fixtures/store.ts @@ -47,6 +47,7 @@ import { FakeProjectOwnersReadModel } from '../../lib/features/project/fake-proj import { FakeFeatureLifecycleStore } from '../../lib/features/feature-lifecycle/fake-feature-lifecycle-store'; import { FakeProjectFlagCreatorsReadModel } from '../../lib/features/project/fake-project-flag-creators-read-model'; import { FakeFeatureStrategiesReadModel } from '../../lib/features/feature-toggle/fake-feature-strategies-read-model'; +import { FakeFeatureLifecycleReadModel } from '../../lib/features/feature-lifecycle/fake-feature-lifecycle-read-model'; const db = { select: () => ({ @@ -103,6 +104,7 @@ const createStores: () => IUnleashStores = () => { projectFlagCreatorsReadModel: new FakeProjectFlagCreatorsReadModel(), featureLifecycleStore: new FakeFeatureLifecycleStore(), featureStrategiesReadModel: new FakeFeatureStrategiesReadModel(), + featureLifecycleReadModel: new FakeFeatureLifecycleReadModel(), }; };