1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-24 01:18:01 +02:00

feat: add project id to prometheus and feature flag (#7008)

Now we are also sending project id to prometheus, also querying from
database. This sets us up for grafana dashboard.
Also put the metrics behind flag, just incase it causes cpu/memory
issues.
This commit is contained in:
Jaanus Sellin 2024-05-08 15:19:23 +03:00 committed by GitHub
parent 81439d23f3
commit cd49ae2a26
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 62 additions and 23 deletions

View File

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

View File

@ -144,26 +144,31 @@ test('can find feature lifecycle stage timings', async () => {
{ {
feature: 'a', feature: 'a',
stage: 'initial', stage: 'initial',
project: 'default',
enteredStageAt: minusTenMinutes, enteredStageAt: minusTenMinutes,
}, },
{ {
feature: 'b', feature: 'b',
stage: 'initial', stage: 'initial',
project: 'default',
enteredStageAt: minusTenMinutes, enteredStageAt: minusTenMinutes,
}, },
{ {
feature: 'a', feature: 'a',
stage: 'pre-live', stage: 'pre-live',
project: 'default',
enteredStageAt: minusOneMinute, enteredStageAt: minusOneMinute,
}, },
{ {
feature: 'b', feature: 'b',
stage: 'live', stage: 'live',
project: 'default',
enteredStageAt: minusOneMinute, enteredStageAt: minusOneMinute,
}, },
{ {
feature: 'c', feature: 'c',
stage: 'initial', stage: 'initial',
project: 'default',
enteredStageAt: minusTenMinutes, enteredStageAt: minusTenMinutes,
}, },
]); ]);

View File

@ -14,7 +14,7 @@ import {
type IUnleashConfig, type IUnleashConfig,
} from '../../types'; } from '../../types';
import type { import type {
FeatureLifecycleFullItem, FeatureLifecycleProjectItem,
FeatureLifecycleView, FeatureLifecycleView,
IFeatureLifecycleStore, IFeatureLifecycleStore,
} from './feature-lifecycle-store-type'; } from './feature-lifecycle-store-type';
@ -215,10 +215,10 @@ export class FeatureLifecycleService extends EventEmitter {
} }
public calculateStageDurations( public calculateStageDurations(
featureLifeCycles: FeatureLifecycleFullItem[], featureLifeCycles: FeatureLifecycleProjectItem[],
) { ) {
const groupedByFeature = featureLifeCycles.reduce<{ const groupedByFeature = featureLifeCycles.reduce<{
[feature: string]: FeatureLifecycleFullItem[]; [feature: string]: FeatureLifecycleProjectItem[];
}>((acc, curr) => { }>((acc, curr) => {
if (!acc[curr.feature]) { if (!acc[curr.feature]) {
acc[curr.feature] = []; acc[curr.feature] = [];
@ -247,6 +247,7 @@ export class FeatureLifecycleService extends EventEmitter {
times.push({ times.push({
feature: stage.feature, feature: stage.feature,
stage: stage.stage, stage: stage.stage,
project: stage.project,
duration, duration,
}); });
}); });

View File

@ -7,14 +7,15 @@ export type FeatureLifecycleStage = {
export type FeatureLifecycleView = IFeatureLifecycleStage[]; export type FeatureLifecycleView = IFeatureLifecycleStage[];
export type FeatureLifecycleFullItem = FeatureLifecycleStage & { export type FeatureLifecycleProjectItem = FeatureLifecycleStage & {
enteredStageAt: Date; enteredStageAt: Date;
project: string;
}; };
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[]>; getAll(): Promise<FeatureLifecycleProjectItem[]>;
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>;

View File

@ -2,17 +2,21 @@ import type {
FeatureLifecycleStage, FeatureLifecycleStage,
IFeatureLifecycleStore, IFeatureLifecycleStore,
FeatureLifecycleView, FeatureLifecycleView,
FeatureLifecycleFullItem, FeatureLifecycleProjectItem,
} 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';
type DBType = { type DBType = {
feature: string;
stage: StageName; stage: StageName;
created_at: string; created_at: string;
}; };
type DBProjectType = DBType & {
feature: string;
project: string;
};
export class FeatureLifecycleStore implements IFeatureLifecycleStore { export class FeatureLifecycleStore implements IFeatureLifecycleStore {
private db: Db; private db: Db;
@ -58,17 +62,20 @@ export class FeatureLifecycleStore implements IFeatureLifecycleStore {
})); }));
} }
async getAll(): Promise<FeatureLifecycleFullItem[]> { async getAll(): Promise<FeatureLifecycleProjectItem[]> {
const results = await this.db('feature_lifecycles').orderBy( const results = await this.db('feature_lifecycles as flc')
'created_at', .select('flc.feature', 'flc.stage', 'flc.created_at', 'f.project')
'asc', .leftJoin('features f', 'f.name', 'flc.feature')
); .orderBy('created_at', 'asc');
return results.map(({ feature, stage, created_at }: DBType) => ({ return results.map(
({ feature, stage, created_at, project }: DBProjectType) => ({
feature, feature,
stage, stage,
project,
enteredStageAt: new Date(created_at), enteredStageAt: new Date(created_at),
})); }),
);
} }
async delete(feature: string): Promise<void> { async delete(feature: string): Promise<void> {

View File

@ -23,6 +23,7 @@ import {
FEATURES_IMPORTED, FEATURES_IMPORTED,
type IApiTokenStore, type IApiTokenStore,
type IFeatureLifecycleStageDuration, type IFeatureLifecycleStageDuration,
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';
@ -105,6 +106,8 @@ export class InstanceStatsService {
private clientMetricsStore: IClientMetricsStoreV2; private clientMetricsStore: IClientMetricsStoreV2;
private flagResolver: IFlagResolver;
private appCount?: Partial<{ [key in TimeRange]: number }>; private appCount?: Partial<{ [key in TimeRange]: number }>;
private getActiveUsers: GetActiveUsers; private getActiveUsers: GetActiveUsers;
@ -144,7 +147,10 @@ export class InstanceStatsService {
| 'apiTokenStore' | 'apiTokenStore'
| 'clientMetricsStoreV2' | 'clientMetricsStoreV2'
>, >,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>, {
getLogger,
flagResolver,
}: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>,
versionService: VersionService, versionService: VersionService,
getActiveUsers: GetActiveUsers, getActiveUsers: GetActiveUsers,
getProductionChanges: GetProductionChanges, getProductionChanges: GetProductionChanges,
@ -169,6 +175,7 @@ export class InstanceStatsService {
this.getProductionChanges = getProductionChanges; this.getProductionChanges = getProductionChanges;
this.apiTokenStore = apiTokenStore; this.apiTokenStore = apiTokenStore;
this.clientMetricsStore = clientMetricsStoreV2; this.clientMetricsStore = clientMetricsStoreV2;
this.flagResolver = flagResolver;
} }
async refreshAppCountSnapshot(): Promise< async refreshAppCountSnapshot(): Promise<
@ -274,7 +281,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(), this.getAllWithStageDuration(),
]); ]);
return { return {
@ -341,4 +348,11 @@ export class InstanceStatsService {
); );
return { ...instanceStats, sum, projects: totalProjects }; return { ...instanceStats, sum, projects: totalProjects };
} }
async getAllWithStageDuration(): Promise<IFeatureLifecycleStageDuration[]> {
if (this.flagResolver.isEnabled('featureLifecycleMetrics')) {
return this.featureLifecycleService.getAllWithStageDuration();
}
return [];
}
} }

View File

@ -30,11 +30,17 @@ let environmentStore: IEnvironmentStore;
let statsService: InstanceStatsService; let statsService: InstanceStatsService;
let stores: IUnleashStores; let stores: IUnleashStores;
let schedulerService: SchedulerService; let schedulerService: SchedulerService;
beforeAll(async () => { beforeAll(async () => {
const config = createTestConfig({ const config = createTestConfig({
server: { server: {
serverMetrics: true, serverMetrics: true,
}, },
experimental: {
flags: {
featureLifecycleMetrics: true,
},
},
}); });
stores = createStores(); stores = createStores();
eventStore = stores.eventStore; eventStore = stores.eventStore;

View File

@ -261,7 +261,7 @@ export default class MetricsMonitor {
const featureLifecycleStageDuration = createHistogram({ const featureLifecycleStageDuration = createHistogram({
name: 'feature_lifecycle_stage_duration', name: 'feature_lifecycle_stage_duration',
labelNames: ['feature_id', 'stage'], labelNames: ['feature_id', 'stage', 'project_id'],
help: 'Duration of feature lifecycle stages', help: 'Duration of feature lifecycle stages',
}); });
@ -294,6 +294,7 @@ export default class MetricsMonitor {
.labels({ .labels({
feature_id: stage.feature, feature_id: stage.feature,
stage: stage.stage, stage: stage.stage,
project_id: stage.project,
}) })
.observe(stage.duration); .observe(stage.duration);
}); });

View File

@ -128,6 +128,7 @@ class InstanceAdminController extends Controller {
featureLifeCycles: [ featureLifeCycles: [
{ {
feature: 'feature1', feature: 'feature1',
project: 'default',
stage: 'archived', stage: 'archived',
duration: 2000, duration: 2000,
}, },

View File

@ -54,6 +54,7 @@ export type IFlagKey =
| 'disableShowContextFieldSelectionValues' | 'disableShowContextFieldSelectionValues'
| 'projectOverviewRefactorFeedback' | 'projectOverviewRefactorFeedback'
| 'featureLifecycle' | 'featureLifecycle'
| 'featureLifecycleMetrics'
| 'projectListFilterMyProjects' | 'projectListFilterMyProjects'
| 'projectsListNewCards' | 'projectsListNewCards'
| 'parseProjectFromSession' | 'parseProjectFromSession'

View File

@ -168,6 +168,7 @@ export interface IFeatureLifecycleStage {
export type IFeatureLifecycleStageDuration = FeatureLifecycleStage & { export type IFeatureLifecycleStageDuration = FeatureLifecycleStage & {
duration: number; duration: number;
project: string;
}; };
export interface IFeatureDependency { export interface IFeatureDependency {