mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
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 ```
This commit is contained in:
parent
eafba10cac
commit
2979f21631
@ -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<number> {
|
||||
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<void> {
|
||||
return this.db(TABLE).where('app_name', appName).del();
|
||||
}
|
||||
|
@ -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/);
|
||||
|
@ -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) {}
|
||||
}
|
||||
|
||||
|
@ -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<IUnleashConfig, 'getLogger'>,
|
||||
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<InstanceStatsSigned> {
|
||||
const instanceStats = await this.getStats();
|
||||
|
||||
|
@ -22,5 +22,6 @@ export interface IClientInstanceStore
|
||||
insert(details: INewClientInstance): Promise<void>;
|
||||
getByAppName(appName: string): Promise<IClientInstance[]>;
|
||||
getDistinctApplications(): Promise<string[]>;
|
||||
getDistinctApplicationsCount(daysBefore?: number): Promise<number>;
|
||||
deleteForApplication(appName: string): Promise<void>;
|
||||
}
|
||||
|
@ -75,6 +75,10 @@ export default class FakeClientInstanceStore implements IClientInstanceStore {
|
||||
return Array.from(apps.values());
|
||||
}
|
||||
|
||||
async getDistinctApplicationsCount(): Promise<number> {
|
||||
return this.getDistinctApplications().then((apps) => apps.length);
|
||||
}
|
||||
|
||||
async insert(details: INewClientInstance): Promise<void> {
|
||||
this.instances.push({ createdAt: new Date(), ...details });
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user