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:
parent
81439d23f3
commit
cd49ae2a26
@ -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;
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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>;
|
||||||
|
@ -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> {
|
||||||
|
@ -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 [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
@ -128,6 +128,7 @@ class InstanceAdminController extends Controller {
|
|||||||
featureLifeCycles: [
|
featureLifeCycles: [
|
||||||
{
|
{
|
||||||
feature: 'feature1',
|
feature: 'feature1',
|
||||||
|
project: 'default',
|
||||||
stage: 'archived',
|
stage: 'archived',
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
},
|
},
|
||||||
|
@ -54,6 +54,7 @@ export type IFlagKey =
|
|||||||
| 'disableShowContextFieldSelectionValues'
|
| 'disableShowContextFieldSelectionValues'
|
||||||
| 'projectOverviewRefactorFeedback'
|
| 'projectOverviewRefactorFeedback'
|
||||||
| 'featureLifecycle'
|
| 'featureLifecycle'
|
||||||
|
| 'featureLifecycleMetrics'
|
||||||
| 'projectListFilterMyProjects'
|
| 'projectListFilterMyProjects'
|
||||||
| 'projectsListNewCards'
|
| 'projectsListNewCards'
|
||||||
| 'parseProjectFromSession'
|
| 'parseProjectFromSession'
|
||||||
|
@ -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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user