mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
feat: largest projects and features metric (#7459)
This commit is contained in:
parent
e8511789fd
commit
72de574012
@ -48,6 +48,7 @@ import { FeatureLifecycleStore } from '../features/feature-lifecycle/feature-lif
|
|||||||
import { ProjectFlagCreatorsReadModel } from '../features/project/project-flag-creators-read-model';
|
import { ProjectFlagCreatorsReadModel } from '../features/project/project-flag-creators-read-model';
|
||||||
import { FeatureStrategiesReadModel } from '../features/feature-toggle/feature-strategies-read-model';
|
import { FeatureStrategiesReadModel } from '../features/feature-toggle/feature-strategies-read-model';
|
||||||
import { FeatureLifecycleReadModel } from '../features/feature-lifecycle/feature-lifecycle-read-model';
|
import { FeatureLifecycleReadModel } from '../features/feature-lifecycle/feature-lifecycle-read-model';
|
||||||
|
import { LargestResourcesReadModel } from '../features/metrics/sizes/largest-resources-read-model';
|
||||||
|
|
||||||
export const createStores = (
|
export const createStores = (
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
@ -166,6 +167,7 @@ export const createStores = (
|
|||||||
db,
|
db,
|
||||||
config.flagResolver,
|
config.flagResolver,
|
||||||
),
|
),
|
||||||
|
largestResourcesReadModel: new LargestResourcesReadModel(db),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import type { IFeatureToggleStore } from '../feature-toggle/types/feature-toggle
|
|||||||
import type { IFlagResolver } from '../../types';
|
import type { IFlagResolver } from '../../types';
|
||||||
|
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
let featureLifeycycleReadModel: IFeatureLifecycleReadModel;
|
let featureLifecycleReadModel: IFeatureLifecycleReadModel;
|
||||||
let featureLifecycleStore: IFeatureLifecycleStore;
|
let featureLifecycleStore: IFeatureLifecycleStore;
|
||||||
let featureToggleStore: IFeatureToggleStore;
|
let featureToggleStore: IFeatureToggleStore;
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ const alwaysOnFlagResolver = {
|
|||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('feature_lifecycle_read_model', getLogger);
|
db = await dbInit('feature_lifecycle_read_model', getLogger);
|
||||||
featureLifeycycleReadModel = new FeatureLifecycleReadModel(
|
featureLifecycleReadModel = new FeatureLifecycleReadModel(
|
||||||
db.rawDatabase,
|
db.rawDatabase,
|
||||||
alwaysOnFlagResolver,
|
alwaysOnFlagResolver,
|
||||||
);
|
);
|
||||||
@ -59,14 +59,14 @@ test('can return stage count', async () => {
|
|||||||
{ feature: 'featureA', stage: 'pre-live' },
|
{ feature: 'featureA', stage: 'pre-live' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const stageCount = await featureLifeycycleReadModel.getStageCount();
|
const stageCount = await featureLifecycleReadModel.getStageCount();
|
||||||
expect(stageCount).toMatchObject([
|
expect(stageCount).toMatchObject([
|
||||||
{ stage: 'pre-live', count: 1 },
|
{ stage: 'pre-live', count: 1 },
|
||||||
{ stage: 'initial', count: 2 },
|
{ stage: 'initial', count: 2 },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const stageCountByProject =
|
const stageCountByProject =
|
||||||
await featureLifeycycleReadModel.getStageCountByProject();
|
await featureLifecycleReadModel.getStageCountByProject();
|
||||||
expect(stageCountByProject).toMatchObject([
|
expect(stageCountByProject).toMatchObject([
|
||||||
{ project: 'default', stage: 'pre-live', count: 1 },
|
{ project: 'default', stage: 'pre-live', count: 1 },
|
||||||
{ project: 'default', stage: 'initial', count: 2 },
|
{ project: 'default', stage: 'initial', count: 2 },
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
import type { ILargestResourcesReadModel } from './largest-resources-read-model-type';
|
||||||
|
|
||||||
|
export class FakeLargestResourcesReadModel
|
||||||
|
implements ILargestResourcesReadModel
|
||||||
|
{
|
||||||
|
async getLargestProjectEnvironments(
|
||||||
|
limit: number,
|
||||||
|
): Promise<{ project: string; environment: string; size: number }[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
async getLargestFeatureEnvironments(
|
||||||
|
limit: number,
|
||||||
|
): Promise<{ feature: string; environment: string; size: number }[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
export interface ILargestResourcesReadModel {
|
||||||
|
getLargestProjectEnvironments(
|
||||||
|
limit: number,
|
||||||
|
): Promise<Array<{ project: string; environment: string; size: number }>>;
|
||||||
|
getLargestFeatureEnvironments(
|
||||||
|
limit: number,
|
||||||
|
): Promise<Array<{ feature: string; environment: string; size: number }>>;
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
import type { ILargestResourcesReadModel } from './largest-resources-read-model-type';
|
||||||
|
import dbInit, {
|
||||||
|
type ITestDb,
|
||||||
|
} from '../../../../test/e2e/helpers/database-init';
|
||||||
|
import type { IFeatureToggleStore } from '../../feature-toggle/types/feature-toggle-store-type';
|
||||||
|
import getLogger from '../../../../test/fixtures/no-logger';
|
||||||
|
import type { IFeatureStrategiesStore } from '../../feature-toggle/types/feature-toggle-strategies-store-type';
|
||||||
|
import type { IFeatureStrategy } from '../../../types';
|
||||||
|
|
||||||
|
let db: ITestDb;
|
||||||
|
let largestResourcesReadModel: ILargestResourcesReadModel;
|
||||||
|
let featureToggleStore: IFeatureToggleStore;
|
||||||
|
let featureStrategiesStore: IFeatureStrategiesStore;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
db = await dbInit('largest_resources_read_model', getLogger);
|
||||||
|
featureToggleStore = db.stores.featureToggleStore;
|
||||||
|
featureStrategiesStore = db.stores.featureStrategiesStore;
|
||||||
|
largestResourcesReadModel = db.stores.largestResourcesReadModel;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
if (db) {
|
||||||
|
await db.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await featureToggleStore.deleteAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
type FeatureConfig = Pick<
|
||||||
|
IFeatureStrategy,
|
||||||
|
'featureName' | 'constraints' | 'parameters' | 'variants'
|
||||||
|
>;
|
||||||
|
const createFeature = async (config: FeatureConfig) => {
|
||||||
|
await featureToggleStore.create('default', {
|
||||||
|
name: config.featureName,
|
||||||
|
createdByUserId: 9999,
|
||||||
|
});
|
||||||
|
await featureStrategiesStore.createStrategyFeatureEnv({
|
||||||
|
strategyName: 'flexibleRollout',
|
||||||
|
projectId: 'default',
|
||||||
|
environment: 'default',
|
||||||
|
featureName: config.featureName,
|
||||||
|
constraints: config.constraints,
|
||||||
|
parameters: config.parameters,
|
||||||
|
variants: config.variants,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
test('can calculate resource size', async () => {
|
||||||
|
await createFeature({
|
||||||
|
featureName: 'featureA',
|
||||||
|
parameters: {
|
||||||
|
groupId: 'flag_init_test_1',
|
||||||
|
rollout: '25',
|
||||||
|
stickiness: 'default',
|
||||||
|
},
|
||||||
|
constraints: [
|
||||||
|
{
|
||||||
|
contextName: 'clientId',
|
||||||
|
operator: 'IN',
|
||||||
|
values: ['1', '2', '3', '4', '5', '6'],
|
||||||
|
caseInsensitive: false,
|
||||||
|
inverted: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
name: 'a',
|
||||||
|
weight: 1000,
|
||||||
|
weightType: 'fix',
|
||||||
|
stickiness: 'default',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await createFeature({
|
||||||
|
featureName: 'featureB',
|
||||||
|
parameters: {},
|
||||||
|
constraints: [],
|
||||||
|
variants: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const [project] =
|
||||||
|
await largestResourcesReadModel.getLargestProjectEnvironments(1);
|
||||||
|
const [feature1, feature2] =
|
||||||
|
await largestResourcesReadModel.getLargestFeatureEnvironments(2);
|
||||||
|
|
||||||
|
expect(project.size).toBeGreaterThan(400);
|
||||||
|
expect(project.size).toBe(feature1.size + feature2.size);
|
||||||
|
expect(feature1.size).toBeGreaterThan(feature2.size);
|
||||||
|
});
|
@ -0,0 +1,76 @@
|
|||||||
|
import type { Db } from '../../../db/db';
|
||||||
|
import type { ILargestResourcesReadModel } from './largest-resources-read-model-type';
|
||||||
|
|
||||||
|
export class LargestResourcesReadModel implements ILargestResourcesReadModel {
|
||||||
|
private db: Db;
|
||||||
|
|
||||||
|
constructor(db: Db) {
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLargestProjectEnvironments(
|
||||||
|
limit: number,
|
||||||
|
): Promise<Array<{ project: string; environment: string; size: number }>> {
|
||||||
|
const { rows } = await this.db.raw(`
|
||||||
|
WITH ProjectSizes AS (
|
||||||
|
SELECT
|
||||||
|
project_name,
|
||||||
|
environment,
|
||||||
|
SUM(pg_column_size(constraints) + pg_column_size(variants) + pg_column_size(parameters)) AS total_size
|
||||||
|
FROM
|
||||||
|
feature_strategies
|
||||||
|
GROUP BY
|
||||||
|
project_name,
|
||||||
|
environment
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
project_name,
|
||||||
|
environment,
|
||||||
|
total_size
|
||||||
|
FROM
|
||||||
|
ProjectSizes
|
||||||
|
ORDER BY
|
||||||
|
total_size DESC
|
||||||
|
LIMIT ${limit}
|
||||||
|
`);
|
||||||
|
|
||||||
|
return rows.map((row) => ({
|
||||||
|
project: row.project_name,
|
||||||
|
environment: row.environment,
|
||||||
|
size: Number(row.total_size),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLargestFeatureEnvironments(
|
||||||
|
limit: number,
|
||||||
|
): Promise<Array<{ feature: string; environment: string; size: number }>> {
|
||||||
|
const { rows } = await this.db.raw(`
|
||||||
|
WITH FeatureSizes AS (
|
||||||
|
SELECT
|
||||||
|
feature_name,
|
||||||
|
environment,
|
||||||
|
SUM(pg_column_size(constraints) + pg_column_size(variants) + pg_column_size(parameters)) AS total_size
|
||||||
|
FROM
|
||||||
|
feature_strategies
|
||||||
|
GROUP BY
|
||||||
|
feature_name,
|
||||||
|
environment
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
feature_name,
|
||||||
|
environment,
|
||||||
|
total_size
|
||||||
|
FROM
|
||||||
|
FeatureSizes
|
||||||
|
ORDER BY
|
||||||
|
total_size DESC
|
||||||
|
LIMIT ${limit}
|
||||||
|
`);
|
||||||
|
|
||||||
|
return rows.map((row) => ({
|
||||||
|
feature: row.feature_name,
|
||||||
|
environment: row.environment,
|
||||||
|
size: Number(row.total_size),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
@ -129,6 +129,16 @@ export default class MetricsMonitor {
|
|||||||
help: 'Maximum number of constraints used on a single strategy',
|
help: 'Maximum number of constraints used on a single strategy',
|
||||||
labelNames: ['feature', 'environment'],
|
labelNames: ['feature', 'environment'],
|
||||||
});
|
});
|
||||||
|
const largestProjectEnvironment = createGauge({
|
||||||
|
name: 'largest_project_environment_size',
|
||||||
|
help: 'The largest project environment size (bytes) based on strategies, constraints, variants and parameters',
|
||||||
|
labelNames: ['project', 'environment'],
|
||||||
|
});
|
||||||
|
const largestFeatureEnvironment = createGauge({
|
||||||
|
name: 'largest_feature_environment_size',
|
||||||
|
help: 'The largest feature environment size (bytes) base on strategies, constraints, variants and parameters',
|
||||||
|
labelNames: ['feature', 'environment'],
|
||||||
|
});
|
||||||
|
|
||||||
const featureTogglesArchivedTotal = createGauge({
|
const featureTogglesArchivedTotal = createGauge({
|
||||||
name: 'feature_toggles_archived_total',
|
name: 'feature_toggles_archived_total',
|
||||||
@ -313,6 +323,8 @@ export default class MetricsMonitor {
|
|||||||
maxConstraintsPerStrategyResult,
|
maxConstraintsPerStrategyResult,
|
||||||
stageCountByProjectResult,
|
stageCountByProjectResult,
|
||||||
stageDurationByProject,
|
stageDurationByProject,
|
||||||
|
largestProjectEnvironments,
|
||||||
|
largestFeatureEnvironments,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
stores.featureStrategiesReadModel.getMaxFeatureStrategies(),
|
stores.featureStrategiesReadModel.getMaxFeatureStrategies(),
|
||||||
stores.featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies(),
|
stores.featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies(),
|
||||||
@ -320,6 +332,12 @@ export default class MetricsMonitor {
|
|||||||
stores.featureStrategiesReadModel.getMaxConstraintsPerStrategy(),
|
stores.featureStrategiesReadModel.getMaxConstraintsPerStrategy(),
|
||||||
stores.featureLifecycleReadModel.getStageCountByProject(),
|
stores.featureLifecycleReadModel.getStageCountByProject(),
|
||||||
stores.featureLifecycleReadModel.getAllWithStageDuration(),
|
stores.featureLifecycleReadModel.getAllWithStageDuration(),
|
||||||
|
stores.largestResourcesReadModel.getLargestProjectEnvironments(
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
stores.largestResourcesReadModel.getLargestFeatureEnvironments(
|
||||||
|
1,
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
featureFlagsTotal.reset();
|
featureFlagsTotal.reset();
|
||||||
@ -403,6 +421,28 @@ export default class MetricsMonitor {
|
|||||||
.set(maxConstraintsPerStrategyResult.count);
|
.set(maxConstraintsPerStrategyResult.count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (largestProjectEnvironments.length > 0) {
|
||||||
|
const projectEnvironment = largestProjectEnvironments[0];
|
||||||
|
largestProjectEnvironment.reset();
|
||||||
|
largestProjectEnvironment
|
||||||
|
.labels({
|
||||||
|
project: projectEnvironment.project,
|
||||||
|
environment: projectEnvironment.environment,
|
||||||
|
})
|
||||||
|
.set(projectEnvironment.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (largestFeatureEnvironments.length > 0) {
|
||||||
|
const featureEnvironment = largestFeatureEnvironments[0];
|
||||||
|
largestFeatureEnvironment.reset();
|
||||||
|
largestFeatureEnvironment
|
||||||
|
.labels({
|
||||||
|
feature: featureEnvironment.feature,
|
||||||
|
environment: featureEnvironment.environment,
|
||||||
|
})
|
||||||
|
.set(featureEnvironment.size);
|
||||||
|
}
|
||||||
|
|
||||||
enabledMetricsBucketsPreviousDay.reset();
|
enabledMetricsBucketsPreviousDay.reset();
|
||||||
enabledMetricsBucketsPreviousDay.set(
|
enabledMetricsBucketsPreviousDay.set(
|
||||||
stats.previousDayMetricsBucketsCount.enabledCount,
|
stats.previousDayMetricsBucketsCount.enabledCount,
|
||||||
|
@ -45,6 +45,7 @@ import { IFeatureLifecycleStore } from '../features/feature-lifecycle/feature-li
|
|||||||
import { IProjectFlagCreatorsReadModel } from '../features/project/project-flag-creators-read-model.type';
|
import { IProjectFlagCreatorsReadModel } from '../features/project/project-flag-creators-read-model.type';
|
||||||
import { IFeatureStrategiesReadModel } from '../features/feature-toggle/types/feature-strategies-read-model-type';
|
import { IFeatureStrategiesReadModel } from '../features/feature-toggle/types/feature-strategies-read-model-type';
|
||||||
import { IFeatureLifecycleReadModel } from '../features/feature-lifecycle/feature-lifecycle-read-model-type';
|
import { IFeatureLifecycleReadModel } from '../features/feature-lifecycle/feature-lifecycle-read-model-type';
|
||||||
|
import { ILargestResourcesReadModel } from '../features/metrics/sizes/largest-resources-read-model-type';
|
||||||
|
|
||||||
export interface IUnleashStores {
|
export interface IUnleashStores {
|
||||||
accessStore: IAccessStore;
|
accessStore: IAccessStore;
|
||||||
@ -94,6 +95,7 @@ export interface IUnleashStores {
|
|||||||
featureLifecycleStore: IFeatureLifecycleStore;
|
featureLifecycleStore: IFeatureLifecycleStore;
|
||||||
featureStrategiesReadModel: IFeatureStrategiesReadModel;
|
featureStrategiesReadModel: IFeatureStrategiesReadModel;
|
||||||
featureLifecycleReadModel: IFeatureLifecycleReadModel;
|
featureLifecycleReadModel: IFeatureLifecycleReadModel;
|
||||||
|
largestResourcesReadModel: ILargestResourcesReadModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -142,4 +144,5 @@ export {
|
|||||||
IProjectFlagCreatorsReadModel,
|
IProjectFlagCreatorsReadModel,
|
||||||
IFeatureStrategiesReadModel,
|
IFeatureStrategiesReadModel,
|
||||||
IFeatureLifecycleReadModel,
|
IFeatureLifecycleReadModel,
|
||||||
|
ILargestResourcesReadModel,
|
||||||
};
|
};
|
||||||
|
2
src/test/fixtures/store.ts
vendored
2
src/test/fixtures/store.ts
vendored
@ -48,6 +48,7 @@ import { FakeFeatureLifecycleStore } from '../../lib/features/feature-lifecycle/
|
|||||||
import { FakeProjectFlagCreatorsReadModel } from '../../lib/features/project/fake-project-flag-creators-read-model';
|
import { FakeProjectFlagCreatorsReadModel } from '../../lib/features/project/fake-project-flag-creators-read-model';
|
||||||
import { FakeFeatureStrategiesReadModel } from '../../lib/features/feature-toggle/fake-feature-strategies-read-model';
|
import { FakeFeatureStrategiesReadModel } from '../../lib/features/feature-toggle/fake-feature-strategies-read-model';
|
||||||
import { FakeFeatureLifecycleReadModel } from '../../lib/features/feature-lifecycle/fake-feature-lifecycle-read-model';
|
import { FakeFeatureLifecycleReadModel } from '../../lib/features/feature-lifecycle/fake-feature-lifecycle-read-model';
|
||||||
|
import { FakeLargestResourcesReadModel } from '../../lib/features/metrics/sizes/fake-largest-resources-read-model';
|
||||||
|
|
||||||
const db = {
|
const db = {
|
||||||
select: () => ({
|
select: () => ({
|
||||||
@ -105,6 +106,7 @@ const createStores: () => IUnleashStores = () => {
|
|||||||
featureLifecycleStore: new FakeFeatureLifecycleStore(),
|
featureLifecycleStore: new FakeFeatureLifecycleStore(),
|
||||||
featureStrategiesReadModel: new FakeFeatureStrategiesReadModel(),
|
featureStrategiesReadModel: new FakeFeatureStrategiesReadModel(),
|
||||||
featureLifecycleReadModel: new FakeFeatureLifecycleReadModel(),
|
featureLifecycleReadModel: new FakeFeatureLifecycleReadModel(),
|
||||||
|
largestResourcesReadModel: new FakeLargestResourcesReadModel(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user