diff --git a/frontend/src/component/admin/instance-admin/InstanceStats/InstanceStats.tsx b/frontend/src/component/admin/instance-admin/InstanceStats/InstanceStats.tsx index ba028b891b..3edda6f7ba 100644 --- a/frontend/src/component/admin/instance-admin/InstanceStats/InstanceStats.tsx +++ b/frontend/src/component/admin/instance-admin/InstanceStats/InstanceStats.tsx @@ -65,6 +65,7 @@ export const InstanceStats: FC = () => { }, { title: 'Release templates', value: stats?.releaseTemplates }, { title: 'Release plans', value: stats?.releasePlans }, + { title: 'Edge instances', value: stats?.edgeInstances }, ]; if (stats?.versionEnterprise) { diff --git a/src/lib/features/instance-stats/createInstanceStatsService.ts b/src/lib/features/instance-stats/createInstanceStatsService.ts index 048fb1a45a..10a8add32e 100644 --- a/src/lib/features/instance-stats/createInstanceStatsService.ts +++ b/src/lib/features/instance-stats/createInstanceStatsService.ts @@ -50,6 +50,10 @@ import { } from './getLicensedUsers.js'; import { ReleasePlanStore } from '../release-plans/release-plan-store.js'; import { ReleasePlanTemplateStore } from '../release-plans/release-plan-template-store.js'; +import { + createFakeGetEdgeInstances, + createGetEdgeInstances, +} from './getEdgeInstances.js'; export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => { const { eventBus, getLogger, flagResolver } = config; @@ -134,6 +138,7 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => { const getActiveUsers = createGetActiveUsers(db); const getProductionChanges = createGetProductionChanges(db); const getLicencedUsers = createGetLicensedUsers(db); + const getEdgeInstances = createGetEdgeInstances(db); const versionService = new VersionService(versionServiceStores, config); const instanceStatsService = new InstanceStatsService( @@ -143,6 +148,7 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => { getActiveUsers, getProductionChanges, getLicencedUsers, + getEdgeInstances, ); return instanceStatsService; @@ -199,6 +205,7 @@ export const createFakeInstanceStatsService = (config: IUnleashConfig) => { const getActiveUsers = createFakeGetActiveUsers(); const getLicensedUsers = createFakeGetLicensedUsers(); const getProductionChanges = createFakeGetProductionChanges(); + const getEdgeInstances = createFakeGetEdgeInstances(); const versionService = new VersionService(versionServiceStores, config); const instanceStatsService = new InstanceStatsService( @@ -208,6 +215,7 @@ export const createFakeInstanceStatsService = (config: IUnleashConfig) => { getActiveUsers, getProductionChanges, getLicensedUsers, + getEdgeInstances, ); return instanceStatsService; diff --git a/src/lib/features/instance-stats/getEdgeInstances.ts b/src/lib/features/instance-stats/getEdgeInstances.ts new file mode 100644 index 0000000000..b567f8eee7 --- /dev/null +++ b/src/lib/features/instance-stats/getEdgeInstances.ts @@ -0,0 +1,59 @@ +import type { Db } from '../../types/index.js'; + +const TABLE = 'edge_node_presence'; +const GRACE_PERCENTAGE = 0.05; + +export type GetEdgeInstances = () => Promise<{ + last30: number; + last60: number; + last90: number; +}>; + +export const createGetEdgeInstances = + (db: Db): GetEdgeInstances => + async () => { + const result = await db + .with('buckets', (qb) => + qb + .from(TABLE) + .whereRaw("bucket_ts >= NOW() - INTERVAL '90 days'") + .groupBy('bucket_ts') + .select( + db.raw('bucket_ts'), + db.raw('COUNT(*)::int AS active_nodes'), + ), + ) + .from('buckets') + .select({ + last30: db.raw( + `COALESCE(CEIL(AVG(active_nodes) FILTER (WHERE bucket_ts >= NOW() - INTERVAL '30 days') * ?)::int, 0)`, + [1 + GRACE_PERCENTAGE], + ), + last60: db.raw( + `COALESCE(CEIL(AVG(active_nodes) FILTER (WHERE bucket_ts >= NOW() - INTERVAL '60 days') * ?)::int, 0)`, + [1 + GRACE_PERCENTAGE], + ), + last90: db.raw( + `COALESCE(CEIL(AVG(active_nodes) FILTER (WHERE bucket_ts >= NOW() - INTERVAL '90 days') * ?)::int, 0)`, + [1 + GRACE_PERCENTAGE], + ), + }) + .first(); + + return { + last30: Number(result?.last30 ?? 0), + last60: Number(result?.last60 ?? 0), + last90: Number(result?.last90 ?? 0), + }; + }; + +export const createFakeGetEdgeInstances = + ( + edgeInstances: Awaited> = { + last30: 0, + last60: 0, + last90: 0, + }, + ): GetEdgeInstances => + () => + Promise.resolve(edgeInstances); diff --git a/src/lib/features/instance-stats/instance-stats-service.ts b/src/lib/features/instance-stats/instance-stats-service.ts index e99be95980..6dd0452dd5 100644 --- a/src/lib/features/instance-stats/instance-stats-service.ts +++ b/src/lib/features/instance-stats/instance-stats-service.ts @@ -33,6 +33,7 @@ import type { GetLicensedUsers } from './getLicensedUsers.js'; import type { IFeatureUsageInfo } from '../../services/version-service.js'; import type { ReleasePlanTemplateStore } from '../release-plans/release-plan-template-store.js'; import type { ReleasePlanStore } from '../release-plans/release-plan-store.js'; +import type { GetEdgeInstances } from './getEdgeInstances.js'; export type TimeRange = 'allTime' | '30d' | '7d'; @@ -74,6 +75,7 @@ export interface InstanceStats { maxConstraintValues: number; releaseTemplates?: number; releasePlans?: number; + edgeInstances?: Awaited>; } export type InstanceStatsSigned = Omit & { @@ -124,6 +126,8 @@ export class InstanceStatsService { getProductionChanges: GetProductionChanges; + getEdgeInstances: GetEdgeInstances; + private featureStrategiesReadModel: IFeatureStrategiesReadModel; private featureStrategiesStore: IFeatureStrategiesStore; @@ -185,6 +189,7 @@ export class InstanceStatsService { getActiveUsers: GetActiveUsers, getProductionChanges: GetProductionChanges, getLicencedUsers: GetLicensedUsers, + getEdgeInstances: GetEdgeInstances, ) { this.strategyStore = strategyStore; this.userStore = userStore; @@ -209,6 +214,8 @@ export class InstanceStatsService { 'getProductionChanges', getProductionChanges.bind(this), ); + this.getEdgeInstances = () => + this.memorize('getEdgeInstances', getEdgeInstances.bind(this)); this.apiTokenStore = apiTokenStore; this.clientMetricsStore = clientMetricsStoreV2; this.flagResolver = flagResolver; @@ -356,6 +363,7 @@ export class InstanceStatsService { maxConstraints, releaseTemplates, releasePlans, + edgeInstances, ] = await Promise.all([ this.getToggleCount(), this.getArchivedToggleCount(), @@ -402,6 +410,7 @@ export class InstanceStatsService { ), this.getReleaseTemplates(), this.getReleasePlans(), + this.getEdgeInstances(), ]); return { @@ -442,6 +451,7 @@ export class InstanceStatsService { maxConstraints: maxConstraints?.count ?? 0, releaseTemplates, releasePlans, + edgeInstances, }; } @@ -471,6 +481,7 @@ export class InstanceStatsService { hostedBy, releaseTemplates, releasePlans, + edgeInstances, ] = await Promise.all([ this.getToggleCount(), this.getRegisteredUsers(), @@ -496,6 +507,7 @@ export class InstanceStatsService { this.getHostedBy(), this.getReleaseTemplates(), this.getReleasePlans(), + this.getEdgeInstances(), ]); const versionInfo = await this.versionService.getVersionInfo(); @@ -533,6 +545,9 @@ export class InstanceStatsService { hostedBy, releaseTemplates, releasePlans, + edgeInstances30: edgeInstances.last30, + edgeInstances60: edgeInstances.last60, + edgeInstances90: edgeInstances.last90, }; return featureInfo; } diff --git a/src/lib/openapi/spec/instance-admin-stats-schema.ts b/src/lib/openapi/spec/instance-admin-stats-schema.ts index 80728fc1e5..ddb20bd50e 100644 --- a/src/lib/openapi/spec/instance-admin-stats-schema.ts +++ b/src/lib/openapi/spec/instance-admin-stats-schema.ts @@ -284,6 +284,34 @@ export const instanceAdminStatsSchema = { example: 1, description: 'The number of release plans in this instance', }, + edgeInstances: { + type: 'object', + description: + 'The billable number of edge instances in the last 30, 60 and 90 days', + properties: { + last30: { + type: 'integer', + description: + 'The billable number of edge instances in the last 30 days', + example: 10, + minimum: 0, + }, + last60: { + type: 'integer', + description: + 'The billable number of edge instances in the last 60 days', + example: 12, + minimum: 0, + }, + last90: { + type: 'integer', + description: + 'The billable number of edge instances in the last 90 days', + example: 15, + minimum: 0, + }, + }, + }, 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 f3d48aa84e..193f73d20f 100644 --- a/src/lib/routes/admin-api/instance-admin.ts +++ b/src/lib/routes/admin-api/instance-admin.ts @@ -133,6 +133,11 @@ class InstanceAdminController extends Controller { maxConstraintValues: 123, releaseTemplates: 3, releasePlans: 5, + edgeInstances: { + last30: 10, + last60: 15, + last90: 20, + }, }; } diff --git a/src/lib/services/version-service.test.ts b/src/lib/services/version-service.test.ts index 376ea45e4a..c0a0ca9aab 100644 --- a/src/lib/services/version-service.test.ts +++ b/src/lib/services/version-service.test.ts @@ -45,6 +45,9 @@ const fakeTelemetryData = { hostedBy: 'self-hosted', releaseTemplates: 2, releasePlans: 4, + edgeInstances30: 0, + edgeInstances60: 0, + edgeInstances90: 0, }; test('yields current versions', async () => { diff --git a/src/lib/services/version-service.ts b/src/lib/services/version-service.ts index 42af259ad6..585e6a04ab 100644 --- a/src/lib/services/version-service.ts +++ b/src/lib/services/version-service.ts @@ -54,6 +54,9 @@ export interface IFeatureUsageInfo { hostedBy: string; releaseTemplates: number; releasePlans: number; + edgeInstances30?: number; + edgeInstances60?: number; + edgeInstances90?: number; } export default class VersionService {