1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: add remaining resource usage to instance stats (#7958)

Updates the instance stats endpoint with 
- maxEnvironmentStrategies
- maxConstraints
- maxConstraintValues

It adds the following rows to the front end table:
- segments (already in the payload, just not used for the table before)
- API tokens (separate rows for type, + one for total) (also existed
before, but wasn't listed)
- Highest number of strategies used for a single flag in a single
environment
- Highest number of constraints used on a single strategy
- Highest number of values used for a single constraint


![image](https://github.com/user-attachments/assets/57798f8e-c466-4590-820b-15afd3729243)
This commit is contained in:
Thomas Heartman 2024-08-22 13:09:26 +02:00 committed by GitHub
parent cb8d689bd8
commit b0541a0af2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 126 additions and 26 deletions

View File

@ -8,13 +8,13 @@ import {
TableRow, TableRow,
} from '@mui/material'; } from '@mui/material';
import { Box } from '@mui/system'; import { Box } from '@mui/system';
import type { VFC } from 'react'; import type { FC } from 'react';
import { useInstanceStats } from 'hooks/api/getters/useInstanceStats/useInstanceStats'; import { useInstanceStats } from 'hooks/api/getters/useInstanceStats/useInstanceStats';
import { formatApiPath } from '../../../../utils/formatPath'; import { formatApiPath } from '../../../../utils/formatPath';
import { PageContent } from '../../../common/PageContent/PageContent'; import { PageContent } from '../../../common/PageContent/PageContent';
import { PageHeader } from '../../../common/PageHeader/PageHeader'; import { PageHeader } from '../../../common/PageHeader/PageHeader';
export const InstanceStats: VFC = () => { export const InstanceStats: FC = () => {
const { stats } = useInstanceStats(); const { stats } = useInstanceStats();
let versionTitle: string; let versionTitle: string;
@ -28,6 +28,11 @@ export const InstanceStats: VFC = () => {
version = stats?.versionOSS; version = stats?.versionOSS;
} }
const apiTokensTotal = Object.values(stats?.apiTokens ?? {}).reduce(
(acc, val) => acc + val,
0,
);
const rows = [ const rows = [
{ title: 'Instance Id', value: stats?.instanceId, offset: false }, { title: 'Instance Id', value: stats?.instanceId, offset: false },
{ title: versionTitle, value: version }, { title: versionTitle, value: version },
@ -41,6 +46,23 @@ export const InstanceStats: VFC = () => {
{ title: 'Strategies', value: stats?.strategies }, { title: 'Strategies', value: stats?.strategies },
{ title: 'Feature exports', value: stats?.featureExports }, { title: 'Feature exports', value: stats?.featureExports },
{ title: 'Feature imports', value: stats?.featureImports }, { title: 'Feature imports', value: stats?.featureImports },
{ title: 'Admin API tokens', value: stats?.apiTokens?.admin },
{ title: 'Client API tokens', value: stats?.apiTokens?.client },
{ title: 'Frontend API tokens', value: stats?.apiTokens?.frontend },
{ title: 'API tokens total', value: apiTokensTotal },
{ title: 'Segments', value: stats?.segments },
{
title: 'Highest number of strategies used for a single flag in a single environment',
value: stats?.maxEnvironmentStrategies,
},
{
title: 'Highest number of constraints used on a single strategy',
value: stats?.maxConstraints,
},
{
title: 'Highest number of values used for a single constraint',
value: stats?.maxConstraintValues,
},
]; ];
if (stats?.versionEnterprise) { if (stats?.versionEnterprise) {

View File

@ -92,4 +92,20 @@ export interface InstanceAdminStatsSchema {
versionEnterprise?: string; versionEnterprise?: string;
/** The version of Unleash OSS that is bundled in this instance */ /** The version of Unleash OSS that is bundled in this instance */
versionOSS?: string; versionOSS?: string;
/** A breakdown of API tokens that exist in this instance */
apiTokens: {
client: number;
admin: number;
frontend: number;
};
// The highest number of strategies used on a single feature flag in a single environment.
maxEnvironmentStrategies: number;
// The highest number of constraints used on a single strategy.
maxConstraints: number;
// The highest number of constraint values used on a single constraint.
maxConstraintValues: number;
} }

View File

@ -40,6 +40,8 @@ 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 { FeatureStrategiesReadModel } from '../feature-toggle/feature-strategies-read-model';
import { FakeFeatureStrategiesReadModel } from '../feature-toggle/fake-feature-strategies-read-model';
export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => { export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => {
const { eventBus, getLogger, flagResolver } = config; const { eventBus, getLogger, flagResolver } = config;
@ -89,6 +91,8 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => {
getLogger, getLogger,
flagResolver, flagResolver,
); );
const featureStrategiesReadModel = new FeatureStrategiesReadModel(db);
const instanceStatsServiceStores = { const instanceStatsServiceStores = {
featureToggleStore, featureToggleStore,
userStore, userStore,
@ -104,6 +108,7 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => {
eventStore, eventStore,
apiTokenStore, apiTokenStore,
clientMetricsStoreV2, clientMetricsStoreV2,
featureStrategiesReadModel,
}; };
const featureStrategiesStore = new FeatureStrategyStore( const featureStrategiesStore = new FeatureStrategyStore(
db, db,
@ -151,6 +156,7 @@ 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 featureStrategiesReadModel = new FakeFeatureStrategiesReadModel();
const instanceStatsServiceStores = { const instanceStatsServiceStores = {
featureToggleStore, featureToggleStore,
@ -167,6 +173,7 @@ export const createFakeInstanceStatsService = (config: IUnleashConfig) => {
eventStore, eventStore,
apiTokenStore, apiTokenStore,
clientMetricsStoreV2, clientMetricsStoreV2,
featureStrategiesReadModel,
}; };
const featureStrategiesStore = new FakeFeatureStrategiesStore(); const featureStrategiesStore = new FakeFeatureStrategiesStore();
const versionServiceStores = { const versionServiceStores = {

View File

@ -5,6 +5,7 @@ import type {
IClientInstanceStore, IClientInstanceStore,
IClientMetricsStoreV2, IClientMetricsStoreV2,
IEventStore, IEventStore,
IFeatureStrategiesReadModel,
IUnleashStores, IUnleashStores,
} from '../../types/stores'; } from '../../types/stores';
import type { IContextFieldStore } from '../../types/stores/context-field-store'; import type { IContextFieldStore } from '../../types/stores/context-field-store';
@ -61,6 +62,9 @@ export interface InstanceStats {
enabledCount: number; enabledCount: number;
variantCount: number; variantCount: number;
}; };
maxEnvironmentStrategies: number;
maxConstraints: number;
maxConstraintValues: number;
} }
export type InstanceStatsSigned = Omit<InstanceStats, 'projects'> & { export type InstanceStatsSigned = Omit<InstanceStats, 'projects'> & {
@ -109,6 +113,8 @@ export class InstanceStatsService {
private getProductionChanges: GetProductionChanges; private getProductionChanges: GetProductionChanges;
private featureStrategiesReadModel: IFeatureStrategiesReadModel;
constructor( constructor(
{ {
featureToggleStore, featureToggleStore,
@ -125,6 +131,7 @@ export class InstanceStatsService {
eventStore, eventStore,
apiTokenStore, apiTokenStore,
clientMetricsStoreV2, clientMetricsStoreV2,
featureStrategiesReadModel,
}: Pick< }: Pick<
IUnleashStores, IUnleashStores,
| 'featureToggleStore' | 'featureToggleStore'
@ -141,6 +148,7 @@ export class InstanceStatsService {
| 'eventStore' | 'eventStore'
| 'apiTokenStore' | 'apiTokenStore'
| 'clientMetricsStoreV2' | 'clientMetricsStoreV2'
| 'featureStrategiesReadModel'
>, >,
{ {
getLogger, getLogger,
@ -169,6 +177,7 @@ export class InstanceStatsService {
this.apiTokenStore = apiTokenStore; this.apiTokenStore = apiTokenStore;
this.clientMetricsStore = clientMetricsStoreV2; this.clientMetricsStore = clientMetricsStoreV2;
this.flagResolver = flagResolver; this.flagResolver = flagResolver;
this.featureStrategiesReadModel = featureStrategiesReadModel;
} }
async refreshAppCountSnapshot(): Promise< async refreshAppCountSnapshot(): Promise<
@ -250,6 +259,9 @@ export class InstanceStatsService {
featureImports, featureImports,
productionChanges, productionChanges,
previousDayMetricsBucketsCount, previousDayMetricsBucketsCount,
maxEnvironmentStrategies,
maxConstraintValues,
maxConstraints,
] = await Promise.all([ ] = await Promise.all([
this.getToggleCount(), this.getToggleCount(),
this.getArchivedToggleCount(), this.getArchivedToggleCount(),
@ -277,6 +289,9 @@ export class InstanceStatsService {
}), }),
this.getProductionChanges(), this.getProductionChanges(),
this.clientMetricsStore.countPreviousDayHourlyMetricsBuckets(), this.clientMetricsStore.countPreviousDayHourlyMetricsBuckets(),
this.featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies(),
this.featureStrategiesReadModel.getMaxConstraintValues(),
this.featureStrategiesReadModel.getMaxConstraintsPerStrategy(),
]); ]);
return { return {
@ -309,6 +324,9 @@ export class InstanceStatsService {
featureImports, featureImports,
productionChanges, productionChanges,
previousDayMetricsBucketsCount, previousDayMetricsBucketsCount,
maxEnvironmentStrategies: maxEnvironmentStrategies?.count ?? 0,
maxConstraintValues: maxConstraintValues?.count ?? 0,
maxConstraints: maxConstraints?.count ?? 0,
}; };
} }

View File

@ -34,7 +34,7 @@ export const instanceAdminStatsSchema = {
example: '5.1.7', example: '5.1.7',
}, },
users: { users: {
type: 'number', type: 'integer',
description: 'The number of users this instance has', description: 'The number of users this instance has',
example: 8, example: 8,
minimum: 0, minimum: 0,
@ -45,14 +45,14 @@ export const instanceAdminStatsSchema = {
'The number client metrics buckets records recorded in the previous day. # features * # apps * # envs * # hours with metrics', 'The number client metrics buckets records recorded in the previous day. # features * # apps * # envs * # hours with metrics',
properties: { properties: {
enabledCount: { enabledCount: {
type: 'number', type: 'integer',
description: description:
'The number of enabled/disabled metrics buckets recorded in the previous day', 'The number of enabled/disabled metrics buckets recorded in the previous day',
example: 10, example: 10,
minimum: 0, minimum: 0,
}, },
variantCount: { variantCount: {
type: 'number', type: 'integer',
description: description:
'The number of variant metrics buckets recorded in the previous day', 'The number of variant metrics buckets recorded in the previous day',
example: 10, example: 10,
@ -66,28 +66,28 @@ export const instanceAdminStatsSchema = {
'The number of active users in the last 7, 30 and 90 days', 'The number of active users in the last 7, 30 and 90 days',
properties: { properties: {
last7: { last7: {
type: 'number', type: 'integer',
description: description:
'The number of active users in the last 7 days', 'The number of active users in the last 7 days',
example: 5, example: 5,
minimum: 0, minimum: 0,
}, },
last30: { last30: {
type: 'number', type: 'integer',
description: description:
'The number of active users in the last 30 days', 'The number of active users in the last 30 days',
example: 10, example: 10,
minimum: 0, minimum: 0,
}, },
last60: { last60: {
type: 'number', type: 'integer',
description: description:
'The number of active users in the last 60 days', 'The number of active users in the last 60 days',
example: 12, example: 12,
minimum: 0, minimum: 0,
}, },
last90: { last90: {
type: 'number', type: 'integer',
description: description:
'The number of active users in the last 90 days', 'The number of active users in the last 90 days',
example: 15, example: 15,
@ -101,21 +101,21 @@ export const instanceAdminStatsSchema = {
'The number of changes to the production environment in the last 30, 60 and 90 days', 'The number of changes to the production environment in the last 30, 60 and 90 days',
properties: { properties: {
last30: { last30: {
type: 'number', type: 'integer',
description: description:
'The number of changes in production in the last 30 days', 'The number of changes in production in the last 30 days',
example: 10, example: 10,
minimum: 0, minimum: 0,
}, },
last60: { last60: {
type: 'number', type: 'integer',
description: description:
'The number of changes in production in the last 60 days', 'The number of changes in production in the last 60 days',
example: 12, example: 12,
minimum: 0, minimum: 0,
}, },
last90: { last90: {
type: 'number', type: 'integer',
description: description:
'The number of changes in production in the last 90 days', 'The number of changes in production in the last 90 days',
example: 15, example: 15,
@ -124,50 +124,50 @@ export const instanceAdminStatsSchema = {
}, },
}, },
featureToggles: { featureToggles: {
type: 'number', type: 'integer',
description: 'The number of feature-toggles this instance has', description: 'The number of feature-toggles this instance has',
example: 47, example: 47,
minimum: 0, minimum: 0,
}, },
projects: { projects: {
type: 'number', type: 'integer',
description: 'The number of projects defined in this instance.', description: 'The number of projects defined in this instance.',
example: 3, example: 3,
minimum: 0, minimum: 0,
}, },
contextFields: { contextFields: {
type: 'number', type: 'integer',
description: description:
'The number of context fields defined in this instance.', 'The number of context fields defined in this instance.',
example: 7, example: 7,
minimum: 0, minimum: 0,
}, },
roles: { roles: {
type: 'number', type: 'integer',
description: 'The number of roles defined in this instance', description: 'The number of roles defined in this instance',
example: 5, example: 5,
minimum: 0, minimum: 0,
}, },
groups: { groups: {
type: 'number', type: 'integer',
description: 'The number of groups defined in this instance', description: 'The number of groups defined in this instance',
example: 12, example: 12,
minimum: 0, minimum: 0,
}, },
environments: { environments: {
type: 'number', type: 'integer',
description: 'The number of environments defined in this instance', description: 'The number of environments defined in this instance',
example: 3, example: 3,
minimum: 0, minimum: 0,
}, },
segments: { segments: {
type: 'number', type: 'integer',
description: 'The number of segments defined in this instance', description: 'The number of segments defined in this instance',
example: 19, example: 19,
minimum: 0, minimum: 0,
}, },
strategies: { strategies: {
type: 'number', type: 'integer',
description: 'The number of strategies defined in this instance', description: 'The number of strategies defined in this instance',
example: 8, example: 8,
minimum: 0, minimum: 0,
@ -200,7 +200,7 @@ export const instanceAdminStatsSchema = {
example: '30d', example: '30d',
}, },
count: { count: {
type: 'number', type: 'integer',
description: description:
'The number of client applications that have been observed in this period', 'The number of client applications that have been observed in this period',
example: 1, example: 1,
@ -209,13 +209,13 @@ export const instanceAdminStatsSchema = {
}, },
}, },
featureExports: { featureExports: {
type: 'number', type: 'integer',
description: 'The number of export operations on this instance', description: 'The number of export operations on this instance',
example: 0, example: 0,
minimum: 0, minimum: 0,
}, },
featureImports: { featureImports: {
type: 'number', type: 'integer',
description: 'The number of import operations on this instance', description: 'The number of import operations on this instance',
example: 0, example: 0,
minimum: 0, minimum: 0,
@ -225,25 +225,46 @@ export const instanceAdminStatsSchema = {
description: 'The number of API tokens in Unleash, split by type', description: 'The number of API tokens in Unleash, split by type',
properties: { properties: {
admin: { admin: {
type: 'number', type: 'integer',
description: 'The number of admin tokens.', description: 'The number of admin tokens.',
minimum: 0, minimum: 0,
example: 5, example: 5,
}, },
client: { client: {
type: 'number', type: 'integer',
description: 'The number of client tokens.', description: 'The number of client tokens.',
minimum: 0, minimum: 0,
example: 5, example: 5,
}, },
frontend: { frontend: {
type: 'number', type: 'integer',
description: 'The number of frontend tokens.', description: 'The number of frontend tokens.',
minimum: 0, minimum: 0,
example: 5, example: 5,
}, },
}, },
}, },
maxEnvironmentStrategies: {
type: 'integer',
minimum: 0,
example: 3,
description:
'The highest number of strategies used on a single feature flag in a single environment.',
},
maxConstraints: {
type: 'integer',
minimum: 0,
example: 4,
description:
'The highest number of constraints used on a single strategy.',
},
maxConstraintValues: {
type: 'integer',
minimum: 0,
example: 17,
description:
'The highest number of constraint values used on a single constraint.',
},
sum: { sum: {
type: 'string', type: 'string',
description: description:

View File

@ -126,6 +126,9 @@ class InstanceAdminController extends Controller {
variantCount: 100, variantCount: 100,
enabledCount: 200, enabledCount: 200,
}, },
maxEnvironmentStrategies: 20,
maxConstraints: 17,
maxConstraintValues: 123,
}; };
} }

View File

@ -133,3 +133,16 @@ test('should return instance statistics as CSV', async () => {
expect(res.text).toMatch(/featureToggles/); expect(res.text).toMatch(/featureToggles/);
expect(res.text).toMatch(/"sum"/); expect(res.text).toMatch(/"sum"/);
}); });
test('contains new max* properties', async () => {
const { body } = await app.request
.get('/api/admin/instance-admin/statistics')
.expect('Content-Type', /json/)
.expect(200);
expect(body).toMatchObject({
maxEnvironmentStrategies: 0,
maxConstraints: 0,
maxConstraintValues: 0,
});
});