Skip to content

Commit

Permalink
refactor: lifecycle stage duration outside instance stats (#7442)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew authored Jun 25, 2024
1 parent 6a9a2c6 commit c3fa468
Show file tree
Hide file tree
Showing 12 changed files with 74 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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([]);
}
Expand Down
13 changes: 0 additions & 13 deletions src/lib/features/feature-lifecycle/fake-feature-lifecycle-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type {
FeatureLifecycleStage,
IFeatureLifecycleStore,
FeatureLifecycleView,
FeatureLifecycleProjectItem,
} from './feature-lifecycle-store-type';

export class FakeFeatureLifecycleStore implements IFeatureLifecycleStore {
Expand Down Expand Up @@ -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] = [];
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { IFeatureLifecycleStage, StageName } from '../../types';
import type {
IFeatureLifecycleStage,
IProjectLifecycleStageDuration,
StageName,
} from '../../types';

export type StageCount = {
stage: StageName;
Expand All @@ -15,4 +19,5 @@ export interface IFeatureLifecycleReadModel {
): Promise<IFeatureLifecycleStage | undefined>;
getStageCount(): Promise<StageCount[]>;
getStageCountByProject(): Promise<StageCountByProject[]>;
getAllWithStageDuration(): Promise<IProjectLifecycleStageDuration[]>;
}
34 changes: 34 additions & 0 deletions src/lib/features/feature-lifecycle/feature-lifecycle-read-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,6 +21,10 @@ type DBType = {
created_at: Date;
};

type DBProjectType = DBType & {
project: string;
};

export class FeatureLifecycleReadModel implements IFeatureLifecycleReadModel {
private db: Db;

Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
type IEventStore,
type IFeatureEnvironmentStore,
type IFlagResolver,
type IProjectLifecycleStageDuration,
type IUnleashConfig,
} from '../../types';
import type {
Expand All @@ -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';

Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down
12 changes: 0 additions & 12 deletions src/lib/features/instance-stats/createInstanceStatsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -88,10 +84,6 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => {
getLogger,
flagResolver,
);
const { featureLifecycleService } = createFeatureLifecycleService(
db,
config,
);
const instanceStatsServiceStores = {
featureToggleStore,
userStore,
Expand Down Expand Up @@ -133,7 +125,6 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => {
versionService,
getActiveUsers,
getProductionChanges,
featureLifecycleService,
);

return instanceStatsService;
Expand All @@ -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,
Expand Down Expand Up @@ -194,7 +183,6 @@ export const createFakeInstanceStatsService = (config: IUnleashConfig) => {
versionService,
getActiveUsers,
getProductionChanges,
featureLifecycleService,
);

return instanceStatsService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,7 +23,6 @@ beforeEach(() => {
versionService,
createFakeGetActiveUsers(),
createFakeGetProductionChanges(),
createFakeFeatureLifecycleService(config).featureLifecycleService,
);

jest.spyOn(instanceStatsService, 'refreshAppCountSnapshot');
Expand Down
17 changes: 0 additions & 17 deletions src/lib/features/instance-stats/instance-stats-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -63,7 +61,6 @@ export interface InstanceStats {
enabledCount: number;
variantCount: number;
};
featureLifeCycles: IProjectLifecycleStageDuration[];
}

export type InstanceStatsSigned = Omit<InstanceStats, 'projects'> & {
Expand Down Expand Up @@ -94,8 +91,6 @@ export class InstanceStatsService {

private eventStore: IEventStore;

private featureLifecycleService: FeatureLifecycleService;

private apiTokenStore: IApiTokenStore;

private versionService: VersionService;
Expand Down Expand Up @@ -154,7 +149,6 @@ export class InstanceStatsService {
versionService: VersionService,
getActiveUsers: GetActiveUsers,
getProductionChanges: GetProductionChanges,
featureLifecycleService: FeatureLifecycleService,
) {
this.strategyStore = strategyStore;
this.userStore = userStore;
Expand All @@ -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;
Expand Down Expand Up @@ -257,7 +250,6 @@ export class InstanceStatsService {
featureImports,
productionChanges,
previousDayMetricsBucketsCount,
featureLifeCycles,
] = await Promise.all([
this.getToggleCount(),
this.getArchivedToggleCount(),
Expand All @@ -281,7 +273,6 @@ export class InstanceStatsService {
this.eventStore.filteredCount({ type: FEATURES_IMPORTED }),
this.getProductionChanges(),
this.clientMetricsStore.countPreviousDayHourlyMetricsBuckets(),
this.getAllWithStageDuration(),
]);

return {
Expand Down Expand Up @@ -314,7 +305,6 @@ export class InstanceStatsService {
featureImports,
productionChanges,
previousDayMetricsBucketsCount,
featureLifeCycles,
};
}

Expand Down Expand Up @@ -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 [];
}
}
35 changes: 24 additions & 11 deletions src/lib/metrics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 () => {
Expand All @@ -59,18 +65,21 @@ 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,
config,
versionService,
createFakeGetActiveUsers(),
createFakeGetProductionChanges(),
featureLifecycleService,
);
featureLifeCycleStore = featureLifecycleStore;

schedulerService = new SchedulerService(
noLogger,
Expand Down Expand Up @@ -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/);
});
4 changes: 3 additions & 1 deletion src/lib/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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,
Expand Down
Loading

0 comments on commit c3fa468

Please sign in to comment.