1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01: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,
IFeatureLifecycleStore,
FeatureLifecycleView,
FeatureLifecycleFullItem,
FeatureLifecycleProjectItem,
} from './feature-lifecycle-store-type';
export class FakeFeatureLifecycleStore implements IFeatureLifecycleStore {
@ -36,12 +36,13 @@ export class FakeFeatureLifecycleStore implements IFeatureLifecycleStore {
return this.lifecycles[feature] || [];
}
async getAll(): Promise<FeatureLifecycleFullItem[]> {
async getAll(): Promise<FeatureLifecycleProjectItem[]> {
const result = Object.entries(this.lifecycles).flatMap(
([key, items]): FeatureLifecycleFullItem[] =>
([key, items]): FeatureLifecycleProjectItem[] =>
items.map((item) => ({
...item,
feature: key,
project: 'fake-project',
})),
);
return result;

View File

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

View File

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

View File

@ -7,14 +7,15 @@ export type FeatureLifecycleStage = {
export type FeatureLifecycleView = IFeatureLifecycleStage[];
export type FeatureLifecycleFullItem = FeatureLifecycleStage & {
export type FeatureLifecycleProjectItem = FeatureLifecycleStage & {
enteredStageAt: Date;
project: string;
};
export interface IFeatureLifecycleStore {
insert(featureLifecycleStages: FeatureLifecycleStage[]): Promise<void>;
get(feature: string): Promise<FeatureLifecycleView>;
getAll(): Promise<FeatureLifecycleFullItem[]>;
getAll(): Promise<FeatureLifecycleProjectItem[]>;
stageExists(stage: FeatureLifecycleStage): Promise<boolean>;
delete(feature: string): Promise<void>;
deleteStage(stage: FeatureLifecycleStage): Promise<void>;

View File

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

View File

@ -23,6 +23,7 @@ import {
FEATURES_IMPORTED,
type IApiTokenStore,
type IFeatureLifecycleStageDuration,
type IFlagResolver,
} from '../../types';
import { CUSTOM_ROOT_ROLE_TYPE } from '../../util';
import type { GetActiveUsers } from './getActiveUsers';
@ -105,6 +106,8 @@ export class InstanceStatsService {
private clientMetricsStore: IClientMetricsStoreV2;
private flagResolver: IFlagResolver;
private appCount?: Partial<{ [key in TimeRange]: number }>;
private getActiveUsers: GetActiveUsers;
@ -144,7 +147,10 @@ export class InstanceStatsService {
| 'apiTokenStore'
| 'clientMetricsStoreV2'
>,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
{
getLogger,
flagResolver,
}: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>,
versionService: VersionService,
getActiveUsers: GetActiveUsers,
getProductionChanges: GetProductionChanges,
@ -169,6 +175,7 @@ export class InstanceStatsService {
this.getProductionChanges = getProductionChanges;
this.apiTokenStore = apiTokenStore;
this.clientMetricsStore = clientMetricsStoreV2;
this.flagResolver = flagResolver;
}
async refreshAppCountSnapshot(): Promise<
@ -274,7 +281,7 @@ export class InstanceStatsService {
this.eventStore.filteredCount({ type: FEATURES_IMPORTED }),
this.getProductionChanges(),
this.clientMetricsStore.countPreviousDayHourlyMetricsBuckets(),
this.featureLifecycleService.getAllWithStageDuration(),
this.getAllWithStageDuration(),
]);
return {
@ -341,4 +348,11 @@ export class InstanceStatsService {
);
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 stores: IUnleashStores;
let schedulerService: SchedulerService;
beforeAll(async () => {
const config = createTestConfig({
server: {
serverMetrics: true,
},
experimental: {
flags: {
featureLifecycleMetrics: true,
},
},
});
stores = createStores();
eventStore = stores.eventStore;

View File

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

View File

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

View File

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

View File

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