From 24b202ef0b4bc51c988b464e5c5653dca776102d Mon Sep 17 00:00:00 2001 From: Gard Rimestad Date: Tue, 9 Jan 2024 16:33:00 +0100 Subject: [PATCH] feat: include environment type label in feature_toggle_update metrics (#5809) This is needed in order to identify what type of an environment a toggle is updated in. This can be test, development, pre-production or production. --- src/lib/metrics.test.ts | 33 +++++++++++++++- src/lib/metrics.ts | 84 ++++++++++++++++++++++++++++++++--------- 2 files changed, 99 insertions(+), 18 deletions(-) diff --git a/src/lib/metrics.test.ts b/src/lib/metrics.test.ts index af09315a7197..c0632f277ca0 100644 --- a/src/lib/metrics.test.ts +++ b/src/lib/metrics.test.ts @@ -6,6 +6,7 @@ import { REQUEST_TIME, DB_TIME } from './metric-events'; import { CLIENT_METRICS, CLIENT_REGISTER, + FEATURE_ENVIRONMENT_ENABLED, FEATURE_UPDATED, } from './types/events'; import { createMetricsMonitor } from './metrics'; @@ -14,11 +15,14 @@ import { InstanceStatsService } from './features/instance-stats/instance-stats-s import VersionService from './services/version-service'; import { createFakeGetActiveUsers } from './features/instance-stats/getActiveUsers'; import { createFakeGetProductionChanges } from './features/instance-stats/getProductionChanges'; +import { IEnvironmentStore } from './types'; +import FakeEnvironmentStore from './features/project-environments/fake-environment-store'; const monitor = createMetricsMonitor(); const eventBus = new EventEmitter(); const prometheusRegister = register; let eventStore: IEventStore; +let environmentStore: IEnvironmentStore; let statsService: InstanceStatsService; let stores; beforeAll(() => { @@ -29,6 +33,8 @@ beforeAll(() => { }); stores = createStores(); eventStore = stores.eventStore; + environmentStore = new FakeEnvironmentStore(); + stores.environmentStore = environmentStore; const versionService = new VersionService( stores, config, @@ -93,7 +99,32 @@ test('should collect metrics for updated toggles', async () => { const metrics = await prometheusRegister.metrics(); expect(metrics).toMatch( - /feature_toggle_update_total\{toggle="TestToggle",project="default",environment="default"\} 1/, + /feature_toggle_update_total\{toggle="TestToggle",project="default",environment="default",environmentType="production"\} 1/, + ); +}); + +test('should set environmentType when toggle is flipped', async () => { + await environmentStore.create({ + name: 'testEnvironment', + enabled: true, + type: 'testType', + sortOrder: 1, + }); + stores.eventStore.emit(FEATURE_ENVIRONMENT_ENABLED, { + featureName: 'TestToggle', + project: 'default', + environment: 'testEnvironment', + data: { name: 'TestToggle' }, + }); + + // Wait for event to be processed, not nice, but it works. + await new Promise((done) => { + setTimeout(done, 1); + }); + const metrics = await prometheusRegister.metrics(); + + expect(metrics).toMatch( + /feature_toggle_update_total\{toggle="TestToggle",project="default",environment="testEnvironment",environmentType="testType"\} 1/, ); }); diff --git a/src/lib/metrics.ts b/src/lib/metrics.ts index 3b6bda5dae57..87eba0901e60 100644 --- a/src/lib/metrics.ts +++ b/src/lib/metrics.ts @@ -1,4 +1,5 @@ import client from 'prom-client'; +import memoizee from 'memoizee'; import EventEmitter from 'events'; import { Knex } from 'knex'; import * as events from './metric-events'; @@ -19,11 +20,12 @@ import { CLIENT_REGISTER, } from './types/events'; import { IUnleashConfig } from './types/option'; -import { IUnleashStores } from './types/stores'; +import { IEnvironmentStore, IUnleashStores } from './types/stores'; import { hoursToMilliseconds, minutesToMilliseconds } from 'date-fns'; import Timer = NodeJS.Timer; import { InstanceStatsService } from './features/instance-stats/instance-stats-service'; import { ValidatedClientMetrics } from './services/client-metrics/schema'; +import { IEnvironment } from './types'; export default class MetricsMonitor { timer?: Timer; @@ -47,7 +49,15 @@ export default class MetricsMonitor { return Promise.resolve(); } - const { eventStore } = stores; + const { eventStore, environmentStore } = stores; + + const cachedEnvironments: () => Promise = memoizee( + async () => environmentStore.getAll(), + { + promise: true, + maxAge: hoursToMilliseconds(1), + }, + ); client.collectDefaultMetrics(); @@ -78,7 +88,7 @@ export default class MetricsMonitor { const featureToggleUpdateTotal = new client.Counter({ name: 'feature_toggle_update_total', help: 'Number of times a toggle has been updated. Environment label would be "n/a" when it is not available, e.g. when a feature toggle is created.', - labelNames: ['toggle', 'project', 'environment'], + labelNames: ['toggle', 'project', 'environment', 'environmentType'], }); const featureToggleUsageTotal = new client.Counter({ name: 'feature_toggle_usage_total', @@ -360,56 +370,82 @@ export default class MetricsMonitor { }); eventStore.on(FEATURE_CREATED, ({ featureName, project }) => { - featureToggleUpdateTotal.labels(featureName, project, 'n/a').inc(); + featureToggleUpdateTotal + .labels(featureName, project, 'n/a', 'n/a') + .inc(); }); eventStore.on(FEATURE_VARIANTS_UPDATED, ({ featureName, project }) => { - featureToggleUpdateTotal.labels(featureName, project, 'n/a').inc(); + featureToggleUpdateTotal + .labels(featureName, project, 'n/a', 'n/a') + .inc(); }); eventStore.on(FEATURE_METADATA_UPDATED, ({ featureName, project }) => { - featureToggleUpdateTotal.labels(featureName, project, 'n/a').inc(); + featureToggleUpdateTotal + .labels(featureName, project, 'n/a', 'n/a') + .inc(); }); eventStore.on(FEATURE_UPDATED, ({ featureName, project }) => { featureToggleUpdateTotal - .labels(featureName, project, 'default') + .labels(featureName, project, 'default', 'production') .inc(); }); eventStore.on( FEATURE_STRATEGY_ADD, - ({ featureName, project, environment }) => { + async ({ featureName, project, environment }) => { + const environmentType = await this.resolveEnvironmentType( + environment, + cachedEnvironments, + ); featureToggleUpdateTotal - .labels(featureName, project, environment) + .labels(featureName, project, environment, environmentType) .inc(); }, ); eventStore.on( FEATURE_STRATEGY_REMOVE, - ({ featureName, project, environment }) => { + async ({ featureName, project, environment }) => { + const environmentType = await this.resolveEnvironmentType( + environment, + cachedEnvironments, + ); featureToggleUpdateTotal - .labels(featureName, project, environment) + .labels(featureName, project, environment, environmentType) .inc(); }, ); eventStore.on( FEATURE_STRATEGY_UPDATE, - ({ featureName, project, environment }) => { + async ({ featureName, project, environment }) => { + const environmentType = await this.resolveEnvironmentType( + environment, + cachedEnvironments, + ); featureToggleUpdateTotal - .labels(featureName, project, environment) + .labels(featureName, project, environment, environmentType) .inc(); }, ); eventStore.on( FEATURE_ENVIRONMENT_DISABLED, - ({ featureName, project, environment }) => { + async ({ featureName, project, environment }) => { + const environmentType = await this.resolveEnvironmentType( + environment, + cachedEnvironments, + ); featureToggleUpdateTotal - .labels(featureName, project, environment) + .labels(featureName, project, environment, environmentType) .inc(); }, ); eventStore.on( FEATURE_ENVIRONMENT_ENABLED, - ({ featureName, project, environment }) => { + async ({ featureName, project, environment }) => { + const environmentType = await this.resolveEnvironmentType( + environment, + cachedEnvironments, + ); featureToggleUpdateTotal - .labels(featureName, project, environment) + .labels(featureName, project, environment, environmentType) .inc(); }, ); @@ -504,6 +540,20 @@ export default class MetricsMonitor { // eslint-disable-next-line no-empty } catch (e) {} } + + async resolveEnvironmentType( + environment: string, + cachedEnvironments: () => Promise, + ): Promise { + const environments = await cachedEnvironments(); + const env = environments.find((e) => e.name === environment); + + if (env) { + return env.type; + } else { + return 'unknown'; + } + } } export function createMetricsMonitor(): MetricsMonitor { return new MetricsMonitor();