1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-09 00:18:00 +01: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,
StageCountByProject,
} from './feature-lifecycle-read-model-type';
import type { IFeatureLifecycleStage } from '../../types';
import type {
IFeatureLifecycleStage,
IProjectLifecycleStageDuration,
} from '../../types';
export class FakeFeatureLifecycleReadModel
implements IFeatureLifecycleReadModel
{
getAllWithStageDuration(): Promise<IProjectLifecycleStageDuration[]> {
return Promise.resolve([]);
}
getStageCount(): Promise<StageCount[]> {
return Promise.resolve([]);
}

View File

@ -2,7 +2,6 @@ import type {
FeatureLifecycleStage,
IFeatureLifecycleStore,
FeatureLifecycleView,
FeatureLifecycleProjectItem,
} from './feature-lifecycle-store-type';
export class FakeFeatureLifecycleStore implements IFeatureLifecycleStore {
@ -41,18 +40,6 @@ export class FakeFeatureLifecycleStore implements IFeatureLifecycleStore {
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> {
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 = {
stage: StageName;
@ -15,4 +19,5 @@ export interface IFeatureLifecycleReadModel {
): Promise<IFeatureLifecycleStage | undefined>;
getStageCount(): Promise<StageCount[]>;
getStageCountByProject(): Promise<StageCountByProject[]>;
getAllWithStageDuration(): Promise<IProjectLifecycleStageDuration[]>;
}

View File

@ -8,8 +8,11 @@ import { getCurrentStage } from './get-current-stage';
import type {
IFeatureLifecycleStage,
IFlagResolver,
IProjectLifecycleStageDuration,
StageName,
} from '../../types';
import { calculateStageDurations } from './calculate-stage-durations';
import type { FeatureLifecycleProjectItem } from './feature-lifecycle-store-type';
type DBType = {
feature: string;
@ -18,6 +21,10 @@ type DBType = {
created_at: Date;
};
type DBProjectType = DBType & {
project: string;
};
export class FeatureLifecycleReadModel implements IFeatureLifecycleReadModel {
private db: Db;
@ -106,4 +113,31 @@ export class FeatureLifecycleReadModel implements IFeatureLifecycleReadModel {
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 IFeatureEnvironmentStore,
type IFlagResolver,
type IProjectLifecycleStageDuration,
type IUnleashConfig,
} from '../../types';
import type {
@ -21,7 +20,6 @@ import EventEmitter from 'events';
import type { Logger } from '../../logger';
import type EventService from '../events/event-service';
import type { FeatureLifecycleCompletedSchema } from '../../openapi';
import { calculateStageDurations } from './calculate-stage-durations';
import type { IClientMetricsEnv } from '../metrics/client-metrics/client-metrics-store-v2-type';
import groupBy from 'lodash.groupby';
@ -231,11 +229,4 @@ export class FeatureLifecycleService extends EventEmitter {
await this.featureLifecycleStore.delete(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 {
insert(featureLifecycleStages: FeatureLifecycleStage[]): Promise<void>;
get(feature: string): Promise<FeatureLifecycleView>;
getAll(): Promise<FeatureLifecycleProjectItem[]>;
stageExists(stage: FeatureLifecycleStage): Promise<boolean>;
delete(feature: string): 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 FakeStrategiesStore from '../../../test/fixtures/fake-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) => {
const { eventBus, getLogger, flagResolver } = config;
@ -88,10 +84,6 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => {
getLogger,
flagResolver,
);
const { featureLifecycleService } = createFeatureLifecycleService(
db,
config,
);
const instanceStatsServiceStores = {
featureToggleStore,
userStore,
@ -133,7 +125,6 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => {
versionService,
getActiveUsers,
getProductionChanges,
featureLifecycleService,
);
return instanceStatsService;
@ -156,8 +147,6 @@ export const createFakeInstanceStatsService = (config: IUnleashConfig) => {
const apiTokenStore = new FakeApiTokenStore();
const clientMetricsStoreV2 = new FakeClientMetricsStoreV2();
const { featureLifecycleService } =
createFakeFeatureLifecycleService(config);
const instanceStatsServiceStores = {
featureToggleStore,
userStore,
@ -194,7 +183,6 @@ export const createFakeInstanceStatsService = (config: IUnleashConfig) => {
versionService,
getActiveUsers,
getProductionChanges,
featureLifecycleService,
);
return instanceStatsService;

View File

@ -4,7 +4,6 @@ import createStores from '../../../test/fixtures/store';
import VersionService from '../../services/version-service';
import { createFakeGetActiveUsers } from './getActiveUsers';
import { createFakeGetProductionChanges } from './getProductionChanges';
import { createFakeFeatureLifecycleService } from '../feature-lifecycle/createFeatureLifecycle';
let instanceStatsService: InstanceStatsService;
let versionService: VersionService;
@ -24,7 +23,6 @@ beforeEach(() => {
versionService,
createFakeGetActiveUsers(),
createFakeGetProductionChanges(),
createFakeFeatureLifecycleService(config).featureLifecycleService,
);
jest.spyOn(instanceStatsService, 'refreshAppCountSnapshot');

View File

@ -22,14 +22,12 @@ import {
FEATURES_EXPORTED,
FEATURES_IMPORTED,
type IApiTokenStore,
type IProjectLifecycleStageDuration,
type IFlagResolver,
} from '../../types';
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 type { FeatureLifecycleService } from '../feature-lifecycle/feature-lifecycle-service';
export type TimeRange = 'allTime' | '30d' | '7d';
@ -63,7 +61,6 @@ export interface InstanceStats {
enabledCount: number;
variantCount: number;
};
featureLifeCycles: IProjectLifecycleStageDuration[];
}
export type InstanceStatsSigned = Omit<InstanceStats, 'projects'> & {
@ -94,8 +91,6 @@ export class InstanceStatsService {
private eventStore: IEventStore;
private featureLifecycleService: FeatureLifecycleService;
private apiTokenStore: IApiTokenStore;
private versionService: VersionService;
@ -154,7 +149,6 @@ export class InstanceStatsService {
versionService: VersionService,
getActiveUsers: GetActiveUsers,
getProductionChanges: GetProductionChanges,
featureLifecycleService: FeatureLifecycleService,
) {
this.strategyStore = strategyStore;
this.userStore = userStore;
@ -169,7 +163,6 @@ export class InstanceStatsService {
this.settingStore = settingStore;
this.eventStore = eventStore;
this.clientInstanceStore = clientInstanceStore;
this.featureLifecycleService = featureLifecycleService;
this.logger = getLogger('services/stats-service.js');
this.getActiveUsers = getActiveUsers;
this.getProductionChanges = getProductionChanges;
@ -257,7 +250,6 @@ export class InstanceStatsService {
featureImports,
productionChanges,
previousDayMetricsBucketsCount,
featureLifeCycles,
] = await Promise.all([
this.getToggleCount(),
this.getArchivedToggleCount(),
@ -281,7 +273,6 @@ export class InstanceStatsService {
this.eventStore.filteredCount({ type: FEATURES_IMPORTED }),
this.getProductionChanges(),
this.clientMetricsStore.countPreviousDayHourlyMetricsBuckets(),
this.getAllWithStageDuration(),
]);
return {
@ -314,7 +305,6 @@ export class InstanceStatsService {
featureImports,
productionChanges,
previousDayMetricsBucketsCount,
featureLifeCycles,
};
}
@ -348,11 +338,4 @@ export class InstanceStatsService {
);
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 { createFakeGetActiveUsers } from './features/instance-stats/getActiveUsers';
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 { SchedulerService } from './services';
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 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 eventBus = new EventEmitter();
@ -33,7 +38,8 @@ let environmentStore: IEnvironmentStore;
let statsService: InstanceStatsService;
let stores: IUnleashStores;
let schedulerService: SchedulerService;
let featureLifeCycleStore: FeatureLifecycleStore;
let featureLifeCycleStore: IFeatureLifecycleStore;
let featureLifeCycleReadModel: IFeatureLifecycleReadModel;
let db: ITestDb;
beforeAll(async () => {
@ -59,8 +65,13 @@ beforeAll(async () => {
);
db = await dbInit('metrics_test', getLogger);
const { featureLifecycleService, featureLifecycleStore } =
createFeatureLifecycleService(db.rawDatabase, config);
featureLifeCycleReadModel = new FeatureLifecycleReadModel(
db.rawDatabase,
config.flagResolver,
);
stores.featureLifecycleReadModel = featureLifeCycleReadModel;
featureLifeCycleStore = new FeatureLifecycleStore(db.rawDatabase);
stores.featureLifecycleStore = featureLifeCycleStore;
statsService = new InstanceStatsService(
stores,
@ -68,9 +79,7 @@ beforeAll(async () => {
versionService,
createFakeGetActiveUsers(),
createFakeGetProductionChanges(),
featureLifecycleService,
);
featureLifeCycleStore = featureLifecycleStore;
schedulerService = new SchedulerService(
noLogger,
@ -303,9 +312,13 @@ test('should collect metrics for lifecycle', async () => {
stage: 'initial',
},
]);
const { featureLifeCycles } = await statsService.getStats();
expect(featureLifeCycles).toHaveLength(1);
const stageCount = await featureLifeCycleReadModel.getStageCountByProject();
const stageDurations =
await featureLifeCycleReadModel.getAllWithStageDuration();
expect(stageCount).toHaveLength(1);
expect(stageDurations).toHaveLength(1);
const metrics = await prometheusRegister.metrics();
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,
maxConstraintsPerStrategyResult,
stageCountByProjectResult,
stageDurationByProject,
] = await Promise.all([
stores.featureStrategiesReadModel.getMaxFeatureStrategies(),
stores.featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies(),
stores.featureStrategiesReadModel.getMaxConstraintValues(),
stores.featureStrategiesReadModel.getMaxConstraintsPerStrategy(),
stores.featureLifecycleReadModel.getStageCountByProject(),
stores.featureLifecycleReadModel.getAllWithStageDuration(),
]);
featureFlagsTotal.reset();
@ -326,7 +328,7 @@ export default class MetricsMonitor {
serviceAccounts.reset();
serviceAccounts.set(stats.serviceAccounts);
stats.featureLifeCycles.forEach((stage) => {
stageDurationByProject.forEach((stage) => {
featureLifecycleStageDuration
.labels({
stage: stage.stage,

View File

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