1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

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
This commit is contained in:
Jaanus Sellin 2024-05-31 12:40:46 +03:00 committed by GitHub
parent 3c73ce9dd9
commit d17ae37800
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 69 additions and 55 deletions

View File

@ -33,10 +33,12 @@ test('can insert and read lifecycle stages', async () => {
); );
function emitMetricsEvent(environment: string) { function emitMetricsEvent(environment: string) {
eventBus.emit(CLIENT_METRICS, { eventBus.emit(CLIENT_METRICS, [
bucket: { toggles: { [featureName]: 'irrelevant' } }, {
environment, featureName,
}); environment,
},
]);
} }
function reachedStage(feature: string, name: StageName) { function reachedStage(feature: string, name: StageName) {
return new Promise((resolve) => return new Promise((resolve) =>

View File

@ -20,9 +20,10 @@ import type {
import EventEmitter from 'events'; import EventEmitter from 'events';
import type { Logger } from '../../logger'; import type { Logger } from '../../logger';
import type EventService from '../events/event-service'; import type EventService from '../events/event-service';
import type { ValidatedClientMetrics } from '../metrics/shared/schema';
import type { FeatureLifecycleCompletedSchema } from '../../openapi'; import type { FeatureLifecycleCompletedSchema } from '../../openapi';
import { calculateStageDurations } from './calculate-stage-durations'; 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'; export const STAGE_ENTERED = 'STAGE_ENTERED';
@ -95,13 +96,20 @@ export class FeatureLifecycleService extends EventEmitter {
}); });
this.eventBus.on( this.eventBus.on(
CLIENT_METRICS, CLIENT_METRICS,
async (event: ValidatedClientMetrics) => { async (events: IClientMetricsEnv[]) => {
if (event.environment) { if (events.length > 0) {
const features = Object.keys(event.bucket.toggles); const groupedByEnvironment = groupBy(events, 'environment');
const environment = event.environment;
await this.checkEnabled(() => for (const [environment, metrics] of Object.entries(
this.featuresReceivedMetrics(features, environment), groupedByEnvironment,
); )) {
const features = metrics.map(
(metric) => metric.featureName,
);
await this.checkEnabled(() =>
this.featuresReceivedMetrics(features, environment),
);
}
} }
}, },
); );

View File

@ -121,29 +121,32 @@ test('should return lifecycle stages', async () => {
eventStore.emit(FEATURE_CREATED, { featureName: 'my_feature_a' }); eventStore.emit(FEATURE_CREATED, { featureName: 'my_feature_a' });
await reachedStage('my_feature_a', 'initial'); await reachedStage('my_feature_a', 'initial');
await expectFeatureStage('my_feature_a', 'initial'); await expectFeatureStage('my_feature_a', 'initial');
eventBus.emit(CLIENT_METRICS, { eventBus.emit(CLIENT_METRICS, [
bucket: { {
toggles: { featureName: 'my_feature_a',
my_feature_a: 'irrelevant', environment: 'default',
non_existent_feature: 'irrelevant',
},
}, },
environment: 'default', {
}); featureName: 'non_existent_feature',
environment: 'default',
},
]);
// missing feature // missing feature
eventBus.emit(CLIENT_METRICS, { eventBus.emit(CLIENT_METRICS, [
environment: 'default', {
bucket: { toggles: {} }, environment: 'default',
}); yes: 0,
// non existent env no: 0,
eventBus.emit(CLIENT_METRICS, {
bucket: {
toggles: {
my_feature_a: 'irrelevant',
},
}, },
environment: 'non-existent', ]);
}); // non existent env
eventBus.emit(CLIENT_METRICS, [
{
featureName: 'my_feature_a',
environment: 'non-existent',
},
]);
await reachedStage('my_feature_a', 'live'); await reachedStage('my_feature_a', 'live');
await expectFeatureStage('my_feature_a', 'live'); await expectFeatureStage('my_feature_a', 'live');
eventStore.emit(FEATURE_ARCHIVED, { featureName: 'my_feature_a' }); eventStore.emit(FEATURE_ARCHIVED, { featureName: 'my_feature_a' });

View File

@ -171,7 +171,7 @@ export default class ClientMetricsServiceV2 {
); );
await this.registerBulkMetrics(clientMetrics); await this.registerBulkMetrics(clientMetrics);
this.config.eventBus.emit(CLIENT_METRICS, value); this.config.eventBus.emit(CLIENT_METRICS, clientMetrics);
} }
} }

View File

@ -1,9 +1,10 @@
import type { Response } from 'express'; import type { Response } from 'express';
import Controller from '../../../routes/controller'; import Controller from '../../../routes/controller';
import type { import {
IFlagResolver, CLIENT_METRICS,
IUnleashConfig, type IFlagResolver,
IUnleashServices, type IUnleashConfig,
type IUnleashServices,
} from '../../../types'; } from '../../../types';
import type ClientInstanceService from './instance-service'; import type ClientInstanceService from './instance-service';
import type { Logger } from '../../../logger'; import type { Logger } from '../../../logger';
@ -161,8 +162,10 @@ export default class ClientMetricsController extends Controller {
promises.push( promises.push(
this.metricsV2.registerBulkMetrics(filteredData), this.metricsV2.registerBulkMetrics(filteredData),
); );
this.config.eventBus.emit(CLIENT_METRICS, data);
} }
await Promise.all(promises); await Promise.all(promises);
res.status(202).end(); res.status(202).end();
} catch (e) { } catch (e) {
res.status(400).end(); res.status(400).end();

View File

@ -162,16 +162,13 @@ test('should set environmentType when toggle is flipped', async () => {
}); });
test('should collect metrics for client metric reports', async () => { test('should collect metrics for client metric reports', async () => {
eventBus.emit(CLIENT_METRICS, { eventBus.emit(CLIENT_METRICS, [
bucket: { {
toggles: { featureName: 'TestToggle',
TestToggle: { yes: 10,
yes: 10, no: 5,
no: 5,
},
},
}, },
}); ]);
const metrics = await prometheusRegister.metrics(); const metrics = await prometheusRegister.metrics();
expect(metrics).toMatch( expect(metrics).toMatch(

View File

@ -24,7 +24,6 @@ import type { IUnleashConfig } from './types/option';
import type { ISettingStore, IUnleashStores } from './types/stores'; import type { ISettingStore, IUnleashStores } from './types/stores';
import { hoursToMilliseconds, minutesToMilliseconds } from 'date-fns'; import { hoursToMilliseconds, minutesToMilliseconds } from 'date-fns';
import type { InstanceStatsService } from './features/instance-stats/instance-stats-service'; import type { InstanceStatsService } from './features/instance-stats/instance-stats-service';
import type { ValidatedClientMetrics } from './features/metrics/shared/schema';
import type { IEnvironment } from './types'; import type { IEnvironment } from './types';
import { import {
createCounter, createCounter,
@ -33,6 +32,7 @@ import {
createHistogram, createHistogram,
} from './util/metrics'; } from './util/metrics';
import type { SchedulerService } from './services'; import type { SchedulerService } from './services';
import type { IClientMetricsEnv } from './features/metrics/client-metrics/client-metrics-store-v2-type';
export default class MetricsMonitor { export default class MetricsMonitor {
constructor() {} constructor() {}
@ -617,30 +617,31 @@ export default class MetricsMonitor {
}); });
const logger = config.getLogger('metrics.ts'); const logger = config.getLogger('metrics.ts');
eventBus.on(CLIENT_METRICS, (m: ValidatedClientMetrics) => { eventBus.on(CLIENT_METRICS, (metrics: IClientMetricsEnv[]) => {
try { try {
for (const entry of Object.entries(m.bucket.toggles)) { for (const metric of metrics) {
featureFlagUsageTotal.increment( featureFlagUsageTotal.increment(
{ {
toggle: entry[0], toggle: metric.featureName,
active: 'true', active: 'true',
appName: m.appName, appName: metric.appName,
}, },
entry[1].yes, metric.yes,
); );
featureFlagUsageTotal.increment( featureFlagUsageTotal.increment(
{ {
toggle: entry[0], toggle: metric.featureName,
active: 'false', active: 'false',
appName: m.appName, appName: metric.appName,
}, },
entry[1].no, metric.no,
); );
} }
} catch (e) { } catch (e) {
logger.warn('Metrics registration failed', e); logger.warn('Metrics registration failed', e);
} }
}); });
eventStore.on(CLIENT_REGISTER, (m) => { eventStore.on(CLIENT_REGISTER, (m) => {
if (m.sdkVersion && m.sdkVersion.indexOf(':') > -1) { if (m.sdkVersion && m.sdkVersion.indexOf(':') > -1) {
const [sdkName, sdkVersion] = m.sdkVersion.split(':'); const [sdkName, sdkVersion] = m.sdkVersion.split(':');