mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-06 01:15:28 +02:00
feat: duration in stage, add feature lifecycle prometheus metrics (#6973)
Introduce a new concept. Duration in stage. Also add it into prometheus metric.
This commit is contained in:
parent
b30860c12f
commit
02440dfed2
@ -2,6 +2,7 @@ import type {
|
|||||||
FeatureLifecycleStage,
|
FeatureLifecycleStage,
|
||||||
IFeatureLifecycleStore,
|
IFeatureLifecycleStore,
|
||||||
FeatureLifecycleView,
|
FeatureLifecycleView,
|
||||||
|
FeatureLifecycleFullItem,
|
||||||
} from './feature-lifecycle-store-type';
|
} from './feature-lifecycle-store-type';
|
||||||
|
|
||||||
export class FakeFeatureLifecycleStore implements IFeatureLifecycleStore {
|
export class FakeFeatureLifecycleStore implements IFeatureLifecycleStore {
|
||||||
@ -35,6 +36,17 @@ export class FakeFeatureLifecycleStore implements IFeatureLifecycleStore {
|
|||||||
return this.lifecycles[feature] || [];
|
return this.lifecycles[feature] || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAll(): Promise<FeatureLifecycleFullItem[]> {
|
||||||
|
const result = Object.entries(this.lifecycles).flatMap(
|
||||||
|
([key, items]): FeatureLifecycleFullItem[] =>
|
||||||
|
items.map((item) => ({
|
||||||
|
...item,
|
||||||
|
feature: key,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
async delete(feature: string): Promise<void> {
|
async delete(feature: string): Promise<void> {
|
||||||
this.lifecycles[feature] = [];
|
this.lifecycles[feature] = [];
|
||||||
}
|
}
|
||||||
|
@ -128,3 +128,71 @@ test('ignores lifecycle state updates when flag disabled', async () => {
|
|||||||
|
|
||||||
expect(lifecycle).toEqual([]);
|
expect(lifecycle).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('can find feature lifecycle stage timings', async () => {
|
||||||
|
const eventBus = new EventEmitter();
|
||||||
|
const { featureLifecycleService, eventStore, environmentStore } =
|
||||||
|
createFakeFeatureLifecycleService({
|
||||||
|
flagResolver: { isEnabled: () => false },
|
||||||
|
eventBus,
|
||||||
|
getLogger: noLoggerProvider,
|
||||||
|
} as unknown as IUnleashConfig);
|
||||||
|
const now = new Date();
|
||||||
|
const minusOneMinute = new Date(now.getTime() - 1 * 60 * 1000);
|
||||||
|
const minusTenMinutes = new Date(now.getTime() - 10 * 60 * 1000);
|
||||||
|
const durations = featureLifecycleService.calculateStageDurations([
|
||||||
|
{
|
||||||
|
feature: 'a',
|
||||||
|
stage: 'initial',
|
||||||
|
enteredStageAt: minusTenMinutes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: 'b',
|
||||||
|
stage: 'initial',
|
||||||
|
enteredStageAt: minusTenMinutes,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: 'a',
|
||||||
|
stage: 'pre-live',
|
||||||
|
enteredStageAt: minusOneMinute,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: 'b',
|
||||||
|
stage: 'live',
|
||||||
|
enteredStageAt: minusOneMinute,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: 'c',
|
||||||
|
stage: 'initial',
|
||||||
|
enteredStageAt: minusTenMinutes,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(durations).toMatchObject([
|
||||||
|
{
|
||||||
|
feature: 'a',
|
||||||
|
stage: 'initial',
|
||||||
|
duration: 9,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: 'a',
|
||||||
|
stage: 'pre-live',
|
||||||
|
duration: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: 'b',
|
||||||
|
stage: 'initial',
|
||||||
|
duration: 9,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: 'b',
|
||||||
|
stage: 'live',
|
||||||
|
duration: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
feature: 'c',
|
||||||
|
stage: 'initial',
|
||||||
|
duration: 10,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
@ -9,10 +9,12 @@ import {
|
|||||||
type IEnvironmentStore,
|
type IEnvironmentStore,
|
||||||
type IEventStore,
|
type IEventStore,
|
||||||
type IFeatureEnvironmentStore,
|
type IFeatureEnvironmentStore,
|
||||||
|
type IFeatureLifecycleStageDuration,
|
||||||
type IFlagResolver,
|
type IFlagResolver,
|
||||||
type IUnleashConfig,
|
type IUnleashConfig,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import type {
|
import type {
|
||||||
|
FeatureLifecycleFullItem,
|
||||||
FeatureLifecycleView,
|
FeatureLifecycleView,
|
||||||
IFeatureLifecycleStore,
|
IFeatureLifecycleStore,
|
||||||
} from './feature-lifecycle-store-type';
|
} from './feature-lifecycle-store-type';
|
||||||
@ -20,6 +22,7 @@ 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 { ValidatedClientMetrics } from '../metrics/shared/schema';
|
import type { ValidatedClientMetrics } from '../metrics/shared/schema';
|
||||||
|
import { differenceInMinutes } from 'date-fns';
|
||||||
|
|
||||||
export const STAGE_ENTERED = 'STAGE_ENTERED';
|
export const STAGE_ENTERED = 'STAGE_ENTERED';
|
||||||
|
|
||||||
@ -203,4 +206,51 @@ 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<
|
||||||
|
IFeatureLifecycleStageDuration[]
|
||||||
|
> {
|
||||||
|
const featureLifeCycles = await this.featureLifecycleStore.getAll();
|
||||||
|
return this.calculateStageDurations(featureLifeCycles);
|
||||||
|
}
|
||||||
|
|
||||||
|
public calculateStageDurations(
|
||||||
|
featureLifeCycles: FeatureLifecycleFullItem[],
|
||||||
|
) {
|
||||||
|
const groupedByFeature = featureLifeCycles.reduce<{
|
||||||
|
[feature: string]: FeatureLifecycleFullItem[];
|
||||||
|
}>((acc, curr) => {
|
||||||
|
if (!acc[curr.feature]) {
|
||||||
|
acc[curr.feature] = [];
|
||||||
|
}
|
||||||
|
acc[curr.feature].push(curr);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const times: IFeatureLifecycleStageDuration[] = [];
|
||||||
|
Object.values(groupedByFeature).forEach((stages) => {
|
||||||
|
stages.sort(
|
||||||
|
(a, b) =>
|
||||||
|
a.enteredStageAt.getTime() - b.enteredStageAt.getTime(),
|
||||||
|
);
|
||||||
|
|
||||||
|
stages.forEach((stage, index) => {
|
||||||
|
const nextStage = stages[index + 1];
|
||||||
|
const endTime = nextStage
|
||||||
|
? nextStage.enteredStageAt
|
||||||
|
: new Date();
|
||||||
|
const duration = differenceInMinutes(
|
||||||
|
endTime,
|
||||||
|
stage.enteredStageAt,
|
||||||
|
);
|
||||||
|
|
||||||
|
times.push({
|
||||||
|
feature: stage.feature,
|
||||||
|
stage: stage.stage,
|
||||||
|
duration,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return times;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,14 @@ export type FeatureLifecycleStage = {
|
|||||||
|
|
||||||
export type FeatureLifecycleView = IFeatureLifecycleStage[];
|
export type FeatureLifecycleView = IFeatureLifecycleStage[];
|
||||||
|
|
||||||
|
export type FeatureLifecycleFullItem = FeatureLifecycleStage & {
|
||||||
|
enteredStageAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
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<FeatureLifecycleFullItem[]>;
|
||||||
stageExists(stage: FeatureLifecycleStage): Promise<boolean>;
|
stageExists(stage: FeatureLifecycleStage): Promise<boolean>;
|
||||||
delete(feature: string): Promise<void>;
|
delete(feature: string): Promise<void>;
|
||||||
deleteStage(stage: FeatureLifecycleStage): Promise<void>;
|
deleteStage(stage: FeatureLifecycleStage): Promise<void>;
|
||||||
|
@ -2,6 +2,7 @@ import type {
|
|||||||
FeatureLifecycleStage,
|
FeatureLifecycleStage,
|
||||||
IFeatureLifecycleStore,
|
IFeatureLifecycleStore,
|
||||||
FeatureLifecycleView,
|
FeatureLifecycleView,
|
||||||
|
FeatureLifecycleFullItem,
|
||||||
} from './feature-lifecycle-store-type';
|
} from './feature-lifecycle-store-type';
|
||||||
import type { Db } from '../../db/db';
|
import type { Db } from '../../db/db';
|
||||||
import type { StageName } from '../../types';
|
import type { StageName } from '../../types';
|
||||||
@ -9,7 +10,7 @@ import type { StageName } from '../../types';
|
|||||||
type DBType = {
|
type DBType = {
|
||||||
feature: string;
|
feature: string;
|
||||||
stage: StageName;
|
stage: StageName;
|
||||||
created_at: Date;
|
created_at: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class FeatureLifecycleStore implements IFeatureLifecycleStore {
|
export class FeatureLifecycleStore implements IFeatureLifecycleStore {
|
||||||
@ -53,7 +54,20 @@ export class FeatureLifecycleStore implements IFeatureLifecycleStore {
|
|||||||
|
|
||||||
return results.map(({ stage, created_at }: DBType) => ({
|
return results.map(({ stage, created_at }: DBType) => ({
|
||||||
stage,
|
stage,
|
||||||
enteredStageAt: created_at,
|
enteredStageAt: new Date(created_at),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll(): Promise<FeatureLifecycleFullItem[]> {
|
||||||
|
const results = await this.db('feature_lifecycles').orderBy(
|
||||||
|
'created_at',
|
||||||
|
'asc',
|
||||||
|
);
|
||||||
|
|
||||||
|
return results.map(({ feature, stage, created_at }: DBType) => ({
|
||||||
|
feature,
|
||||||
|
stage,
|
||||||
|
enteredStageAt: new Date(created_at),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,6 +40,10 @@ 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;
|
||||||
@ -84,6 +88,10 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => {
|
|||||||
getLogger,
|
getLogger,
|
||||||
flagResolver,
|
flagResolver,
|
||||||
);
|
);
|
||||||
|
const { featureLifecycleService } = createFeatureLifecycleService(
|
||||||
|
db,
|
||||||
|
config,
|
||||||
|
);
|
||||||
const instanceStatsServiceStores = {
|
const instanceStatsServiceStores = {
|
||||||
featureToggleStore,
|
featureToggleStore,
|
||||||
userStore,
|
userStore,
|
||||||
@ -125,6 +133,7 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => {
|
|||||||
versionService,
|
versionService,
|
||||||
getActiveUsers,
|
getActiveUsers,
|
||||||
getProductionChanges,
|
getProductionChanges,
|
||||||
|
featureLifecycleService,
|
||||||
);
|
);
|
||||||
|
|
||||||
return instanceStatsService;
|
return instanceStatsService;
|
||||||
@ -146,6 +155,9 @@ export const createFakeInstanceStatsService = (config: IUnleashConfig) => {
|
|||||||
const eventStore = new FakeEventStore();
|
const eventStore = new FakeEventStore();
|
||||||
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,
|
||||||
@ -182,6 +194,7 @@ export const createFakeInstanceStatsService = (config: IUnleashConfig) => {
|
|||||||
versionService,
|
versionService,
|
||||||
getActiveUsers,
|
getActiveUsers,
|
||||||
getProductionChanges,
|
getProductionChanges,
|
||||||
|
featureLifecycleService,
|
||||||
);
|
);
|
||||||
|
|
||||||
return instanceStatsService;
|
return instanceStatsService;
|
||||||
|
@ -4,6 +4,7 @@ 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;
|
||||||
@ -23,6 +24,7 @@ beforeEach(() => {
|
|||||||
versionService,
|
versionService,
|
||||||
createFakeGetActiveUsers(),
|
createFakeGetActiveUsers(),
|
||||||
createFakeGetProductionChanges(),
|
createFakeGetProductionChanges(),
|
||||||
|
createFakeFeatureLifecycleService(config).featureLifecycleService,
|
||||||
);
|
);
|
||||||
|
|
||||||
jest.spyOn(instanceStatsService, 'refreshAppCountSnapshot');
|
jest.spyOn(instanceStatsService, 'refreshAppCountSnapshot');
|
||||||
|
@ -22,11 +22,13 @@ import {
|
|||||||
FEATURES_EXPORTED,
|
FEATURES_EXPORTED,
|
||||||
FEATURES_IMPORTED,
|
FEATURES_IMPORTED,
|
||||||
type IApiTokenStore,
|
type IApiTokenStore,
|
||||||
|
type IFeatureLifecycleStageDuration,
|
||||||
} 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';
|
||||||
|
|
||||||
@ -60,6 +62,7 @@ export interface InstanceStats {
|
|||||||
enabledCount: number;
|
enabledCount: number;
|
||||||
variantCount: number;
|
variantCount: number;
|
||||||
};
|
};
|
||||||
|
featureLifeCycles: IFeatureLifecycleStageDuration[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InstanceStatsSigned = Omit<InstanceStats, 'projects'> & {
|
export type InstanceStatsSigned = Omit<InstanceStats, 'projects'> & {
|
||||||
@ -90,6 +93,8 @@ export class InstanceStatsService {
|
|||||||
|
|
||||||
private eventStore: IEventStore;
|
private eventStore: IEventStore;
|
||||||
|
|
||||||
|
private featureLifecycleService: FeatureLifecycleService;
|
||||||
|
|
||||||
private apiTokenStore: IApiTokenStore;
|
private apiTokenStore: IApiTokenStore;
|
||||||
|
|
||||||
private versionService: VersionService;
|
private versionService: VersionService;
|
||||||
@ -143,6 +148,7 @@ 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;
|
||||||
@ -157,6 +163,7 @@ 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;
|
||||||
@ -243,6 +250,7 @@ export class InstanceStatsService {
|
|||||||
featureImports,
|
featureImports,
|
||||||
productionChanges,
|
productionChanges,
|
||||||
previousDayMetricsBucketsCount,
|
previousDayMetricsBucketsCount,
|
||||||
|
featureLifeCycles,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
this.getToggleCount(),
|
this.getToggleCount(),
|
||||||
this.getArchivedToggleCount(),
|
this.getArchivedToggleCount(),
|
||||||
@ -266,6 +274,7 @@ 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.featureLifecycleService.getAllWithStageDuration(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -298,6 +307,7 @@ export class InstanceStatsService {
|
|||||||
featureImports,
|
featureImports,
|
||||||
productionChanges,
|
productionChanges,
|
||||||
previousDayMetricsBucketsCount,
|
previousDayMetricsBucketsCount,
|
||||||
|
featureLifeCycles,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ import type { IEnvironmentStore, 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 { createFakeFeatureLifecycleService } from './features';
|
||||||
|
|
||||||
const monitor = createMetricsMonitor();
|
const monitor = createMetricsMonitor();
|
||||||
const eventBus = new EventEmitter();
|
const eventBus = new EventEmitter();
|
||||||
@ -45,12 +46,15 @@ beforeAll(async () => {
|
|||||||
createFakeGetActiveUsers(),
|
createFakeGetActiveUsers(),
|
||||||
createFakeGetProductionChanges(),
|
createFakeGetProductionChanges(),
|
||||||
);
|
);
|
||||||
|
const { featureLifecycleService } =
|
||||||
|
createFakeFeatureLifecycleService(config);
|
||||||
statsService = new InstanceStatsService(
|
statsService = new InstanceStatsService(
|
||||||
stores,
|
stores,
|
||||||
config,
|
config,
|
||||||
versionService,
|
versionService,
|
||||||
createFakeGetActiveUsers(),
|
createFakeGetActiveUsers(),
|
||||||
createFakeGetProductionChanges(),
|
createFakeGetProductionChanges(),
|
||||||
|
featureLifecycleService,
|
||||||
);
|
);
|
||||||
|
|
||||||
schedulerService = new SchedulerService(
|
schedulerService = new SchedulerService(
|
||||||
@ -275,3 +279,8 @@ test('should collect metrics for project disabled numbers', async () => {
|
|||||||
/project_environments_disabled{project_id=\"default\"} 1/,
|
/project_environments_disabled{project_id=\"default\"} 1/,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should collect metrics for lifecycle', async () => {
|
||||||
|
const metrics = await prometheusRegister.metrics();
|
||||||
|
expect(metrics).toMatch(/feature_lifecycle_stage_duration/);
|
||||||
|
});
|
||||||
|
@ -259,6 +259,12 @@ export default class MetricsMonitor {
|
|||||||
help: 'Duration of mapFeaturesForClient function',
|
help: 'Duration of mapFeaturesForClient function',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const featureLifecycleStageDuration = createHistogram({
|
||||||
|
name: 'feature_lifecycle_stage_duration',
|
||||||
|
labelNames: ['feature_id', 'stage'],
|
||||||
|
help: 'Duration of feature lifecycle stages',
|
||||||
|
});
|
||||||
|
|
||||||
const projectEnvironmentsDisabled = createCounter({
|
const projectEnvironmentsDisabled = createCounter({
|
||||||
name: 'project_environments_disabled',
|
name: 'project_environments_disabled',
|
||||||
help: 'How many "environment disabled" events we have received for each project',
|
help: 'How many "environment disabled" events we have received for each project',
|
||||||
@ -283,6 +289,15 @@ export default class MetricsMonitor {
|
|||||||
serviceAccounts.reset();
|
serviceAccounts.reset();
|
||||||
serviceAccounts.set(stats.serviceAccounts);
|
serviceAccounts.set(stats.serviceAccounts);
|
||||||
|
|
||||||
|
stats.featureLifeCycles.forEach((stage) => {
|
||||||
|
featureLifecycleStageDuration
|
||||||
|
.labels({
|
||||||
|
feature_id: stage.feature,
|
||||||
|
stage: stage.stage,
|
||||||
|
})
|
||||||
|
.observe(stage.duration);
|
||||||
|
});
|
||||||
|
|
||||||
apiTokens.reset();
|
apiTokens.reset();
|
||||||
|
|
||||||
for (const [type, value] of stats.apiTokens) {
|
for (const [type, value] of stats.apiTokens) {
|
||||||
@ -358,7 +373,10 @@ export default class MetricsMonitor {
|
|||||||
|
|
||||||
rateLimits.reset();
|
rateLimits.reset();
|
||||||
rateLimits
|
rateLimits
|
||||||
.labels({ endpoint: '/api/client/metrics', method: 'POST' })
|
.labels({
|
||||||
|
endpoint: '/api/client/metrics',
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
.set(config.metricsRateLimiting.clientMetricsMaxPerMinute);
|
.set(config.metricsRateLimiting.clientMetricsMaxPerMinute);
|
||||||
rateLimits
|
rateLimits
|
||||||
.labels({
|
.labels({
|
||||||
@ -389,7 +407,10 @@ export default class MetricsMonitor {
|
|||||||
})
|
})
|
||||||
.set(config.rateLimiting.createUserMaxPerMinute);
|
.set(config.rateLimiting.createUserMaxPerMinute);
|
||||||
rateLimits
|
rateLimits
|
||||||
.labels({ endpoint: '/auth/simple', method: 'POST' })
|
.labels({
|
||||||
|
endpoint: '/auth/simple',
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
.set(config.rateLimiting.simpleLoginMaxPerMinute);
|
.set(config.rateLimiting.simpleLoginMaxPerMinute);
|
||||||
rateLimits
|
rateLimits
|
||||||
.labels({
|
.labels({
|
||||||
@ -407,7 +428,6 @@ export default class MetricsMonitor {
|
|||||||
);
|
);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
await schedulerService.schedule(
|
await schedulerService.schedule(
|
||||||
collectStaticCounters.bind(this),
|
collectStaticCounters.bind(this),
|
||||||
hoursToMilliseconds(2),
|
hoursToMilliseconds(2),
|
||||||
@ -419,7 +439,12 @@ export default class MetricsMonitor {
|
|||||||
events.REQUEST_TIME,
|
events.REQUEST_TIME,
|
||||||
({ path, method, time, statusCode, appName }) => {
|
({ path, method, time, statusCode, appName }) => {
|
||||||
requestDuration
|
requestDuration
|
||||||
.labels({ path, method, status: statusCode, appName })
|
.labels({
|
||||||
|
path,
|
||||||
|
method,
|
||||||
|
status: statusCode,
|
||||||
|
appName,
|
||||||
|
})
|
||||||
.observe(time);
|
.observe(time);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -432,7 +457,10 @@ export default class MetricsMonitor {
|
|||||||
events.FUNCTION_TIME,
|
events.FUNCTION_TIME,
|
||||||
({ functionName, className, time }) => {
|
({ functionName, className, time }) => {
|
||||||
functionDuration
|
functionDuration
|
||||||
.labels({ functionName, className })
|
.labels({
|
||||||
|
functionName,
|
||||||
|
className,
|
||||||
|
})
|
||||||
.observe(time);
|
.observe(time);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -446,7 +474,12 @@ export default class MetricsMonitor {
|
|||||||
});
|
});
|
||||||
|
|
||||||
eventBus.on(events.DB_TIME, ({ store, action, time }) => {
|
eventBus.on(events.DB_TIME, ({ store, action, time }) => {
|
||||||
dbDuration.labels({ store, action }).observe(time);
|
dbDuration
|
||||||
|
.labels({
|
||||||
|
store,
|
||||||
|
action,
|
||||||
|
})
|
||||||
|
.observe(time);
|
||||||
});
|
});
|
||||||
|
|
||||||
eventBus.on(events.PROXY_REPOSITORY_CREATED, () => {
|
eventBus.on(events.PROXY_REPOSITORY_CREATED, () => {
|
||||||
@ -704,6 +737,7 @@ export default class MetricsMonitor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMetricsMonitor(): MetricsMonitor {
|
export function createMetricsMonitor(): MetricsMonitor {
|
||||||
return new MetricsMonitor();
|
return new MetricsMonitor();
|
||||||
}
|
}
|
||||||
|
@ -125,6 +125,13 @@ class InstanceAdminController extends Controller {
|
|||||||
variantCount: 100,
|
variantCount: 100,
|
||||||
enabledCount: 200,
|
enabledCount: 200,
|
||||||
},
|
},
|
||||||
|
featureLifeCycles: [
|
||||||
|
{
|
||||||
|
feature: 'feature1',
|
||||||
|
stage: 'archived',
|
||||||
|
duration: 2000,
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import type { IProjectStats } from '../features/project/project-service';
|
|||||||
import type { CreateFeatureStrategySchema } from '../openapi';
|
import type { CreateFeatureStrategySchema } from '../openapi';
|
||||||
import type { ProjectEnvironment } from '../features/project/project-store-type';
|
import type { ProjectEnvironment } from '../features/project/project-store-type';
|
||||||
import type { FeatureSearchEnvironmentSchema } from '../openapi/spec/feature-search-environment-schema';
|
import type { FeatureSearchEnvironmentSchema } from '../openapi/spec/feature-search-environment-schema';
|
||||||
|
import type { FeatureLifecycleStage } from '../features/feature-lifecycle/feature-lifecycle-store-type';
|
||||||
|
|
||||||
export type Operator = (typeof ALL_OPERATORS)[number];
|
export type Operator = (typeof ALL_OPERATORS)[number];
|
||||||
|
|
||||||
@ -159,11 +160,16 @@ export type StageName =
|
|||||||
| 'live'
|
| 'live'
|
||||||
| 'completed'
|
| 'completed'
|
||||||
| 'archived';
|
| 'archived';
|
||||||
|
|
||||||
export interface IFeatureLifecycleStage {
|
export interface IFeatureLifecycleStage {
|
||||||
stage: StageName;
|
stage: StageName;
|
||||||
enteredStageAt: Date;
|
enteredStageAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IFeatureLifecycleStageDuration = FeatureLifecycleStage & {
|
||||||
|
duration: number;
|
||||||
|
};
|
||||||
|
|
||||||
export interface IFeatureDependency {
|
export interface IFeatureDependency {
|
||||||
feature: string;
|
feature: string;
|
||||||
dependency: IDependency;
|
dependency: IDependency;
|
||||||
|
Loading…
Reference in New Issue
Block a user