From 2979f21631770215906c82d50415f9e2e61fcc47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Fri, 16 Dec 2022 12:16:51 +0100 Subject: [PATCH] feat: expose number of registered applications metric (#2692) ## About the changes This metric will expose an aggregated view of how many client applications are registered in Unleash. Since applications are ephemeral we are exposing this metric in different time windows based on when the application was last seen. The caveat is that we issue a database query for each new range we want to add. Hopefully, this should not be a problem because: a) the amount of ranges we'd expose is small and unlikely to grow b) this is currently updated at startup time and even if we update it on a scheduled basis the refresh rate will be rather sparse ## Sample data This is how metrics will look like ``` # HELP client_apps_total Number of registered client apps aggregated by range by last seen # TYPE client_apps_total gauge client_apps_total{range="allTime"} 3 client_apps_total{range="30d"} 3 client_apps_total{range="7d"} 2 ``` --- src/lib/db/client-instance-store.ts | 16 +++++++- src/lib/metrics.test.ts | 8 ++++ src/lib/metrics.ts | 13 +++++++ src/lib/services/instance-stats-service.ts | 37 ++++++++++++++++++- src/lib/types/stores/client-instance-store.ts | 1 + .../fixtures/fake-client-instance-store.ts | 4 ++ 6 files changed, 76 insertions(+), 3 deletions(-) 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 }); }