diff --git a/src/lib/db/client-instance-store.ts b/src/lib/db/client-instance-store.ts index c1983a94d3..ae4429352c 100644 --- a/src/lib/db/client-instance-store.ts +++ b/src/lib/db/client-instance-store.ts @@ -6,7 +6,7 @@ import { IClientInstanceStore, INewClientInstance, } from '../types/stores/client-instance-store'; -import { hoursToMilliseconds } from 'date-fns'; +import { hoursToMilliseconds, subDays } from 'date-fns'; import Timeout = NodeJS.Timeout; const metricsHelper = require('../util/metrics-helper'); @@ -182,6 +182,20 @@ export default class ClientInstanceStore implements IClientInstanceStore { return rows.map((r) => r.app_name); } + async getDistinctApplicationsCount(daysBefore?: number): Promise { + let query = this.db.from(TABLE); + if (daysBefore) { + query = query.where( + 'last_seen', + '>', + subDays(new Date(), daysBefore), + ); + } + return query + .countDistinct('app_name') + .then((res) => Number(res[0].count)); + } + async deleteForApplication(appName: string): Promise { return this.db(TABLE).where('app_name', appName).del(); } diff --git a/src/lib/metrics.test.ts b/src/lib/metrics.test.ts index ec9b05ac44..3f4866384e 100644 --- a/src/lib/metrics.test.ts +++ b/src/lib/metrics.test.ts @@ -122,6 +122,14 @@ test('should collect metrics for feature toggle size', async () => { expect(metrics).toMatch(/feature_toggles_total{version="(.*)"} 0/); }); +test('should collect metrics for feature toggle size', async () => { + await new Promise((done) => { + setTimeout(done, 10); + }); + const metrics = await prometheusRegister.metrics(); + expect(metrics).toMatch(/client_apps_total{range="(.*)"} 0/); +}); + test('Should collect metrics for database', async () => { const metrics = await prometheusRegister.metrics(); expect(metrics).toMatch(/db_pool_max/); diff --git a/src/lib/metrics.ts b/src/lib/metrics.ts index 2529c494a1..813935e151 100644 --- a/src/lib/metrics.ts +++ b/src/lib/metrics.ts @@ -118,6 +118,12 @@ export default class MetricsMonitor { help: 'Number of strategies', }); + const clientAppsTotal = new client.Gauge({ + name: 'client_apps_total', + help: 'Number of registered client apps aggregated by range by last seen', + labelNames: ['range'], + }); + const samlEnabled = new client.Gauge({ name: 'saml_enabled', help: 'Whether SAML is enabled', @@ -170,6 +176,13 @@ export default class MetricsMonitor { oidcEnabled.reset(); oidcEnabled.set(stats.OIDCenabled ? 1 : 0); + + clientAppsTotal.reset(); + stats.clientApps.forEach((clientStat) => + clientAppsTotal + .labels({ range: clientStat.range }) + .set(clientStat.count), + ); } catch (e) {} } diff --git a/src/lib/services/instance-stats-service.ts b/src/lib/services/instance-stats-service.ts index b7b66267d6..8aadace9cc 100644 --- a/src/lib/services/instance-stats-service.ts +++ b/src/lib/services/instance-stats-service.ts @@ -1,7 +1,7 @@ import { sha256 } from 'js-sha256'; import { Logger } from '../logger'; import { IUnleashConfig } from '../types/option'; -import { IUnleashStores } from '../types/stores'; +import { IClientInstanceStore, IUnleashStores } from '../types/stores'; import { IContextFieldStore } from '../types/stores/context-field-store'; import { IEnvironmentStore } from '../types/stores/environment-store'; import { IFeatureToggleStore } from '../types/stores/feature-toggle-store'; @@ -14,7 +14,8 @@ import { IRoleStore } from '../types/stores/role-store'; import VersionService from './version-service'; import { ISettingStore } from '../types/stores/settings-store'; -// eslint-disable-next-line @typescript-eslint/no-unused-vars +type TimeRange = 'allTime' | '30d' | '7d'; + export interface InstanceStats { instanceId: string; timestamp: Date; @@ -31,6 +32,7 @@ export interface InstanceStats { strategies: number; SAMLenabled: boolean; OIDCenabled: boolean; + clientApps: { range: TimeRange; count: number }[]; } interface InstanceStatsSigned extends InstanceStats { @@ -62,6 +64,8 @@ export class InstanceStatsService { private settingStore: ISettingStore; + private clientInstanceStore: IClientInstanceStore; + constructor( { featureToggleStore, @@ -74,6 +78,7 @@ export class InstanceStatsService { segmentStore, roleStore, settingStore, + clientInstanceStore, }: Pick< IUnleashStores, | 'featureToggleStore' @@ -86,6 +91,7 @@ export class InstanceStatsService { | 'segmentStore' | 'roleStore' | 'settingStore' + | 'clientInstanceStore' >, { getLogger }: Pick, versionService: VersionService, @@ -101,6 +107,7 @@ export class InstanceStatsService { this.roleStore = roleStore; this.versionService = versionService; this.settingStore = settingStore; + this.clientInstanceStore = clientInstanceStore; this.logger = getLogger('services/stats-service.js'); } @@ -141,6 +148,7 @@ export class InstanceStatsService { strategies, SAMLenabled, OIDCenabled, + clientApps, ] = await Promise.all([ this.getToggleCount(), this.userStore.count(), @@ -153,6 +161,7 @@ export class InstanceStatsService { this.strategyStore.count(), this.hasSAML(), this.hasOIDC(), + this.getLabeledAppCounts(), ]); return { @@ -171,9 +180,33 @@ export class InstanceStatsService { strategies, SAMLenabled, OIDCenabled, + clientApps, }; } + async getLabeledAppCounts(): Promise< + { range: TimeRange; count: number }[] + > { + return [ + { + range: 'allTime', + count: await this.clientInstanceStore.getDistinctApplicationsCount(), + }, + { + range: '30d', + count: await this.clientInstanceStore.getDistinctApplicationsCount( + 30, + ), + }, + { + range: '7d', + count: await this.clientInstanceStore.getDistinctApplicationsCount( + 7, + ), + }, + ]; + } + async getSignedStats(): Promise { const instanceStats = await this.getStats(); diff --git a/src/lib/types/stores/client-instance-store.ts b/src/lib/types/stores/client-instance-store.ts index ce225f7341..3a1d32a0a1 100644 --- a/src/lib/types/stores/client-instance-store.ts +++ b/src/lib/types/stores/client-instance-store.ts @@ -22,5 +22,6 @@ export interface IClientInstanceStore insert(details: INewClientInstance): Promise; getByAppName(appName: string): Promise; getDistinctApplications(): Promise; + getDistinctApplicationsCount(daysBefore?: number): Promise; deleteForApplication(appName: string): Promise; } diff --git a/src/test/fixtures/fake-client-instance-store.ts b/src/test/fixtures/fake-client-instance-store.ts index 78ddf1e715..614872d95b 100644 --- a/src/test/fixtures/fake-client-instance-store.ts +++ b/src/test/fixtures/fake-client-instance-store.ts @@ -75,6 +75,10 @@ export default class FakeClientInstanceStore implements IClientInstanceStore { return Array.from(apps.values()); } + async getDistinctApplicationsCount(): Promise { + return this.getDistinctApplications().then((apps) => apps.length); + } + async insert(details: INewClientInstance): Promise { this.instances.push({ createdAt: new Date(), ...details }); }