1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-12 13:48:35 +02:00

refactor: lifecycle stage duration outside instance stats (#7442)

This commit is contained in:
Mateusz Kwasniewski 2024-06-25 11:22:26 +02:00 committed by GitHub
parent 6a9a2c687d
commit c3fa468a9d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 74 additions and 75 deletions

View File

@ -3,11 +3,17 @@ import type {
StageCount, StageCount,
StageCountByProject, StageCountByProject,
} from './feature-lifecycle-read-model-type'; } from './feature-lifecycle-read-model-type';
import type { IFeatureLifecycleStage } from '../../types'; import type {
IFeatureLifecycleStage,
IProjectLifecycleStageDuration,
} from '../../types';
export class FakeFeatureLifecycleReadModel export class FakeFeatureLifecycleReadModel
implements IFeatureLifecycleReadModel implements IFeatureLifecycleReadModel
{ {
getAllWithStageDuration(): Promise<IProjectLifecycleStageDuration[]> {
return Promise.resolve([]);
}
getStageCount(): Promise<StageCount[]> { getStageCount(): Promise<StageCount[]> {
return Promise.resolve([]); return Promise.resolve([]);
} }

View File

@ -2,7 +2,6 @@ import type {
FeatureLifecycleStage, FeatureLifecycleStage,
IFeatureLifecycleStore, IFeatureLifecycleStore,
FeatureLifecycleView, FeatureLifecycleView,
FeatureLifecycleProjectItem,
} from './feature-lifecycle-store-type'; } from './feature-lifecycle-store-type';
export class FakeFeatureLifecycleStore implements IFeatureLifecycleStore { export class FakeFeatureLifecycleStore implements IFeatureLifecycleStore {
@ -41,18 +40,6 @@ export class FakeFeatureLifecycleStore implements IFeatureLifecycleStore {
return this.lifecycles[feature] || []; return this.lifecycles[feature] || [];
} }
async getAll(): Promise<FeatureLifecycleProjectItem[]> {
const result = Object.entries(this.lifecycles).flatMap(
([key, items]): FeatureLifecycleProjectItem[] =>
items.map((item) => ({
...item,
feature: key,
project: 'fake-project',
})),
);
return result;
}
async delete(feature: string): Promise<void> { async delete(feature: string): Promise<void> {
this.lifecycles[feature] = []; this.lifecycles[feature] = [];
} }

View File

@ -1,4 +1,8 @@
import type { IFeatureLifecycleStage, StageName } from '../../types'; import type {
IFeatureLifecycleStage,
IProjectLifecycleStageDuration,
StageName,
} from '../../types';
export type StageCount = { export type StageCount = {
stage: StageName; stage: StageName;
@ -15,4 +19,5 @@ export interface IFeatureLifecycleReadModel {
): Promise<IFeatureLifecycleStage | undefined>; ): Promise<IFeatureLifecycleStage | undefined>;
getStageCount(): Promise<StageCount[]>; getStageCount(): Promise<StageCount[]>;
getStageCountByProject(): Promise<StageCountByProject[]>; getStageCountByProject(): Promise<StageCountByProject[]>;
getAllWithStageDuration(): Promise<IProjectLifecycleStageDuration[]>;
} }

View File

@ -8,8 +8,11 @@ import { getCurrentStage } from './get-current-stage';
import type { import type {
IFeatureLifecycleStage, IFeatureLifecycleStage,
IFlagResolver, IFlagResolver,
IProjectLifecycleStageDuration,
StageName, StageName,
} from '../../types'; } from '../../types';
import { calculateStageDurations } from './calculate-stage-durations';
import type { FeatureLifecycleProjectItem } from './feature-lifecycle-store-type';
type DBType = { type DBType = {
feature: string; feature: string;
@ -18,6 +21,10 @@ type DBType = {
created_at: Date; created_at: Date;
}; };
type DBProjectType = DBType & {
project: string;
};
export class FeatureLifecycleReadModel implements IFeatureLifecycleReadModel { export class FeatureLifecycleReadModel implements IFeatureLifecycleReadModel {
private db: Db; private db: Db;
@ -106,4 +113,31 @@ export class FeatureLifecycleReadModel implements IFeatureLifecycleReadModel {
return getCurrentStage(stages); return getCurrentStage(stages);
} }
private async getAll(): Promise<FeatureLifecycleProjectItem[]> {
const results = await this.db('feature_lifecycles as flc')
.select('flc.feature', 'flc.stage', 'flc.created_at', 'f.project')
.leftJoin('features as f', 'f.name', 'flc.feature')
.orderBy('created_at', 'asc');
return results.map(
({ feature, stage, created_at, project }: DBProjectType) => ({
feature,
stage,
project,
enteredStageAt: new Date(created_at),
}),
);
}
public async getAllWithStageDuration(): Promise<
IProjectLifecycleStageDuration[]
> {
if (!this.flagResolver.isEnabled('featureLifecycleMetrics')) {
return [];
}
const featureLifeCycles = await this.getAll();
return calculateStageDurations(featureLifeCycles);
}
} }

View File

@ -10,7 +10,6 @@ import {
type IEventStore, type IEventStore,
type IFeatureEnvironmentStore, type IFeatureEnvironmentStore,
type IFlagResolver, type IFlagResolver,
type IProjectLifecycleStageDuration,
type IUnleashConfig, type IUnleashConfig,
} from '../../types'; } from '../../types';
import type { import type {
@ -21,7 +20,6 @@ import EventEmitter from 'events';
import type { Logger } from '../../logger'; import type { Logger } from '../../logger';
import type EventService from '../events/event-service'; import type EventService from '../events/event-service';
import type { FeatureLifecycleCompletedSchema } from '../../openapi'; import type { FeatureLifecycleCompletedSchema } from '../../openapi';
import { calculateStageDurations } from './calculate-stage-durations';
import type { IClientMetricsEnv } from '../metrics/client-metrics/client-metrics-store-v2-type'; import type { IClientMetricsEnv } from '../metrics/client-metrics/client-metrics-store-v2-type';
import groupBy from 'lodash.groupby'; import groupBy from 'lodash.groupby';
@ -231,11 +229,4 @@ export class FeatureLifecycleService extends EventEmitter {
await this.featureLifecycleStore.delete(feature); await this.featureLifecycleStore.delete(feature);
await this.featureInitialized(feature); await this.featureInitialized(feature);
} }
public async getAllWithStageDuration(): Promise<
IProjectLifecycleStageDuration[]
> {
const featureLifeCycles = await this.featureLifecycleStore.getAll();
return calculateStageDurations(featureLifeCycles);
}
} }

View File

@ -17,7 +17,6 @@ export type FeatureLifecycleProjectItem = FeatureLifecycleStage & {
export interface IFeatureLifecycleStore { export interface IFeatureLifecycleStore {
insert(featureLifecycleStages: FeatureLifecycleStage[]): Promise<void>; insert(featureLifecycleStages: FeatureLifecycleStage[]): Promise<void>;
get(feature: string): Promise<FeatureLifecycleView>; get(feature: string): Promise<FeatureLifecycleView>;
getAll(): Promise<FeatureLifecycleProjectItem[]>;
stageExists(stage: FeatureLifecycleStage): Promise<boolean>; stageExists(stage: FeatureLifecycleStage): Promise<boolean>;
delete(feature: string): Promise<void>; delete(feature: string): Promise<void>;
deleteAll(): Promise<void>; deleteAll(): Promise<void>;

View File

@ -40,10 +40,6 @@ import FakeSettingStore from '../../../test/fixtures/fake-setting-store';
import FakeSegmentStore from '../../../test/fixtures/fake-segment-store'; import FakeSegmentStore from '../../../test/fixtures/fake-segment-store';
import FakeStrategiesStore from '../../../test/fixtures/fake-strategies-store'; import FakeStrategiesStore from '../../../test/fixtures/fake-strategies-store';
import FakeFeatureStrategiesStore from '../feature-toggle/fakes/fake-feature-strategies-store'; import FakeFeatureStrategiesStore from '../feature-toggle/fakes/fake-feature-strategies-store';
import {
createFakeFeatureLifecycleService,
createFeatureLifecycleService,
} from '../feature-lifecycle/createFeatureLifecycle';
export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => { export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => {
const { eventBus, getLogger, flagResolver } = config; const { eventBus, getLogger, flagResolver } = config;
@ -88,10 +84,6 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => {
getLogger, getLogger,
flagResolver, flagResolver,
); );
const { featureLifecycleService } = createFeatureLifecycleService(
db,
config,
);
const instanceStatsServiceStores = { const instanceStatsServiceStores = {
featureToggleStore, featureToggleStore,
userStore, userStore,
@ -133,7 +125,6 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => {
versionService, versionService,
getActiveUsers, getActiveUsers,
getProductionChanges, getProductionChanges,
featureLifecycleService,
); );
return instanceStatsService; return instanceStatsService;
@ -156,8 +147,6 @@ export const createFakeInstanceStatsService = (config: IUnleashConfig) => {
const apiTokenStore = new FakeApiTokenStore(); const apiTokenStore = new FakeApiTokenStore();
const clientMetricsStoreV2 = new FakeClientMetricsStoreV2(); const clientMetricsStoreV2 = new FakeClientMetricsStoreV2();
const { featureLifecycleService } =
createFakeFeatureLifecycleService(config);
const instanceStatsServiceStores = { const instanceStatsServiceStores = {
featureToggleStore, featureToggleStore,
userStore, userStore,
@ -194,7 +183,6 @@ export const createFakeInstanceStatsService = (config: IUnleashConfig) => {
versionService, versionService,
getActiveUsers, getActiveUsers,
getProductionChanges, getProductionChanges,
featureLifecycleService,
); );
return instanceStatsService; return instanceStatsService;

View File

@ -4,7 +4,6 @@ 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 { createFakeFeatureLifecycleService } from '../feature-lifecycle/createFeatureLifecycle';
let instanceStatsService: InstanceStatsService; let instanceStatsService: InstanceStatsService;
let versionService: VersionService; let versionService: VersionService;
@ -24,7 +23,6 @@ beforeEach(() => {
versionService, versionService,
createFakeGetActiveUsers(), createFakeGetActiveUsers(),
createFakeGetProductionChanges(), createFakeGetProductionChanges(),
createFakeFeatureLifecycleService(config).featureLifecycleService,
); );
jest.spyOn(instanceStatsService, 'refreshAppCountSnapshot'); jest.spyOn(instanceStatsService, 'refreshAppCountSnapshot');

View File

@ -22,14 +22,12 @@ import {
FEATURES_EXPORTED, FEATURES_EXPORTED,
FEATURES_IMPORTED, FEATURES_IMPORTED,
type IApiTokenStore, type IApiTokenStore,
type IProjectLifecycleStageDuration,
type IFlagResolver, type IFlagResolver,
} from '../../types'; } from '../../types';
import { CUSTOM_ROOT_ROLE_TYPE } from '../../util'; 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 type { FeatureLifecycleService } from '../feature-lifecycle/feature-lifecycle-service';
export type TimeRange = 'allTime' | '30d' | '7d'; export type TimeRange = 'allTime' | '30d' | '7d';
@ -63,7 +61,6 @@ export interface InstanceStats {
enabledCount: number; enabledCount: number;
variantCount: number; variantCount: number;
}; };
featureLifeCycles: IProjectLifecycleStageDuration[];
} }
export type InstanceStatsSigned = Omit<InstanceStats, 'projects'> & { export type InstanceStatsSigned = Omit<InstanceStats, 'projects'> & {
@ -94,8 +91,6 @@ export class InstanceStatsService {
private eventStore: IEventStore; private eventStore: IEventStore;
private featureLifecycleService: FeatureLifecycleService;
private apiTokenStore: IApiTokenStore; private apiTokenStore: IApiTokenStore;
private versionService: VersionService; private versionService: VersionService;
@ -154,7 +149,6 @@ export class InstanceStatsService {
versionService: VersionService, versionService: VersionService,
getActiveUsers: GetActiveUsers, getActiveUsers: GetActiveUsers,
getProductionChanges: GetProductionChanges, getProductionChanges: GetProductionChanges,
featureLifecycleService: FeatureLifecycleService,
) { ) {
this.strategyStore = strategyStore; this.strategyStore = strategyStore;
this.userStore = userStore; this.userStore = userStore;
@ -169,7 +163,6 @@ export class InstanceStatsService {
this.settingStore = settingStore; this.settingStore = settingStore;
this.eventStore = eventStore; this.eventStore = eventStore;
this.clientInstanceStore = clientInstanceStore; this.clientInstanceStore = clientInstanceStore;
this.featureLifecycleService = featureLifecycleService;
this.logger = getLogger('services/stats-service.js'); this.logger = getLogger('services/stats-service.js');
this.getActiveUsers = getActiveUsers; this.getActiveUsers = getActiveUsers;
this.getProductionChanges = getProductionChanges; this.getProductionChanges = getProductionChanges;
@ -257,7 +250,6 @@ export class InstanceStatsService {
featureImports, featureImports,
productionChanges, productionChanges,
previousDayMetricsBucketsCount, previousDayMetricsBucketsCount,
featureLifeCycles,
] = await Promise.all([ ] = await Promise.all([
this.getToggleCount(), this.getToggleCount(),
this.getArchivedToggleCount(), this.getArchivedToggleCount(),
@ -281,7 +273,6 @@ export class InstanceStatsService {
this.eventStore.filteredCount({ type: FEATURES_IMPORTED }), this.eventStore.filteredCount({ type: FEATURES_IMPORTED }),
this.getProductionChanges(), this.getProductionChanges(),
this.clientMetricsStore.countPreviousDayHourlyMetricsBuckets(), this.clientMetricsStore.countPreviousDayHourlyMetricsBuckets(),
this.getAllWithStageDuration(),
]); ]);
return { return {
@ -314,7 +305,6 @@ export class InstanceStatsService {
featureImports, featureImports,
productionChanges, productionChanges,
previousDayMetricsBucketsCount, previousDayMetricsBucketsCount,
featureLifeCycles,
}; };
} }
@ -348,11 +338,4 @@ export class InstanceStatsService {
); );
return { ...instanceStats, sum, projects: totalProjects }; return { ...instanceStats, sum, projects: totalProjects };
} }
async getAllWithStageDuration(): Promise<IProjectLifecycleStageDuration[]> {
if (this.flagResolver.isEnabled('featureLifecycleMetrics')) {
return this.featureLifecycleService.getAllWithStageDuration();
}
return [];
}
} }

View File

@ -16,14 +16,19 @@ import { InstanceStatsService } from './features/instance-stats/instance-stats-s
import VersionService from './services/version-service'; import VersionService from './services/version-service';
import { createFakeGetActiveUsers } from './features/instance-stats/getActiveUsers'; import { createFakeGetActiveUsers } from './features/instance-stats/getActiveUsers';
import { createFakeGetProductionChanges } from './features/instance-stats/getProductionChanges'; import { createFakeGetProductionChanges } from './features/instance-stats/getProductionChanges';
import type { IEnvironmentStore, IUnleashStores } from './types'; import type {
IEnvironmentStore,
IFeatureLifecycleReadModel,
IFeatureLifecycleStore,
IUnleashStores,
} from './types';
import FakeEnvironmentStore from './features/project-environments/fake-environment-store'; import FakeEnvironmentStore from './features/project-environments/fake-environment-store';
import { SchedulerService } from './services'; import { SchedulerService } from './services';
import noLogger from '../test/fixtures/no-logger'; import noLogger from '../test/fixtures/no-logger';
import { createFeatureLifecycleService } from './features';
import dbInit, { type ITestDb } from '../test/e2e/helpers/database-init';
import getLogger from '../test/fixtures/no-logger'; import getLogger from '../test/fixtures/no-logger';
import type { FeatureLifecycleStore } from './features/feature-lifecycle/feature-lifecycle-store'; import dbInit, { type ITestDb } from '../test/e2e/helpers/database-init';
import { FeatureLifecycleStore } from './features/feature-lifecycle/feature-lifecycle-store';
import { FeatureLifecycleReadModel } from './features/feature-lifecycle/feature-lifecycle-read-model';
const monitor = createMetricsMonitor(); const monitor = createMetricsMonitor();
const eventBus = new EventEmitter(); const eventBus = new EventEmitter();
@ -33,7 +38,8 @@ let environmentStore: IEnvironmentStore;
let statsService: InstanceStatsService; let statsService: InstanceStatsService;
let stores: IUnleashStores; let stores: IUnleashStores;
let schedulerService: SchedulerService; let schedulerService: SchedulerService;
let featureLifeCycleStore: FeatureLifecycleStore; let featureLifeCycleStore: IFeatureLifecycleStore;
let featureLifeCycleReadModel: IFeatureLifecycleReadModel;
let db: ITestDb; let db: ITestDb;
beforeAll(async () => { beforeAll(async () => {
@ -59,8 +65,13 @@ beforeAll(async () => {
); );
db = await dbInit('metrics_test', getLogger); db = await dbInit('metrics_test', getLogger);
const { featureLifecycleService, featureLifecycleStore } = featureLifeCycleReadModel = new FeatureLifecycleReadModel(
createFeatureLifecycleService(db.rawDatabase, config); db.rawDatabase,
config.flagResolver,
);
stores.featureLifecycleReadModel = featureLifeCycleReadModel;
featureLifeCycleStore = new FeatureLifecycleStore(db.rawDatabase);
stores.featureLifecycleStore = featureLifeCycleStore;
statsService = new InstanceStatsService( statsService = new InstanceStatsService(
stores, stores,
@ -68,9 +79,7 @@ beforeAll(async () => {
versionService, versionService,
createFakeGetActiveUsers(), createFakeGetActiveUsers(),
createFakeGetProductionChanges(), createFakeGetProductionChanges(),
featureLifecycleService,
); );
featureLifeCycleStore = featureLifecycleStore;
schedulerService = new SchedulerService( schedulerService = new SchedulerService(
noLogger, noLogger,
@ -303,9 +312,13 @@ test('should collect metrics for lifecycle', async () => {
stage: 'initial', stage: 'initial',
}, },
]); ]);
const { featureLifeCycles } = await statsService.getStats(); const stageCount = await featureLifeCycleReadModel.getStageCountByProject();
expect(featureLifeCycles).toHaveLength(1); const stageDurations =
await featureLifeCycleReadModel.getAllWithStageDuration();
expect(stageCount).toHaveLength(1);
expect(stageDurations).toHaveLength(1);
const metrics = await prometheusRegister.metrics(); const metrics = await prometheusRegister.metrics();
expect(metrics).toMatch(/feature_lifecycle_stage_duration/); expect(metrics).toMatch(/feature_lifecycle_stage_duration/);
expect(metrics).toMatch(/stage_count_by_project/);
}); });

View File

@ -306,12 +306,14 @@ export default class MetricsMonitor {
maxConstraintValuesResult, maxConstraintValuesResult,
maxConstraintsPerStrategyResult, maxConstraintsPerStrategyResult,
stageCountByProjectResult, stageCountByProjectResult,
stageDurationByProject,
] = await Promise.all([ ] = await Promise.all([
stores.featureStrategiesReadModel.getMaxFeatureStrategies(), stores.featureStrategiesReadModel.getMaxFeatureStrategies(),
stores.featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies(), stores.featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies(),
stores.featureStrategiesReadModel.getMaxConstraintValues(), stores.featureStrategiesReadModel.getMaxConstraintValues(),
stores.featureStrategiesReadModel.getMaxConstraintsPerStrategy(), stores.featureStrategiesReadModel.getMaxConstraintsPerStrategy(),
stores.featureLifecycleReadModel.getStageCountByProject(), stores.featureLifecycleReadModel.getStageCountByProject(),
stores.featureLifecycleReadModel.getAllWithStageDuration(),
]); ]);
featureFlagsTotal.reset(); featureFlagsTotal.reset();
@ -326,7 +328,7 @@ export default class MetricsMonitor {
serviceAccounts.reset(); serviceAccounts.reset();
serviceAccounts.set(stats.serviceAccounts); serviceAccounts.set(stats.serviceAccounts);
stats.featureLifeCycles.forEach((stage) => { stageDurationByProject.forEach((stage) => {
featureLifecycleStageDuration featureLifecycleStageDuration
.labels({ .labels({
stage: stage.stage, stage: stage.stage,

View File

@ -125,13 +125,6 @@ class InstanceAdminController extends Controller {
variantCount: 100, variantCount: 100,
enabledCount: 200, enabledCount: 200,
}, },
featureLifeCycles: [
{
project: 'default',
stage: 'archived',
duration: 2000,
},
],
}; };
} }