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:
parent
6a9a2c687d
commit
c3fa468a9d
@ -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([]);
|
||||
}
|
||||
|
@ -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] = [];
|
||||
}
|
||||
|
@ -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[]>;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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>;
|
||||
|
@ -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;
|
||||
|
@ -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');
|
||||
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
|
@ -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/);
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -125,13 +125,6 @@ class InstanceAdminController extends Controller {
|
||||
variantCount: 100,
|
||||
enabledCount: 200,
|
||||
},
|
||||
featureLifeCycles: [
|
||||
{
|
||||
project: 'default',
|
||||
stage: 'archived',
|
||||
duration: 2000,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user