From 555b27a65371a1b4600fa12ae509de720575d662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nuno=20G=C3=B3is?= Date: Mon, 7 Aug 2023 14:59:29 +0100 Subject: [PATCH] feat: add prom metric for total custom root roles (#4435) https://linear.app/unleash/issue/2-1293/label-our-metrics-about-roles-to-include-also-if-the-role-is-a-root Adds a Prometheus metric for total custom root roles. Also adds it to the instance telemetry collection. Q: Should we use a `labeledRoles` kind of metric instead, similar to what we're doing for `clientApps` and their ranges? --- .../admin/instance-privacy/InstancePrivacy.tsx | 2 ++ src/lib/db/role-store.ts | 17 +++++++++++++---- src/lib/metrics.test.ts | 2 +- src/lib/metrics.ts | 8 ++++++++ src/lib/routes/admin-api/instance-admin.ts | 1 + src/lib/services/instance-stats-service.ts | 5 +++++ src/lib/services/version-service.ts | 7 +++++++ src/lib/types/stores/role-store.ts | 2 ++ src/test/fixtures/fake-role-store.ts | 5 +++++ 9 files changed, 44 insertions(+), 5 deletions(-) diff --git a/frontend/src/component/admin/instance-privacy/InstancePrivacy.tsx b/frontend/src/component/admin/instance-privacy/InstancePrivacy.tsx index 618a4eadc4..e2c08f240d 100644 --- a/frontend/src/component/admin/instance-privacy/InstancePrivacy.tsx +++ b/frontend/src/component/admin/instance-privacy/InstancePrivacy.tsx @@ -42,6 +42,8 @@ const featureCollectionDetails = { 'Context Fields': 'The number of custom context fields in use', Groups: 'The number of groups present in your instance', Roles: 'The number of custom roles defined in your instance', + 'Custom Root Roles': + 'The number of custom root roles defined in your instance', Environments: 'The number of environments in your instance', Segments: 'The number of segments defined in your instance', Strategies: 'The number of strategies defined in your instance', diff --git a/src/lib/db/role-store.ts b/src/lib/db/role-store.ts index 468830861c..104d72329d 100644 --- a/src/lib/db/role-store.ts +++ b/src/lib/db/role-store.ts @@ -9,6 +9,8 @@ import { } from 'lib/types/stores/role-store'; import { IRole, IUserRole } from 'lib/types/stores/access-store'; import { Db } from './db'; +import { PROJECT_ROLE_TYPES, ROOT_ROLE_TYPES } from '../util'; +import { RoleSchema } from 'lib/openapi'; const T = { ROLE_USER: 'role_user', @@ -54,6 +56,14 @@ export default class RoleStore implements IRoleStore { .then((res) => Number(res[0].count)); } + async filteredCount(filter: Partial): Promise { + return this.db + .from(T.ROLES) + .count('*') + .where(filter) + .then((res) => Number(res[0].count)); + } + async create(role: ICustomRoleInsert): Promise { const row = await this.db(T.ROLES) .insert({ @@ -144,8 +154,7 @@ export default class RoleStore implements IRoleStore { return this.db .select(['id', 'name', 'type', 'description']) .from(T.ROLES) - .where('type', 'custom') - .orWhere('type', 'project'); + .whereIn('type', PROJECT_ROLE_TYPES); } async getRolesForProject(projectId: string): Promise { @@ -160,7 +169,7 @@ export default class RoleStore implements IRoleStore { return this.db .select(['id', 'name', 'type', 'description']) .from(T.ROLES) - .whereIn('type', ['root', 'root-custom']); + .whereIn('type', ROOT_ROLE_TYPES); } async removeRolesForProject(projectId: string): Promise { @@ -177,7 +186,7 @@ export default class RoleStore implements IRoleStore { .distinctOn('user_id') .from(`${T.ROLES} AS r`) .leftJoin(`${T.ROLE_USER} AS ru`, 'r.id', 'ru.role_id') - .whereIn('r.type', ['root', 'root-custom']); + .whereIn('r.type', ROOT_ROLE_TYPES); return rows.map((row) => ({ roleId: Number(row.id), diff --git a/src/lib/metrics.test.ts b/src/lib/metrics.test.ts index ccbc43359b..51d4d26f6f 100644 --- a/src/lib/metrics.test.ts +++ b/src/lib/metrics.test.ts @@ -122,7 +122,7 @@ 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 () => { +test('should collect metrics for total client apps', async () => { await new Promise((done) => { setTimeout(done, 10); }); diff --git a/src/lib/metrics.ts b/src/lib/metrics.ts index d8b04efc1d..6249e6a848 100644 --- a/src/lib/metrics.ts +++ b/src/lib/metrics.ts @@ -104,6 +104,11 @@ export default class MetricsMonitor { help: 'Number of roles', }); + const customRootRolesTotal = new client.Gauge({ + name: 'custom_root_roles_total', + help: 'Number of custom root roles', + }); + const segmentsTotal = new client.Gauge({ name: 'segments_total', help: 'Number of segments', @@ -169,6 +174,9 @@ export default class MetricsMonitor { rolesTotal.reset(); rolesTotal.set(stats.roles); + customRootRolesTotal.reset(); + customRootRolesTotal.set(stats.customRootRoles); + segmentsTotal.reset(); segmentsTotal.set(stats.segments); diff --git a/src/lib/routes/admin-api/instance-admin.ts b/src/lib/routes/admin-api/instance-admin.ts index 120ee7218d..32942069bf 100644 --- a/src/lib/routes/admin-api/instance-admin.ts +++ b/src/lib/routes/admin-api/instance-admin.ts @@ -99,6 +99,7 @@ class InstanceAdminController extends Controller { instanceId: 'ed3861ae-78f9-4e8c-8e57-b57efc15f82b', projects: 1, roles: 5, + customRootRoles: 1, segments: 2, strategies: 8, sum: 'some-sha256-hash', diff --git a/src/lib/services/instance-stats-service.ts b/src/lib/services/instance-stats-service.ts index e45bf3acd7..70cbe20b19 100644 --- a/src/lib/services/instance-stats-service.ts +++ b/src/lib/services/instance-stats-service.ts @@ -18,6 +18,7 @@ import { IRoleStore } from '../types/stores/role-store'; import VersionService from './version-service'; import { ISettingStore } from '../types/stores/settings-store'; import { FEATURES_EXPORTED, FEATURES_IMPORTED } from '../types'; +import { CUSTOM_ROOT_ROLE_TYPE } from '../util'; export type TimeRange = 'allTime' | '30d' | '7d'; @@ -31,6 +32,7 @@ export interface InstanceStats { projects: number; contextFields: number; roles: number; + customRootRoles: number; featureExports: number; featureImports: number; groups: number; @@ -177,6 +179,7 @@ export class InstanceStatsService { contextFields, groups, roles, + customRootRoles, environments, segments, strategies, @@ -192,6 +195,7 @@ export class InstanceStatsService { this.contextFieldStore.count(), this.groupStore.count(), this.roleStore.count(), + this.roleStore.filteredCount({ type: CUSTOM_ROOT_ROLE_TYPE }), this.environmentStore.count(), this.segmentStore.count(), this.strategyStore.count(), @@ -212,6 +216,7 @@ export class InstanceStatsService { projects, contextFields, roles, + customRootRoles, groups, environments, segments, diff --git a/src/lib/services/version-service.ts b/src/lib/services/version-service.ts index 6dca45bc79..84759dd8b7 100644 --- a/src/lib/services/version-service.ts +++ b/src/lib/services/version-service.ts @@ -19,6 +19,7 @@ import { ISettingStore } from '../types/stores/settings-store'; import { hoursToMilliseconds } from 'date-fns'; import { IStrategyStore } from 'lib/types'; import { FEATURES_EXPORTED, FEATURES_IMPORTED } from '../types'; +import { CUSTOM_ROOT_ROLE_TYPE } from '../util'; export interface IVersionInfo { oss: string; @@ -46,6 +47,7 @@ export interface IFeatureUsageInfo { projects: number; contextFields: number; roles: number; + customRootRoles: number; featureExports: number; featureImports: number; groups: number; @@ -226,6 +228,7 @@ export default class VersionService { contextFields, groups, roles, + customRootRoles, environments, segments, strategies, @@ -242,6 +245,9 @@ export default class VersionService { this.contextFieldStore.count(), this.groupStore.count(), this.roleStore.count(), + this.roleStore.filteredCount({ + type: CUSTOM_ROOT_ROLE_TYPE, + }), this.environmentStore.count(), this.segmentStore.count(), this.strategyStore.count(), @@ -262,6 +268,7 @@ export default class VersionService { contextFields, groups, roles, + customRootRoles, environments, segments, strategies, diff --git a/src/lib/types/stores/role-store.ts b/src/lib/types/stores/role-store.ts index 7b809f28fe..1bc367aa6c 100644 --- a/src/lib/types/stores/role-store.ts +++ b/src/lib/types/stores/role-store.ts @@ -1,3 +1,4 @@ +import { RoleSchema } from 'lib/openapi'; import { ICustomRole } from '../model'; import { IRole, IUserRole } from './access-store'; import { Store } from './store'; @@ -29,4 +30,5 @@ export interface IRoleStore extends Store { getRootRoleForAllUsers(): Promise; nameInUse(name: string, existingId?: number): Promise; count(): Promise; + filteredCount(filter: Partial): Promise; } diff --git a/src/test/fixtures/fake-role-store.ts b/src/test/fixtures/fake-role-store.ts index b4888c8cd4..bcf22bea67 100644 --- a/src/test/fixtures/fake-role-store.ts +++ b/src/test/fixtures/fake-role-store.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ +import { RoleSchema } from 'lib/openapi'; import { ICustomRole } from 'lib/types/model'; import { IRole, IUserRole } from 'lib/types/stores/access-store'; import { @@ -12,6 +13,10 @@ export default class FakeRoleStore implements IRoleStore { return Promise.resolve(0); } + filteredCount(search: Partial): Promise { + return Promise.resolve(0); + } + roles: ICustomRole[] = []; getGroupRolesForProject(projectId: string): Promise {