From b0541a0af281a9705c92ce21ca6d7c46be953a6c Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Thu, 22 Aug 2024 13:09:26 +0200 Subject: [PATCH] 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) --- .../InstanceStats/InstanceStats.tsx | 26 ++++++- .../models/instanceAdminStatsSchema.ts | 16 +++++ .../createInstanceStatsService.ts | 7 ++ .../instance-stats/instance-stats-service.ts | 18 +++++ .../spec/instance-admin-stats-schema.ts | 69 ++++++++++++------- src/lib/routes/admin-api/instance-admin.ts | 3 + .../e2e/api/admin/instance-admin.e2e.test.ts | 13 ++++ 7 files changed, 126 insertions(+), 26 deletions(-) diff --git a/frontend/src/component/admin/instance-admin/InstanceStats/InstanceStats.tsx b/frontend/src/component/admin/instance-admin/InstanceStats/InstanceStats.tsx index 8c014489b413..2e1ed46141e3 100644 --- a/frontend/src/component/admin/instance-admin/InstanceStats/InstanceStats.tsx +++ b/frontend/src/component/admin/instance-admin/InstanceStats/InstanceStats.tsx @@ -8,13 +8,13 @@ import { TableRow, } from '@mui/material'; import { Box } from '@mui/system'; -import type { VFC } from 'react'; +import type { FC } from 'react'; import { useInstanceStats } from 'hooks/api/getters/useInstanceStats/useInstanceStats'; import { formatApiPath } from '../../../../utils/formatPath'; import { PageContent } from '../../../common/PageContent/PageContent'; import { PageHeader } from '../../../common/PageHeader/PageHeader'; -export const InstanceStats: VFC = () => { +export const InstanceStats: FC = () => { const { stats } = useInstanceStats(); let versionTitle: string; @@ -28,6 +28,11 @@ export const InstanceStats: VFC = () => { version = stats?.versionOSS; } + const apiTokensTotal = Object.values(stats?.apiTokens ?? {}).reduce( + (acc, val) => acc + val, + 0, + ); + const rows = [ { title: 'Instance Id', value: stats?.instanceId, offset: false }, { title: versionTitle, value: version }, @@ -41,6 +46,23 @@ export const InstanceStats: VFC = () => { { title: 'Strategies', value: stats?.strategies }, { title: 'Feature exports', value: stats?.featureExports }, { 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) { diff --git a/frontend/src/openapi/models/instanceAdminStatsSchema.ts b/frontend/src/openapi/models/instanceAdminStatsSchema.ts index 90d4ff6dab88..afe40ddd6dbf 100644 --- a/frontend/src/openapi/models/instanceAdminStatsSchema.ts +++ b/frontend/src/openapi/models/instanceAdminStatsSchema.ts @@ -92,4 +92,20 @@ export interface InstanceAdminStatsSchema { versionEnterprise?: string; /** The version of Unleash OSS that is bundled in this instance */ 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; } diff --git a/src/lib/features/instance-stats/createInstanceStatsService.ts b/src/lib/features/instance-stats/createInstanceStatsService.ts index ed73b81b6f67..b406120b7b54 100644 --- a/src/lib/features/instance-stats/createInstanceStatsService.ts +++ b/src/lib/features/instance-stats/createInstanceStatsService.ts @@ -40,6 +40,8 @@ import FakeSettingStore from '../../../test/fixtures/fake-setting-store'; import FakeSegmentStore from '../../../test/fixtures/fake-segment-store'; import FakeStrategiesStore from '../../../test/fixtures/fake-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) => { const { eventBus, getLogger, flagResolver } = config; @@ -89,6 +91,8 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => { getLogger, flagResolver, ); + + const featureStrategiesReadModel = new FeatureStrategiesReadModel(db); const instanceStatsServiceStores = { featureToggleStore, userStore, @@ -104,6 +108,7 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => { eventStore, apiTokenStore, clientMetricsStoreV2, + featureStrategiesReadModel, }; const featureStrategiesStore = new FeatureStrategyStore( db, @@ -151,6 +156,7 @@ export const createFakeInstanceStatsService = (config: IUnleashConfig) => { const eventStore = new FakeEventStore(); const apiTokenStore = new FakeApiTokenStore(); const clientMetricsStoreV2 = new FakeClientMetricsStoreV2(); + const featureStrategiesReadModel = new FakeFeatureStrategiesReadModel(); const instanceStatsServiceStores = { featureToggleStore, @@ -167,6 +173,7 @@ export const createFakeInstanceStatsService = (config: IUnleashConfig) => { eventStore, apiTokenStore, clientMetricsStoreV2, + featureStrategiesReadModel, }; const featureStrategiesStore = new FakeFeatureStrategiesStore(); const versionServiceStores = { diff --git a/src/lib/features/instance-stats/instance-stats-service.ts b/src/lib/features/instance-stats/instance-stats-service.ts index 63cc6229c09b..3fb505fd6480 100644 --- a/src/lib/features/instance-stats/instance-stats-service.ts +++ b/src/lib/features/instance-stats/instance-stats-service.ts @@ -5,6 +5,7 @@ import type { IClientInstanceStore, IClientMetricsStoreV2, IEventStore, + IFeatureStrategiesReadModel, IUnleashStores, } from '../../types/stores'; import type { IContextFieldStore } from '../../types/stores/context-field-store'; @@ -61,6 +62,9 @@ export interface InstanceStats { enabledCount: number; variantCount: number; }; + maxEnvironmentStrategies: number; + maxConstraints: number; + maxConstraintValues: number; } export type InstanceStatsSigned = Omit & { @@ -109,6 +113,8 @@ export class InstanceStatsService { private getProductionChanges: GetProductionChanges; + private featureStrategiesReadModel: IFeatureStrategiesReadModel; + constructor( { featureToggleStore, @@ -125,6 +131,7 @@ export class InstanceStatsService { eventStore, apiTokenStore, clientMetricsStoreV2, + featureStrategiesReadModel, }: Pick< IUnleashStores, | 'featureToggleStore' @@ -141,6 +148,7 @@ export class InstanceStatsService { | 'eventStore' | 'apiTokenStore' | 'clientMetricsStoreV2' + | 'featureStrategiesReadModel' >, { getLogger, @@ -169,6 +177,7 @@ export class InstanceStatsService { this.apiTokenStore = apiTokenStore; this.clientMetricsStore = clientMetricsStoreV2; this.flagResolver = flagResolver; + this.featureStrategiesReadModel = featureStrategiesReadModel; } async refreshAppCountSnapshot(): Promise< @@ -250,6 +259,9 @@ export class InstanceStatsService { featureImports, productionChanges, previousDayMetricsBucketsCount, + maxEnvironmentStrategies, + maxConstraintValues, + maxConstraints, ] = await Promise.all([ this.getToggleCount(), this.getArchivedToggleCount(), @@ -277,6 +289,9 @@ export class InstanceStatsService { }), this.getProductionChanges(), this.clientMetricsStore.countPreviousDayHourlyMetricsBuckets(), + this.featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies(), + this.featureStrategiesReadModel.getMaxConstraintValues(), + this.featureStrategiesReadModel.getMaxConstraintsPerStrategy(), ]); return { @@ -309,6 +324,9 @@ export class InstanceStatsService { featureImports, productionChanges, previousDayMetricsBucketsCount, + maxEnvironmentStrategies: maxEnvironmentStrategies?.count ?? 0, + maxConstraintValues: maxConstraintValues?.count ?? 0, + maxConstraints: maxConstraints?.count ?? 0, }; } diff --git a/src/lib/openapi/spec/instance-admin-stats-schema.ts b/src/lib/openapi/spec/instance-admin-stats-schema.ts index 0a54a3e7b36d..564c877ed693 100644 --- a/src/lib/openapi/spec/instance-admin-stats-schema.ts +++ b/src/lib/openapi/spec/instance-admin-stats-schema.ts @@ -34,7 +34,7 @@ export const instanceAdminStatsSchema = { example: '5.1.7', }, users: { - type: 'number', + type: 'integer', description: 'The number of users this instance has', example: 8, 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', properties: { enabledCount: { - type: 'number', + type: 'integer', description: 'The number of enabled/disabled metrics buckets recorded in the previous day', example: 10, minimum: 0, }, variantCount: { - type: 'number', + type: 'integer', description: 'The number of variant metrics buckets recorded in the previous day', example: 10, @@ -66,28 +66,28 @@ export const instanceAdminStatsSchema = { 'The number of active users in the last 7, 30 and 90 days', properties: { last7: { - type: 'number', + type: 'integer', description: 'The number of active users in the last 7 days', example: 5, minimum: 0, }, last30: { - type: 'number', + type: 'integer', description: 'The number of active users in the last 30 days', example: 10, minimum: 0, }, last60: { - type: 'number', + type: 'integer', description: 'The number of active users in the last 60 days', example: 12, minimum: 0, }, last90: { - type: 'number', + type: 'integer', description: 'The number of active users in the last 90 days', 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', properties: { last30: { - type: 'number', + type: 'integer', description: 'The number of changes in production in the last 30 days', example: 10, minimum: 0, }, last60: { - type: 'number', + type: 'integer', description: 'The number of changes in production in the last 60 days', example: 12, minimum: 0, }, last90: { - type: 'number', + type: 'integer', description: 'The number of changes in production in the last 90 days', example: 15, @@ -124,50 +124,50 @@ export const instanceAdminStatsSchema = { }, }, featureToggles: { - type: 'number', + type: 'integer', description: 'The number of feature-toggles this instance has', example: 47, minimum: 0, }, projects: { - type: 'number', + type: 'integer', description: 'The number of projects defined in this instance.', example: 3, minimum: 0, }, contextFields: { - type: 'number', + type: 'integer', description: 'The number of context fields defined in this instance.', example: 7, minimum: 0, }, roles: { - type: 'number', + type: 'integer', description: 'The number of roles defined in this instance', example: 5, minimum: 0, }, groups: { - type: 'number', + type: 'integer', description: 'The number of groups defined in this instance', example: 12, minimum: 0, }, environments: { - type: 'number', + type: 'integer', description: 'The number of environments defined in this instance', example: 3, minimum: 0, }, segments: { - type: 'number', + type: 'integer', description: 'The number of segments defined in this instance', example: 19, minimum: 0, }, strategies: { - type: 'number', + type: 'integer', description: 'The number of strategies defined in this instance', example: 8, minimum: 0, @@ -200,7 +200,7 @@ export const instanceAdminStatsSchema = { example: '30d', }, count: { - type: 'number', + type: 'integer', description: 'The number of client applications that have been observed in this period', example: 1, @@ -209,13 +209,13 @@ export const instanceAdminStatsSchema = { }, }, featureExports: { - type: 'number', + type: 'integer', description: 'The number of export operations on this instance', example: 0, minimum: 0, }, featureImports: { - type: 'number', + type: 'integer', description: 'The number of import operations on this instance', example: 0, minimum: 0, @@ -225,25 +225,46 @@ export const instanceAdminStatsSchema = { description: 'The number of API tokens in Unleash, split by type', properties: { admin: { - type: 'number', + type: 'integer', description: 'The number of admin tokens.', minimum: 0, example: 5, }, client: { - type: 'number', + type: 'integer', description: 'The number of client tokens.', minimum: 0, example: 5, }, frontend: { - type: 'number', + type: 'integer', description: 'The number of frontend tokens.', minimum: 0, 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: { type: 'string', description: diff --git a/src/lib/routes/admin-api/instance-admin.ts b/src/lib/routes/admin-api/instance-admin.ts index 89d81fabbf8c..200704f76a92 100644 --- a/src/lib/routes/admin-api/instance-admin.ts +++ b/src/lib/routes/admin-api/instance-admin.ts @@ -126,6 +126,9 @@ class InstanceAdminController extends Controller { variantCount: 100, enabledCount: 200, }, + maxEnvironmentStrategies: 20, + maxConstraints: 17, + maxConstraintValues: 123, }; } diff --git a/src/test/e2e/api/admin/instance-admin.e2e.test.ts b/src/test/e2e/api/admin/instance-admin.e2e.test.ts index 5534b4bd80e5..9b1cdb34ed1a 100644 --- a/src/test/e2e/api/admin/instance-admin.e2e.test.ts +++ b/src/test/e2e/api/admin/instance-admin.e2e.test.ts @@ -133,3 +133,16 @@ test('should return instance statistics as CSV', async () => { expect(res.text).toMatch(/featureToggles/); 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, + }); +});