From 07fcdbb053e0e189674ab5405f6d08335590f23e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Wed, 29 Nov 2023 13:09:30 +0100 Subject: [PATCH] fix: add metrics for service account and api tokens (#5478) --- src/lib/db/api-token-store.ts | 16 +++++++++++- src/lib/db/user-store.ts | 10 +++++++ .../instance-stats/instance-stats-service.ts | 19 +++++++++++++- src/lib/metrics.ts | 26 ++++++++++++++++--- src/lib/routes/admin-api/instance-admin.ts | 2 ++ src/lib/types/stores/api-token-store.ts | 1 + src/lib/types/stores/user-store.ts | 1 + src/test/fixtures/fake-api-token-store.ts | 9 ++++++- src/test/fixtures/fake-user-store.ts | 3 +++ 9 files changed, 81 insertions(+), 6 deletions(-) diff --git a/src/lib/db/api-token-store.ts b/src/lib/db/api-token-store.ts index 22af6db431..031df26cc3 100644 --- a/src/lib/db/api-token-store.ts +++ b/src/lib/db/api-token-store.ts @@ -96,12 +96,26 @@ export class ApiTokenStore implements IApiTokenStore { }); } - count(): Promise { + async count(): Promise { return this.db(TABLE) .count('*') .then((res) => Number(res[0].count)); } + async countByType(): Promise> { + return this.db(TABLE) + .select('type') + .count('*') + .groupBy('type') + .then((res) => { + const map = new Map(); + res.forEach((row) => { + map.set(row.type.toString(), Number(row.count)); + }); + return map; + }); + } + async getAll(): Promise { const stopTimer = this.timer('getAll'); const rows = await this.makeTokenProjectQuery(); diff --git a/src/lib/db/user-store.ts b/src/lib/db/user-store.ts index 6141ecff6a..ab0b0b9d1c 100644 --- a/src/lib/db/user-store.ts +++ b/src/lib/db/user-store.ts @@ -201,6 +201,16 @@ class UserStore implements IUserStore { .then((res) => Number(res[0].count)); } + async countServiceAccounts(): Promise { + return this.db(TABLE) + .where({ + deleted_at: null, + is_service: true, + }) + .count('*') + .then((res) => Number(res[0].count)); + } + destroy(): void {} async exists(id: number): Promise { diff --git a/src/lib/features/instance-stats/instance-stats-service.ts b/src/lib/features/instance-stats/instance-stats-service.ts index 8420783215..2154f7e99b 100644 --- a/src/lib/features/instance-stats/instance-stats-service.ts +++ b/src/lib/features/instance-stats/instance-stats-service.ts @@ -17,7 +17,11 @@ import { ISegmentStore } from '../../types/stores/segment-store'; import { IRoleStore } from '../../types/stores/role-store'; import VersionService from '../../services/version-service'; import { ISettingStore } from '../../types/stores/settings-store'; -import { FEATURES_EXPORTED, FEATURES_IMPORTED } from '../../types'; +import { + FEATURES_EXPORTED, + FEATURES_IMPORTED, + IApiTokenStore, +} from '../../types'; import { CUSTOM_ROOT_ROLE_TYPE } from '../../util'; import { type GetActiveUsers } from './getActiveUsers'; import { ProjectModeCount } from '../../db/project-store'; @@ -31,6 +35,8 @@ export interface InstanceStats { versionOSS: string; versionEnterprise?: string; users: number; + serviceAccounts: number; + apiTokens: Map; featureToggles: number; projects: ProjectModeCount[]; contextFields: number; @@ -78,6 +84,8 @@ export class InstanceStatsService { private eventStore: IEventStore; + private apiTokenStore: IApiTokenStore; + private versionService: VersionService; private settingStore: ISettingStore; @@ -106,6 +114,7 @@ export class InstanceStatsService { settingStore, clientInstanceStore, eventStore, + apiTokenStore, }: Pick< IUnleashStores, | 'featureToggleStore' @@ -120,6 +129,7 @@ export class InstanceStatsService { | 'settingStore' | 'clientInstanceStore' | 'eventStore' + | 'apiTokenStore' >, { getLogger }: Pick, versionService: VersionService, @@ -142,6 +152,7 @@ export class InstanceStatsService { this.logger = getLogger('services/stats-service.js'); this.getActiveUsers = getActiveUsers; this.getProductionChanges = getProductionChanges; + this.apiTokenStore = apiTokenStore; } async refreshStatsSnapshot(): Promise { @@ -194,6 +205,8 @@ export class InstanceStatsService { const [ featureToggles, users, + serviceAccounts, + apiTokens, activeUsers, projects, contextFields, @@ -213,6 +226,8 @@ export class InstanceStatsService { ] = await Promise.all([ this.getToggleCount(), this.userStore.count(), + this.userStore.countServiceAccounts(), + this.apiTokenStore.countByType(), this.getActiveUsers(), this.getProjectModeCount(), this.contextFieldStore.count(), @@ -237,6 +252,8 @@ export class InstanceStatsService { versionOSS: versionInfo.current.oss, versionEnterprise: versionInfo.current.enterprise, users, + serviceAccounts, + apiTokens, activeUsers, featureToggles, projects, diff --git a/src/lib/metrics.ts b/src/lib/metrics.ts index 98f0bad420..3b6bda5dae 100644 --- a/src/lib/metrics.ts +++ b/src/lib/metrics.ts @@ -31,8 +31,8 @@ export default class MetricsMonitor { poolMetricsTimer?: Timer; constructor() { - this.timer = null; - this.poolMetricsTimer = null; + this.timer = undefined; + this.poolMetricsTimer = undefined; } startMonitoring( @@ -44,7 +44,7 @@ export default class MetricsMonitor { db: Knex, ): Promise { if (!config.server.serverMetrics) { - return; + return Promise.resolve(); } const { eventStore } = stores; @@ -94,6 +94,15 @@ export default class MetricsMonitor { name: 'users_total', help: 'Number of users', }); + const serviceAccounts = new client.Gauge({ + name: 'service_accounts_total', + help: 'Number of service accounts', + }); + const apiTokens = new client.Gauge({ + name: 'api_tokens_total', + help: 'Number of API tokens', + labelNames: ['type'], + }); const usersActive7days = new client.Gauge({ name: 'users_active_7', help: 'Number of users active in the last 7 days', @@ -214,6 +223,15 @@ export default class MetricsMonitor { usersTotal.reset(); usersTotal.set(stats.users); + serviceAccounts.reset(); + serviceAccounts.set(stats.serviceAccounts); + + apiTokens.reset(); + + for (const [type, value] of stats.apiTokens) { + apiTokens.labels(type).set(value); + } + usersActive7days.reset(); usersActive7days.set(stats.activeUsers.last7); usersActive30days.reset(); @@ -420,6 +438,8 @@ export default class MetricsMonitor { }); this.configureDbMetrics(db, eventBus); + + return Promise.resolve(); } stopMonitoring(): void { diff --git a/src/lib/routes/admin-api/instance-admin.ts b/src/lib/routes/admin-api/instance-admin.ts index 374cf9f132..b8c8457bb8 100644 --- a/src/lib/routes/admin-api/instance-admin.ts +++ b/src/lib/routes/admin-api/instance-admin.ts @@ -105,6 +105,8 @@ class InstanceAdminController extends Controller { sum: 'some-sha256-hash', timestamp: new Date(2023, 6, 12, 10, 0, 0, 0), users: 10, + serviceAccounts: 2, + apiTokens: new Map([]), versionEnterprise: '5.1.7', versionOSS: '5.1.7', activeUsers: { diff --git a/src/lib/types/stores/api-token-store.ts b/src/lib/types/stores/api-token-store.ts index 516beae3b4..f6df32860b 100644 --- a/src/lib/types/stores/api-token-store.ts +++ b/src/lib/types/stores/api-token-store.ts @@ -7,4 +7,5 @@ export interface IApiTokenStore extends Store { setExpiry(secret: string, expiresAt: Date): Promise; markSeenAt(secrets: string[]): Promise; count(): Promise; + countByType(): Promise>; } diff --git a/src/lib/types/stores/user-store.ts b/src/lib/types/stores/user-store.ts index 32b3beaa83..0506be91e1 100644 --- a/src/lib/types/stores/user-store.ts +++ b/src/lib/types/stores/user-store.ts @@ -32,4 +32,5 @@ export interface IUserStore extends Store { incLoginAttempts(user: IUser): Promise; successfullyLogin(user: IUser): Promise; count(): Promise; + countServiceAccounts(): Promise; } diff --git a/src/test/fixtures/fake-api-token-store.ts b/src/test/fixtures/fake-api-token-store.ts index 6064700eb8..f36ed67eb2 100644 --- a/src/test/fixtures/fake-api-token-store.ts +++ b/src/test/fixtures/fake-api-token-store.ts @@ -1,5 +1,9 @@ import { IApiTokenStore } from '../../lib/types/stores/api-token-store'; -import { IApiToken, IApiTokenCreate } from '../../lib/types/models/api-token'; +import { + ApiTokenType, + IApiToken, + IApiTokenCreate, +} from '../../lib/types/models/api-token'; import NotFoundError from '../../lib/error/notfound-error'; import EventEmitter from 'events'; @@ -8,6 +12,9 @@ export default class FakeApiTokenStore extends EventEmitter implements IApiTokenStore { + countByType(): Promise> { + return Promise.resolve(new Map()); + } tokens: IApiToken[] = []; async delete(key: string): Promise { diff --git a/src/test/fixtures/fake-user-store.ts b/src/test/fixtures/fake-user-store.ts index bd78c34de0..7ec37699e3 100644 --- a/src/test/fixtures/fake-user-store.ts +++ b/src/test/fixtures/fake-user-store.ts @@ -14,6 +14,9 @@ class UserStoreMock implements IUserStore { this.idSeq = 1; this.data = []; } + countServiceAccounts(): Promise { + return Promise.resolve(0); + } async hasUser({ id,