1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-08 01:15:49 +02:00

feat: store memory footprints to grafana (#9001)

When there is new revision, we will start storing memory footprint for
old client-api and the new delta-api.
We will be sending it as prometheus metrics.

The memory size will only be recalculated if revision changes, which
does not happen very often.
This commit is contained in:
Jaanus Sellin 2024-12-19 13:15:30 +02:00 committed by GitHub
parent 3bed01bb8c
commit b701fec75d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 105 additions and 10 deletions

View File

@ -35,6 +35,7 @@ import {
import type ConfigurationRevisionService from '../feature-toggle/configuration-revision-service'; import type ConfigurationRevisionService from '../feature-toggle/configuration-revision-service';
import type { ClientFeatureToggleService } from './client-feature-toggle-service'; import type { ClientFeatureToggleService } from './client-feature-toggle-service';
import { import {
CLIENT_FEATURES_MEMORY,
CLIENT_METRICS_NAMEPREFIX, CLIENT_METRICS_NAMEPREFIX,
CLIENT_METRICS_TAGS, CLIENT_METRICS_TAGS,
} from '../../internals'; } from '../../internals';
@ -69,6 +70,8 @@ export default class FeatureController extends Controller {
private eventBus: EventEmitter; private eventBus: EventEmitter;
private clientFeaturesCacheMap = new Map<string, number>();
private featuresAndSegments: ( private featuresAndSegments: (
query: IFeatureToggleQuery, query: IFeatureToggleQuery,
etag: string, etag: string,
@ -162,6 +165,32 @@ export default class FeatureController extends Controller {
private async resolveFeaturesAndSegments( private async resolveFeaturesAndSegments(
query?: IFeatureToggleQuery, query?: IFeatureToggleQuery,
): Promise<[FeatureConfigurationClient[], IClientSegment[]]> { ): Promise<[FeatureConfigurationClient[], IClientSegment[]]> {
if (this.flagResolver.isEnabled('deltaApi')) {
const features =
await this.clientFeatureToggleService.getClientFeatures(query);
const segments =
await this.clientFeatureToggleService.getActiveSegmentsForClient();
try {
const featuresSize = this.getCacheSizeInBytes(features);
const segmentsSize = this.getCacheSizeInBytes(segments);
this.clientFeaturesCacheMap.set(
JSON.stringify(query),
featuresSize + segmentsSize,
);
await this.clientFeatureToggleService.getClientDelta(
undefined,
query!,
);
this.storeFootprint();
} catch (e) {
this.logger.error('Delta diff failed', e);
}
return [features, segments];
}
return Promise.all([ return Promise.all([
this.clientFeatureToggleService.getClientFeatures(query), this.clientFeatureToggleService.getClientFeatures(query),
this.clientFeatureToggleService.getActiveSegmentsForClient(), this.clientFeatureToggleService.getActiveSegmentsForClient(),
@ -270,7 +299,6 @@ export default class FeatureController extends Controller {
query, query,
etag, etag,
); );
if (this.clientSpecService.requestSupportsSpec(req, 'segments')) { if (this.clientSpecService.requestSupportsSpec(req, 'segments')) {
this.openApiService.respondWithValidation( this.openApiService.respondWithValidation(
200, 200,
@ -335,4 +363,17 @@ export default class FeatureController extends Controller {
}, },
); );
} }
storeFootprint() {
let memory = 0;
for (const value of this.clientFeaturesCacheMap.values()) {
memory += value;
}
this.eventBus.emit(CLIENT_FEATURES_MEMORY, { memory });
}
getCacheSizeInBytes(value: any): number {
const jsonString = JSON.stringify(value);
return Buffer.byteLength(jsonString, 'utf8');
}
} }

View File

@ -5,6 +5,7 @@ import type {
IFeatureToggleQuery, IFeatureToggleQuery,
IFlagResolver, IFlagResolver,
ISegmentReadModel, ISegmentReadModel,
IUnleashConfig,
} from '../../../types'; } from '../../../types';
import type ConfigurationRevisionService from '../../feature-toggle/configuration-revision-service'; import type ConfigurationRevisionService from '../../feature-toggle/configuration-revision-service';
import { UPDATE_REVISION } from '../../feature-toggle/configuration-revision-service'; import { UPDATE_REVISION } from '../../feature-toggle/configuration-revision-service';
@ -13,6 +14,9 @@ import type {
FeatureConfigurationDeltaClient, FeatureConfigurationDeltaClient,
IClientFeatureToggleDeltaReadModel, IClientFeatureToggleDeltaReadModel,
} from './client-feature-toggle-delta-read-model-type'; } from './client-feature-toggle-delta-read-model-type';
import { CLIENT_DELTA_MEMORY } from '../../../metric-events';
import type EventEmitter from 'events';
import type { Logger } from '../../../logger';
type DeletedFeature = { type DeletedFeature = {
name: string; name: string;
@ -86,7 +90,6 @@ export const calculateRequiredClientRevision = (
const targetedRevisions = revisions.filter( const targetedRevisions = revisions.filter(
(revision) => revision.revisionId > requiredRevisionId, (revision) => revision.revisionId > requiredRevisionId,
); );
console.log('targeted revisions', targetedRevisions);
const projectFeatureRevisions = targetedRevisions.map((revision) => const projectFeatureRevisions = targetedRevisions.map((revision) =>
filterRevisionByProject(revision, projects), filterRevisionByProject(revision, projects),
); );
@ -105,20 +108,23 @@ export class ClientFeatureToggleDelta {
private currentRevisionId: number = 0; private currentRevisionId: number = 0;
private interval: NodeJS.Timer;
private flagResolver: IFlagResolver; private flagResolver: IFlagResolver;
private configurationRevisionService: ConfigurationRevisionService; private configurationRevisionService: ConfigurationRevisionService;
private readonly segmentReadModel: ISegmentReadModel; private readonly segmentReadModel: ISegmentReadModel;
private eventBus: EventEmitter;
private readonly logger: Logger;
constructor( constructor(
clientFeatureToggleDeltaReadModel: IClientFeatureToggleDeltaReadModel, clientFeatureToggleDeltaReadModel: IClientFeatureToggleDeltaReadModel,
segmentReadModel: ISegmentReadModel, segmentReadModel: ISegmentReadModel,
eventStore: IEventStore, eventStore: IEventStore,
configurationRevisionService: ConfigurationRevisionService, configurationRevisionService: ConfigurationRevisionService,
flagResolver: IFlagResolver, flagResolver: IFlagResolver,
config: IUnleashConfig,
) { ) {
this.eventStore = eventStore; this.eventStore = eventStore;
this.configurationRevisionService = configurationRevisionService; this.configurationRevisionService = configurationRevisionService;
@ -126,6 +132,8 @@ export class ClientFeatureToggleDelta {
clientFeatureToggleDeltaReadModel; clientFeatureToggleDeltaReadModel;
this.flagResolver = flagResolver; this.flagResolver = flagResolver;
this.segmentReadModel = segmentReadModel; this.segmentReadModel = segmentReadModel;
this.eventBus = config.eventBus;
this.logger = config.getLogger('delta/client-feature-toggle-delta.js');
this.onUpdateRevisionEvent = this.onUpdateRevisionEvent.bind(this); this.onUpdateRevisionEvent = this.onUpdateRevisionEvent.bind(this);
this.delta = {}; this.delta = {};
@ -161,6 +169,8 @@ export class ClientFeatureToggleDelta {
await this.updateSegments(); await this.updateSegments();
} }
// TODO: 19.12 this logic seems to be not logical, when no revisionId is coming, it should not go to db, but take latest from cache
// Should get the latest state if revision does not exist or if sdkRevision is not present // Should get the latest state if revision does not exist or if sdkRevision is not present
// We should be able to do this without going to the database by merging revisions from the delta with // We should be able to do this without going to the database by merging revisions from the delta with
// the base case // the base case
@ -203,12 +213,13 @@ export class ClientFeatureToggleDelta {
private async onUpdateRevisionEvent() { private async onUpdateRevisionEvent() {
if (this.flagResolver.isEnabled('deltaApi')) { if (this.flagResolver.isEnabled('deltaApi')) {
await this.listenToRevisionChange(); await this.updateFeaturesDelta();
await this.updateSegments(); await this.updateSegments();
this.storeFootprint();
} }
} }
public async listenToRevisionChange() { public async updateFeaturesDelta() {
const keys = Object.keys(this.delta); const keys = Object.keys(this.delta);
if (keys.length === 0) return; if (keys.length === 0) return;
@ -248,7 +259,6 @@ export class ClientFeatureToggleDelta {
removed, removed,
}); });
} }
this.currentRevisionId = latestRevision; this.currentRevisionId = latestRevision;
} }
@ -279,8 +289,9 @@ export class ClientFeatureToggleDelta {
removed: [], removed: [],
}, },
]); ]);
this.delta[environment] = delta; this.delta[environment] = delta;
this.storeFootprint();
} }
async getClientFeatures( async getClientFeatures(
@ -294,4 +305,20 @@ export class ClientFeatureToggleDelta {
private async updateSegments(): Promise<void> { private async updateSegments(): Promise<void> {
this.segments = await this.segmentReadModel.getActiveForClient(); this.segments = await this.segmentReadModel.getActiveForClient();
} }
storeFootprint() {
try {
const featuresMemory = this.getCacheSizeInBytes(this.delta);
const segmentsMemory = this.getCacheSizeInBytes(this.segments);
const memory = featuresMemory + segmentsMemory;
this.eventBus.emit(CLIENT_DELTA_MEMORY, { memory });
} catch (e) {
this.logger.error('Client delta footprint error', e);
}
}
getCacheSizeInBytes(value: any): number {
const jsonString = JSON.stringify(value);
return Buffer.byteLength(jsonString, 'utf8');
}
} }

View File

@ -28,6 +28,7 @@ export const createClientFeatureToggleDelta = (
eventStore, eventStore,
configurationRevisionService, configurationRevisionService,
flagResolver, flagResolver,
config,
); );
return clientFeatureToggleDelta; return clientFeatureToggleDelta;

View File

@ -12,7 +12,7 @@ export class RevisionDelta {
private delta: Revision[]; private delta: Revision[];
private maxLength: number; private maxLength: number;
constructor(data: Revision[] = [], maxLength: number = 100) { constructor(data: Revision[] = [], maxLength: number = 20) {
this.delta = data; this.delta = data;
this.maxLength = maxLength; this.maxLength = maxLength;
} }

View File

@ -16,6 +16,8 @@ const REQUEST_ORIGIN = 'request_origin' as const;
const ADDON_EVENTS_HANDLED = 'addon-event-handled' as const; const ADDON_EVENTS_HANDLED = 'addon-event-handled' as const;
const CLIENT_METRICS_NAMEPREFIX = 'client-api-nameprefix'; const CLIENT_METRICS_NAMEPREFIX = 'client-api-nameprefix';
const CLIENT_METRICS_TAGS = 'client-api-tags'; const CLIENT_METRICS_TAGS = 'client-api-tags';
const CLIENT_FEATURES_MEMORY = 'client_features_memory';
const CLIENT_DELTA_MEMORY = 'client_delta_memory';
type MetricEvent = type MetricEvent =
| typeof REQUEST_TIME | typeof REQUEST_TIME
@ -32,7 +34,9 @@ type MetricEvent =
| typeof EXCEEDS_LIMIT | typeof EXCEEDS_LIMIT
| typeof REQUEST_ORIGIN | typeof REQUEST_ORIGIN
| typeof CLIENT_METRICS_NAMEPREFIX | typeof CLIENT_METRICS_NAMEPREFIX
| typeof CLIENT_METRICS_TAGS; | typeof CLIENT_METRICS_TAGS
| typeof CLIENT_FEATURES_MEMORY
| typeof CLIENT_DELTA_MEMORY;
type RequestOriginEventPayload = { type RequestOriginEventPayload = {
type: 'UI' | 'API'; type: 'UI' | 'API';
@ -82,6 +86,8 @@ export {
ADDON_EVENTS_HANDLED, ADDON_EVENTS_HANDLED,
CLIENT_METRICS_NAMEPREFIX, CLIENT_METRICS_NAMEPREFIX,
CLIENT_METRICS_TAGS, CLIENT_METRICS_TAGS,
CLIENT_FEATURES_MEMORY,
CLIENT_DELTA_MEMORY,
type MetricEvent, type MetricEvent,
type MetricEventPayload, type MetricEventPayload,
emitMetricEvent, emitMetricEvent,

View File

@ -624,6 +624,16 @@ export function registerPrometheusMetrics(
help: 'Number of API tokens without a project', help: 'Number of API tokens without a project',
}); });
const clientFeaturesMemory = createGauge({
name: 'client_features_memory',
help: 'The amount of memory client features endpoint is using for caching',
});
const clientDeltaMemory = createGauge({
name: 'client_delta_memory',
help: 'The amount of memory client features delta endpoint is using for caching',
});
const orphanedTokensActive = createGauge({ const orphanedTokensActive = createGauge({
name: 'orphaned_api_tokens_active', name: 'orphaned_api_tokens_active',
help: 'Number of API tokens without a project, last seen within 3 months', help: 'Number of API tokens without a project, last seen within 3 months',
@ -752,6 +762,16 @@ export function registerPrometheusMetrics(
tagsUsed.inc(); tagsUsed.inc();
}); });
eventBus.on(events.CLIENT_FEATURES_MEMORY, (event: { memory: number }) => {
clientFeaturesMemory.reset();
clientFeaturesMemory.set(event.memory);
});
eventBus.on(events.CLIENT_DELTA_MEMORY, (event: { memory: number }) => {
clientDeltaMemory.reset();
clientDeltaMemory.set(event.memory);
});
events.onMetricEvent( events.onMetricEvent(
eventBus, eventBus,
events.REQUEST_ORIGIN, events.REQUEST_ORIGIN,