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:
parent
3bed01bb8c
commit
b701fec75d
@ -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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ export const createClientFeatureToggleDelta = (
|
|||||||
eventStore,
|
eventStore,
|
||||||
configurationRevisionService,
|
configurationRevisionService,
|
||||||
flagResolver,
|
flagResolver,
|
||||||
|
config,
|
||||||
);
|
);
|
||||||
|
|
||||||
return clientFeatureToggleDelta;
|
return clientFeatureToggleDelta;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user