From d17ae37800d77e323f5b59c235f26cce7c623e75 Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Fri, 31 May 2024 12:40:46 +0300 Subject: [PATCH] feat: now CLIENT_METRICS event will be emitted with new structure (#7210) 1. CLIENT_METRICS event will be emitted with new structure 2. CLIENT_METRICS event will be emitted from bulkMetrics endpoint --- .../feature-lifecycle-service.test.ts | 10 +++-- .../feature-lifecycle-service.ts | 24 +++++++---- .../feature-lifecycle.e2e.test.ts | 43 ++++++++++--------- .../client-metrics/metrics-service-v2.ts | 2 +- src/lib/features/metrics/instance/metrics.ts | 11 +++-- src/lib/metrics.test.ts | 15 +++---- src/lib/metrics.ts | 19 ++++---- 7 files changed, 69 insertions(+), 55 deletions(-) diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle-service.test.ts b/src/lib/features/feature-lifecycle/feature-lifecycle-service.test.ts index 747365d7c6..61b80cf665 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle-service.test.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle-service.test.ts @@ -33,10 +33,12 @@ test('can insert and read lifecycle stages', async () => { ); function emitMetricsEvent(environment: string) { - eventBus.emit(CLIENT_METRICS, { - bucket: { toggles: { [featureName]: 'irrelevant' } }, - environment, - }); + eventBus.emit(CLIENT_METRICS, [ + { + featureName, + environment, + }, + ]); } function reachedStage(feature: string, name: StageName) { return new Promise((resolve) => diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts b/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts index 62978b04b4..13a4bb0457 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle-service.ts @@ -20,9 +20,10 @@ import type { import EventEmitter from 'events'; import type { Logger } from '../../logger'; import type EventService from '../events/event-service'; -import type { ValidatedClientMetrics } from '../metrics/shared/schema'; import type { FeatureLifecycleCompletedSchema } from '../../openapi'; import { calculateStageDurations } from './calculate-stage-durations'; +import type { IClientMetricsEnv } from '../metrics/client-metrics/client-metrics-store-v2-type'; +import groupBy from 'lodash.groupby'; export const STAGE_ENTERED = 'STAGE_ENTERED'; @@ -95,13 +96,20 @@ export class FeatureLifecycleService extends EventEmitter { }); this.eventBus.on( CLIENT_METRICS, - async (event: ValidatedClientMetrics) => { - if (event.environment) { - const features = Object.keys(event.bucket.toggles); - const environment = event.environment; - await this.checkEnabled(() => - this.featuresReceivedMetrics(features, environment), - ); + async (events: IClientMetricsEnv[]) => { + if (events.length > 0) { + const groupedByEnvironment = groupBy(events, 'environment'); + + for (const [environment, metrics] of Object.entries( + groupedByEnvironment, + )) { + const features = metrics.map( + (metric) => metric.featureName, + ); + await this.checkEnabled(() => + this.featuresReceivedMetrics(features, environment), + ); + } } }, ); diff --git a/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts b/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts index c4363041dd..541730ae64 100644 --- a/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts +++ b/src/lib/features/feature-lifecycle/feature-lifecycle.e2e.test.ts @@ -121,29 +121,32 @@ test('should return lifecycle stages', async () => { eventStore.emit(FEATURE_CREATED, { featureName: 'my_feature_a' }); await reachedStage('my_feature_a', 'initial'); await expectFeatureStage('my_feature_a', 'initial'); - eventBus.emit(CLIENT_METRICS, { - bucket: { - toggles: { - my_feature_a: 'irrelevant', - non_existent_feature: 'irrelevant', - }, + eventBus.emit(CLIENT_METRICS, [ + { + featureName: 'my_feature_a', + environment: 'default', }, - environment: 'default', - }); + { + featureName: 'non_existent_feature', + environment: 'default', + }, + ]); + // missing feature - eventBus.emit(CLIENT_METRICS, { - environment: 'default', - bucket: { toggles: {} }, - }); - // non existent env - eventBus.emit(CLIENT_METRICS, { - bucket: { - toggles: { - my_feature_a: 'irrelevant', - }, + eventBus.emit(CLIENT_METRICS, [ + { + environment: 'default', + yes: 0, + no: 0, }, - environment: 'non-existent', - }); + ]); + // non existent env + eventBus.emit(CLIENT_METRICS, [ + { + featureName: 'my_feature_a', + environment: 'non-existent', + }, + ]); await reachedStage('my_feature_a', 'live'); await expectFeatureStage('my_feature_a', 'live'); eventStore.emit(FEATURE_ARCHIVED, { featureName: 'my_feature_a' }); diff --git a/src/lib/features/metrics/client-metrics/metrics-service-v2.ts b/src/lib/features/metrics/client-metrics/metrics-service-v2.ts index af3d4f17d1..47c614118a 100644 --- a/src/lib/features/metrics/client-metrics/metrics-service-v2.ts +++ b/src/lib/features/metrics/client-metrics/metrics-service-v2.ts @@ -171,7 +171,7 @@ export default class ClientMetricsServiceV2 { ); await this.registerBulkMetrics(clientMetrics); - this.config.eventBus.emit(CLIENT_METRICS, value); + this.config.eventBus.emit(CLIENT_METRICS, clientMetrics); } } diff --git a/src/lib/features/metrics/instance/metrics.ts b/src/lib/features/metrics/instance/metrics.ts index c7f1681793..ca42e2854b 100644 --- a/src/lib/features/metrics/instance/metrics.ts +++ b/src/lib/features/metrics/instance/metrics.ts @@ -1,9 +1,10 @@ import type { Response } from 'express'; import Controller from '../../../routes/controller'; -import type { - IFlagResolver, - IUnleashConfig, - IUnleashServices, +import { + CLIENT_METRICS, + type IFlagResolver, + type IUnleashConfig, + type IUnleashServices, } from '../../../types'; import type ClientInstanceService from './instance-service'; import type { Logger } from '../../../logger'; @@ -161,8 +162,10 @@ export default class ClientMetricsController extends Controller { promises.push( this.metricsV2.registerBulkMetrics(filteredData), ); + this.config.eventBus.emit(CLIENT_METRICS, data); } await Promise.all(promises); + res.status(202).end(); } catch (e) { res.status(400).end(); diff --git a/src/lib/metrics.test.ts b/src/lib/metrics.test.ts index 493272aa6c..b8cb5b0a63 100644 --- a/src/lib/metrics.test.ts +++ b/src/lib/metrics.test.ts @@ -162,16 +162,13 @@ test('should set environmentType when toggle is flipped', async () => { }); test('should collect metrics for client metric reports', async () => { - eventBus.emit(CLIENT_METRICS, { - bucket: { - toggles: { - TestToggle: { - yes: 10, - no: 5, - }, - }, + eventBus.emit(CLIENT_METRICS, [ + { + featureName: 'TestToggle', + yes: 10, + no: 5, }, - }); + ]); const metrics = await prometheusRegister.metrics(); expect(metrics).toMatch( diff --git a/src/lib/metrics.ts b/src/lib/metrics.ts index 6bfc50368b..833c2aabab 100644 --- a/src/lib/metrics.ts +++ b/src/lib/metrics.ts @@ -24,7 +24,6 @@ import type { IUnleashConfig } from './types/option'; import type { ISettingStore, IUnleashStores } from './types/stores'; import { hoursToMilliseconds, minutesToMilliseconds } from 'date-fns'; import type { InstanceStatsService } from './features/instance-stats/instance-stats-service'; -import type { ValidatedClientMetrics } from './features/metrics/shared/schema'; import type { IEnvironment } from './types'; import { createCounter, @@ -33,6 +32,7 @@ import { createHistogram, } from './util/metrics'; import type { SchedulerService } from './services'; +import type { IClientMetricsEnv } from './features/metrics/client-metrics/client-metrics-store-v2-type'; export default class MetricsMonitor { constructor() {} @@ -617,30 +617,31 @@ export default class MetricsMonitor { }); const logger = config.getLogger('metrics.ts'); - eventBus.on(CLIENT_METRICS, (m: ValidatedClientMetrics) => { + eventBus.on(CLIENT_METRICS, (metrics: IClientMetricsEnv[]) => { try { - for (const entry of Object.entries(m.bucket.toggles)) { + for (const metric of metrics) { featureFlagUsageTotal.increment( { - toggle: entry[0], + toggle: metric.featureName, active: 'true', - appName: m.appName, + appName: metric.appName, }, - entry[1].yes, + metric.yes, ); featureFlagUsageTotal.increment( { - toggle: entry[0], + toggle: metric.featureName, active: 'false', - appName: m.appName, + appName: metric.appName, }, - entry[1].no, + metric.no, ); } } catch (e) { logger.warn('Metrics registration failed', e); } }); + eventStore.on(CLIENT_REGISTER, (m) => { if (m.sdkVersion && m.sdkVersion.indexOf(':') > -1) { const [sdkName, sdkVersion] = m.sdkVersion.split(':');