cds-caching 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/db/statistics.cds +56 -0
- package/index.cds +5 -0
- package/lib/CachingService.d.ts +29 -0
- package/lib/CachingService.js +49 -0
- package/lib/config-normalizer.js +2 -0
- package/lib/operations/AsyncOperations.js +52 -24
- package/lib/operations/BasicOperations.js +6 -3
- package/lib/operations/CapOperations.js +59 -27
- package/lib/support/CacheStatisticsHandler.js +258 -13
- package/lib/support/RuntimeConfigurationManager.js +21 -1
- package/lib/support/StatisticsPersistenceManager.js +137 -4
- package/package.json +1 -1
- package/srv/caching-api-service.js +25 -1
|
@@ -26,15 +26,17 @@ class CacheStatisticsHandler {
|
|
|
26
26
|
persistenceInterval: 10 * 1000, // 10 seconds
|
|
27
27
|
maxLatencies: 2000,
|
|
28
28
|
maxKeyMetrics: 1000, // Track top accessed keys
|
|
29
|
+
maxTagMetrics: 1000, // Track top accessed tags
|
|
29
30
|
maxMetricFieldLength: MAX_FIELD_LENGTH, // Cap per free-text KeyMetrics field
|
|
30
31
|
keyMetricsEnabled: false,
|
|
32
|
+
tagMetricsEnabled: false,
|
|
31
33
|
metricsEnabled: false, // Main metrics enabled flag (process default)
|
|
32
34
|
...options
|
|
33
35
|
};
|
|
34
36
|
|
|
35
37
|
/** @type {Map<string, object>} */
|
|
36
38
|
this._buckets = new Map();
|
|
37
|
-
/** @type {Map<string, { metricsEnabled?: boolean, keyMetricsEnabled?: boolean }>} */
|
|
39
|
+
/** @type {Map<string, { metricsEnabled?: boolean, keyMetricsEnabled?: boolean, tagMetricsEnabled?: boolean }>} */
|
|
38
40
|
this._overlays = new Map();
|
|
39
41
|
/** Throwaway bucket for MTX writes outside a tenant context (discarded). */
|
|
40
42
|
this._discardBucket = null;
|
|
@@ -71,6 +73,7 @@ class CacheStatisticsHandler {
|
|
|
71
73
|
nativeErrors: 0,
|
|
72
74
|
totalNativeOperations: 0,
|
|
73
75
|
keyAccess: new Map(),
|
|
76
|
+
tagAccess: new Map(),
|
|
74
77
|
startTime: Date.now(),
|
|
75
78
|
lastReset: Date.now()
|
|
76
79
|
};
|
|
@@ -134,13 +137,22 @@ class CacheStatisticsHandler {
|
|
|
134
137
|
return this.options.keyMetricsEnabled === true;
|
|
135
138
|
}
|
|
136
139
|
|
|
140
|
+
_tagMetricsOn(tenantId) {
|
|
141
|
+
const key = tenantId ?? this._bucketKey();
|
|
142
|
+
if (key && this._overlays.has(key)) {
|
|
143
|
+
const overlay = this._overlays.get(key);
|
|
144
|
+
if (overlay.tagMetricsEnabled !== undefined) return overlay.tagMetricsEnabled;
|
|
145
|
+
}
|
|
146
|
+
return this.options.tagMetricsEnabled === true;
|
|
147
|
+
}
|
|
148
|
+
|
|
137
149
|
_isBucketEmpty(bucket) {
|
|
138
150
|
const c = bucket?.current;
|
|
139
151
|
if (!c) return true;
|
|
140
152
|
return c.hits === 0 && c.misses === 0 && c.errors === 0
|
|
141
153
|
&& c.nativeSets === 0 && c.nativeGets === 0 && c.nativeDeletes === 0
|
|
142
154
|
&& c.nativeClears === 0 && c.nativeDeleteByTags === 0 && c.nativeErrors === 0
|
|
143
|
-
&& c.keyAccess.size === 0;
|
|
155
|
+
&& c.keyAccess.size === 0 && c.tagAccess.size === 0;
|
|
144
156
|
}
|
|
145
157
|
|
|
146
158
|
/**
|
|
@@ -153,7 +165,7 @@ class CacheStatisticsHandler {
|
|
|
153
165
|
try {
|
|
154
166
|
if (isMultitenantMode()) {
|
|
155
167
|
await this.persistAllTenantMetrics();
|
|
156
|
-
} else if (this._metricsOn(DEFAULT_TENANT) || this._keyMetricsOn(DEFAULT_TENANT)) {
|
|
168
|
+
} else if (this._metricsOn(DEFAULT_TENANT) || this._keyMetricsOn(DEFAULT_TENANT) || this._tagMetricsOn(DEFAULT_TENANT)) {
|
|
157
169
|
await this.persistMetrics();
|
|
158
170
|
}
|
|
159
171
|
} catch (error) {
|
|
@@ -184,6 +196,17 @@ class CacheStatisticsHandler {
|
|
|
184
196
|
}
|
|
185
197
|
}
|
|
186
198
|
|
|
199
|
+
enableTagMetrics(enabled) {
|
|
200
|
+
if (isMultitenantMode() && hasTenantContext()) {
|
|
201
|
+
const t = currentTenant();
|
|
202
|
+
const overlay = this._overlays.get(t) || {};
|
|
203
|
+
overlay.tagMetricsEnabled = enabled;
|
|
204
|
+
this._overlays.set(t, overlay);
|
|
205
|
+
} else {
|
|
206
|
+
this.options.tagMetricsEnabled = enabled;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
187
210
|
enableMetrics(enabled) {
|
|
188
211
|
if (isMultitenantMode() && hasTenantContext()) {
|
|
189
212
|
const t = currentTenant();
|
|
@@ -218,11 +241,16 @@ class CacheStatisticsHandler {
|
|
|
218
241
|
this.recordKeyAccess(key, 'hit', { ...metadata, latency });
|
|
219
242
|
}
|
|
220
243
|
|
|
244
|
+
// Record tag access if tag metrics is enabled (independent of main metrics)
|
|
245
|
+
if (this._tagMetricsOn()) {
|
|
246
|
+
this._recordTagsAccess(metadata.tags, 'hit', { ...metadata, latency });
|
|
247
|
+
}
|
|
248
|
+
|
|
221
249
|
// Emit OTel metric (independent of metricsEnabled)
|
|
222
250
|
telemetry.recordHit(latency, { 'cache.name': this.options.cache });
|
|
223
251
|
|
|
224
252
|
// Log for debugging
|
|
225
|
-
this.log.debug(`Recorded HIT for key: ${key}, latency: ${latency}ms, enabled: ${this._metricsOn()}, keyMetrics: ${this._keyMetricsOn()}`);
|
|
253
|
+
this.log.debug(`Recorded HIT for key: ${key}, latency: ${latency}ms, enabled: ${this._metricsOn()}, keyMetrics: ${this._keyMetricsOn()}, tagMetrics: ${this._tagMetricsOn()}`);
|
|
226
254
|
}
|
|
227
255
|
|
|
228
256
|
/**
|
|
@@ -248,11 +276,16 @@ class CacheStatisticsHandler {
|
|
|
248
276
|
this.recordKeyAccess(key, 'miss', { ...metadata, latency });
|
|
249
277
|
}
|
|
250
278
|
|
|
279
|
+
// Record tag access if tag metrics is enabled (independent of main metrics)
|
|
280
|
+
if (this._tagMetricsOn()) {
|
|
281
|
+
this._recordTagsAccess(metadata.tags, 'miss', { ...metadata, latency });
|
|
282
|
+
}
|
|
283
|
+
|
|
251
284
|
// Emit OTel metric (independent of metricsEnabled)
|
|
252
285
|
telemetry.recordMiss(latency, { 'cache.name': this.options.cache });
|
|
253
286
|
|
|
254
287
|
// Log for debugging
|
|
255
|
-
this.log.debug(`Recorded MISS for key: ${key}, latency: ${latency}ms, enabled: ${this._metricsOn()}, keyMetrics: ${this._keyMetricsOn()}`);
|
|
288
|
+
this.log.debug(`Recorded MISS for key: ${key}, latency: ${latency}ms, enabled: ${this._metricsOn()}, keyMetrics: ${this._keyMetricsOn()}, tagMetrics: ${this._tagMetricsOn()}`);
|
|
256
289
|
}
|
|
257
290
|
|
|
258
291
|
/**
|
|
@@ -279,11 +312,15 @@ class CacheStatisticsHandler {
|
|
|
279
312
|
this.recordKeyAccess(key, 'set', { ...metadata, latency });
|
|
280
313
|
}
|
|
281
314
|
|
|
315
|
+
if (this._tagMetricsOn()) {
|
|
316
|
+
this._recordTagsAccess(metadata.tags, 'set', { ...metadata, latency });
|
|
317
|
+
}
|
|
318
|
+
|
|
282
319
|
// Emit OTel metric (independent of metricsEnabled)
|
|
283
320
|
telemetry.recordSet({ 'cache.name': this.options.cache });
|
|
284
321
|
|
|
285
322
|
// Log for debugging
|
|
286
|
-
this.log.debug(`Recorded SET for key: ${key}, latency: ${latency}ms, enabled: ${this._metricsOn()}, keyMetrics: ${this._keyMetricsOn()}`);
|
|
323
|
+
this.log.debug(`Recorded SET for key: ${key}, latency: ${latency}ms, enabled: ${this._metricsOn()}, keyMetrics: ${this._keyMetricsOn()}, tagMetrics: ${this._tagMetricsOn()}`);
|
|
287
324
|
}
|
|
288
325
|
|
|
289
326
|
/**
|
|
@@ -310,11 +347,15 @@ class CacheStatisticsHandler {
|
|
|
310
347
|
this.recordKeyAccess(key, 'delete', { ...metadata, latency });
|
|
311
348
|
}
|
|
312
349
|
|
|
350
|
+
if (this._tagMetricsOn()) {
|
|
351
|
+
this._recordTagsAccess(metadata.tags, 'delete', { ...metadata, latency });
|
|
352
|
+
}
|
|
353
|
+
|
|
313
354
|
// Emit OTel metric (independent of metricsEnabled)
|
|
314
355
|
telemetry.recordDelete({ 'cache.name': this.options.cache });
|
|
315
356
|
|
|
316
357
|
// Log for debugging
|
|
317
|
-
this.log.debug(`Recorded DELETE for key: ${key}, latency: ${latency}ms, enabled: ${this._metricsOn()}, keyMetrics: ${this._keyMetricsOn()}`);
|
|
358
|
+
this.log.debug(`Recorded DELETE for key: ${key}, latency: ${latency}ms, enabled: ${this._metricsOn()}, keyMetrics: ${this._keyMetricsOn()}, tagMetrics: ${this._tagMetricsOn()}`);
|
|
318
359
|
}
|
|
319
360
|
|
|
320
361
|
/**
|
|
@@ -341,11 +382,15 @@ class CacheStatisticsHandler {
|
|
|
341
382
|
this.recordKeyAccess(key, 'nativeSet', metadata);
|
|
342
383
|
}
|
|
343
384
|
|
|
385
|
+
if (this._tagMetricsOn()) {
|
|
386
|
+
this._recordTagsAccess(metadata.tags, 'nativeSet', metadata);
|
|
387
|
+
}
|
|
388
|
+
|
|
344
389
|
// Emit OTel metric (independent of metricsEnabled)
|
|
345
390
|
telemetry.recordSet({ 'cache.name': this.options.cache });
|
|
346
391
|
|
|
347
392
|
// Log for debugging
|
|
348
|
-
this.log.debug(`Recorded NATIVE SET for key: ${key}, enabled: ${this._metricsOn()}, keyMetrics: ${this._keyMetricsOn()}`);
|
|
393
|
+
this.log.debug(`Recorded NATIVE SET for key: ${key}, enabled: ${this._metricsOn()}, keyMetrics: ${this._keyMetricsOn()}, tagMetrics: ${this._tagMetricsOn()}`);
|
|
349
394
|
}
|
|
350
395
|
|
|
351
396
|
/**
|
|
@@ -372,8 +417,12 @@ class CacheStatisticsHandler {
|
|
|
372
417
|
this.recordKeyAccess(key, hit ? 'nativeHit' : 'nativeMiss', metadata);
|
|
373
418
|
}
|
|
374
419
|
|
|
420
|
+
if (this._tagMetricsOn()) {
|
|
421
|
+
this._recordTagsAccess(metadata.tags, hit ? 'nativeHit' : 'nativeMiss', metadata);
|
|
422
|
+
}
|
|
423
|
+
|
|
375
424
|
// Log for debugging
|
|
376
|
-
this.log.debug(`Recorded NATIVE GET for key: ${key}, hit: ${hit}, enabled: ${this._metricsOn()}, keyMetrics: ${this._keyMetricsOn()}`);
|
|
425
|
+
this.log.debug(`Recorded NATIVE GET for key: ${key}, hit: ${hit}, enabled: ${this._metricsOn()}, keyMetrics: ${this._keyMetricsOn()}, tagMetrics: ${this._tagMetricsOn()}`);
|
|
377
426
|
}
|
|
378
427
|
|
|
379
428
|
/**
|
|
@@ -400,11 +449,15 @@ class CacheStatisticsHandler {
|
|
|
400
449
|
this.recordKeyAccess(key, 'nativeDelete', metadata);
|
|
401
450
|
}
|
|
402
451
|
|
|
452
|
+
if (this._tagMetricsOn()) {
|
|
453
|
+
this._recordTagsAccess(metadata.tags, 'nativeDelete', metadata);
|
|
454
|
+
}
|
|
455
|
+
|
|
403
456
|
// Emit OTel metric (independent of metricsEnabled)
|
|
404
457
|
telemetry.recordDelete({ 'cache.name': this.options.cache });
|
|
405
458
|
|
|
406
459
|
// Log for debugging
|
|
407
|
-
this.log.debug(`Recorded NATIVE DELETE for key: ${key}, enabled: ${this._metricsOn()}, keyMetrics: ${this._keyMetricsOn()}`);
|
|
460
|
+
this.log.debug(`Recorded NATIVE DELETE for key: ${key}, enabled: ${this._metricsOn()}, keyMetrics: ${this._keyMetricsOn()}, tagMetrics: ${this._tagMetricsOn()}`);
|
|
408
461
|
}
|
|
409
462
|
|
|
410
463
|
/**
|
|
@@ -748,6 +801,157 @@ class CacheStatisticsHandler {
|
|
|
748
801
|
}
|
|
749
802
|
}
|
|
750
803
|
|
|
804
|
+
/**
|
|
805
|
+
* Normalize tags from metadata into a deduped list of resolved tag strings.
|
|
806
|
+
* @param {string[]|undefined} tags
|
|
807
|
+
* @returns {string[]}
|
|
808
|
+
* @private
|
|
809
|
+
*/
|
|
810
|
+
_normalizeTags(tags) {
|
|
811
|
+
if (!Array.isArray(tags) || tags.length === 0) return [];
|
|
812
|
+
const seen = new Set();
|
|
813
|
+
const out = [];
|
|
814
|
+
for (const tag of tags) {
|
|
815
|
+
if (typeof tag !== 'string' || !tag) continue;
|
|
816
|
+
if (seen.has(tag)) continue;
|
|
817
|
+
seen.add(tag);
|
|
818
|
+
out.push(tag);
|
|
819
|
+
}
|
|
820
|
+
return out;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Record access for every tag on a metadata object.
|
|
825
|
+
* @param {string[]|undefined} tags
|
|
826
|
+
* @param {string} operation
|
|
827
|
+
* @param {object} metadata
|
|
828
|
+
* @private
|
|
829
|
+
*/
|
|
830
|
+
_recordTagsAccess(tags, operation, metadata = {}) {
|
|
831
|
+
for (const tag of this._normalizeTags(tags)) {
|
|
832
|
+
this.recordTagAccess(tag, operation, metadata);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Record tag access with latency tracking.
|
|
838
|
+
* A hit on an entry with N tags increments N tag rows — tag totals may exceed cache totals.
|
|
839
|
+
* @param {string} tag - resolved tag string
|
|
840
|
+
* @param {string} operation - hit|miss|set|delete|nativeHit|nativeMiss|nativeSet|nativeDelete
|
|
841
|
+
* @param {object} metadata
|
|
842
|
+
*/
|
|
843
|
+
recordTagAccess(tag, operation, metadata = {}) {
|
|
844
|
+
if (!tag) {
|
|
845
|
+
this.log.warn('recordTagAccess called with null/undefined tag');
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (!this.stats.current.tagAccess.has(tag)) {
|
|
850
|
+
this.stats.current.tagAccess.set(tag, {
|
|
851
|
+
tag,
|
|
852
|
+
hits: 0,
|
|
853
|
+
misses: 0,
|
|
854
|
+
sets: 0,
|
|
855
|
+
deletes: 0,
|
|
856
|
+
errors: 0,
|
|
857
|
+
totalRequests: 0,
|
|
858
|
+
hitRatio: 0,
|
|
859
|
+
cacheEfficiency: 0,
|
|
860
|
+
hitLatencies: [],
|
|
861
|
+
missLatencies: [],
|
|
862
|
+
setLatencies: [],
|
|
863
|
+
deleteLatencies: [],
|
|
864
|
+
avgHitLatency: 0,
|
|
865
|
+
avgMissLatency: 0,
|
|
866
|
+
avgReadThroughLatency: 0,
|
|
867
|
+
minHitLatency: Infinity,
|
|
868
|
+
maxHitLatency: 0,
|
|
869
|
+
minMissLatency: Infinity,
|
|
870
|
+
maxMissLatency: 0,
|
|
871
|
+
throughput: 0,
|
|
872
|
+
errorRate: 0,
|
|
873
|
+
nativeHits: 0,
|
|
874
|
+
nativeMisses: 0,
|
|
875
|
+
nativeSets: 0,
|
|
876
|
+
nativeDeletes: 0,
|
|
877
|
+
nativeErrors: 0,
|
|
878
|
+
totalNativeOperations: 0,
|
|
879
|
+
nativeThroughput: 0,
|
|
880
|
+
nativeErrorRate: 0,
|
|
881
|
+
lastAccess: Date.now(),
|
|
882
|
+
timestamp: Date.now(),
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const tagStats = this.stats.current.tagAccess.get(tag);
|
|
887
|
+
tagStats.lastAccess = Date.now();
|
|
888
|
+
|
|
889
|
+
switch (operation) {
|
|
890
|
+
case 'hit':
|
|
891
|
+
case 'miss':
|
|
892
|
+
case 'set':
|
|
893
|
+
case 'delete': {
|
|
894
|
+
const pluralMap = { hit: 'hits', miss: 'misses', set: 'sets', delete: 'deletes' };
|
|
895
|
+
const prop = pluralMap[operation];
|
|
896
|
+
if (prop) tagStats[prop]++;
|
|
897
|
+
tagStats.totalRequests = tagStats.hits + tagStats.misses;
|
|
898
|
+
if (metadata.latency !== undefined) {
|
|
899
|
+
this.recordKeyLatency(tagStats, operation, metadata.latency);
|
|
900
|
+
}
|
|
901
|
+
break;
|
|
902
|
+
}
|
|
903
|
+
case 'nativeHit':
|
|
904
|
+
case 'nativeMiss':
|
|
905
|
+
case 'nativeSet':
|
|
906
|
+
case 'nativeDelete': {
|
|
907
|
+
const nativeProperty = {
|
|
908
|
+
nativeHit: 'nativeHits',
|
|
909
|
+
nativeMiss: 'nativeMisses',
|
|
910
|
+
nativeSet: 'nativeSets',
|
|
911
|
+
nativeDelete: 'nativeDeletes',
|
|
912
|
+
}[operation];
|
|
913
|
+
if (!nativeProperty) return;
|
|
914
|
+
tagStats[nativeProperty]++;
|
|
915
|
+
tagStats.totalNativeOperations = tagStats.nativeHits + tagStats.nativeMisses + tagStats.nativeSets + tagStats.nativeDeletes;
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
default:
|
|
919
|
+
this.log.warn(`Unknown tag operation type: ${operation}`);
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const totalReadThrough = tagStats.hits + tagStats.misses;
|
|
924
|
+
if (totalReadThrough > 0) {
|
|
925
|
+
tagStats.hitRatio = (tagStats.hits / totalReadThrough) * 100;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const uptimeMs = Date.now() - tagStats.timestamp;
|
|
929
|
+
if (uptimeMs > 0) {
|
|
930
|
+
tagStats.throughput = (tagStats.totalRequests / uptimeMs) * 1000;
|
|
931
|
+
tagStats.nativeThroughput = (tagStats.totalNativeOperations / uptimeMs) * 1000;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
if (tagStats.totalRequests > 0) {
|
|
935
|
+
tagStats.errorRate = (tagStats.errors / tagStats.totalRequests) * 100;
|
|
936
|
+
}
|
|
937
|
+
if (tagStats.totalNativeOperations > 0) {
|
|
938
|
+
tagStats.nativeErrorRate = (tagStats.nativeErrors / tagStats.totalNativeOperations) * 100;
|
|
939
|
+
}
|
|
940
|
+
if (tagStats.avgHitLatency > 0 && tagStats.avgMissLatency > 0) {
|
|
941
|
+
tagStats.cacheEfficiency = tagStats.avgMissLatency / tagStats.avgHitLatency;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if (this.stats.current.tagAccess.size > this.options.maxTagMetrics) {
|
|
945
|
+
const entries = Array.from(this.stats.current.tagAccess.entries());
|
|
946
|
+
entries.sort((a, b) => {
|
|
947
|
+
const totalA = a[1].totalRequests + a[1].totalNativeOperations;
|
|
948
|
+
const totalB = b[1].totalRequests + b[1].totalNativeOperations;
|
|
949
|
+
return totalB - totalA;
|
|
950
|
+
});
|
|
951
|
+
this.stats.current.tagAccess = new Map(entries.slice(0, this.options.maxTagMetrics));
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
751
955
|
/**
|
|
752
956
|
* Record latency for a specific key operation
|
|
753
957
|
* @param {object} keyStats - the key statistics object
|
|
@@ -999,7 +1203,7 @@ class CacheStatisticsHandler {
|
|
|
999
1203
|
*/
|
|
1000
1204
|
async persistMetrics() {
|
|
1001
1205
|
// Only persist if at least one type of metrics is enabled
|
|
1002
|
-
if (!this._metricsOn() && !this._keyMetricsOn()) {
|
|
1206
|
+
if (!this._metricsOn() && !this._keyMetricsOn() && !this._tagMetricsOn()) {
|
|
1003
1207
|
this.log.debug(`PersistStats called but all statistics are disabled for cache ${this.options.cache}`);
|
|
1004
1208
|
return;
|
|
1005
1209
|
}
|
|
@@ -1026,6 +1230,7 @@ class CacheStatisticsHandler {
|
|
|
1026
1230
|
try {
|
|
1027
1231
|
await this.persistenceManager.persistHourlyStats(stats, hourlyId, hourlyTimestamp, this._metricsOn());
|
|
1028
1232
|
await this.persistenceManager.persistKeyMetrics(this.stats.current.keyAccess, this._keyMetricsOn());
|
|
1233
|
+
await this.persistenceManager.persistTagMetrics(this.stats.current.tagAccess, this._tagMetricsOn());
|
|
1029
1234
|
|
|
1030
1235
|
this.resetCurrentStats(tenantKey);
|
|
1031
1236
|
|
|
@@ -1157,6 +1362,24 @@ class CacheStatisticsHandler {
|
|
|
1157
1362
|
return this.stats.current.keyAccess;
|
|
1158
1363
|
}
|
|
1159
1364
|
|
|
1365
|
+
async getTagMetrics(tag, from, to) {
|
|
1366
|
+
if (!this._tagMetricsOn() || !isPluginModelAvailable()) return null;
|
|
1367
|
+
const query = SELECT.from("plugin_cds_caching_TagMetrics")
|
|
1368
|
+
.where({ cache: this.options.cache, tag });
|
|
1369
|
+
|
|
1370
|
+
if (from) query.and({ timestamp: { '>=': from.toISOString() } });
|
|
1371
|
+
if (to) query.and({ timestamp: { '<=': to.toISOString() } });
|
|
1372
|
+
|
|
1373
|
+
query.orderBy({ timestamp: 'desc' });
|
|
1374
|
+
|
|
1375
|
+
return await query;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
async getCurrentTagMetrics() {
|
|
1379
|
+
if (!this._tagMetricsOn()) return null;
|
|
1380
|
+
return this.stats.current.tagAccess;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1160
1383
|
async getCurrentStats() {
|
|
1161
1384
|
if (!this._metricsOn()) return null;
|
|
1162
1385
|
|
|
@@ -1259,10 +1482,10 @@ class CacheStatisticsHandler {
|
|
|
1259
1482
|
* Manually trigger persistence (for testing and debugging)
|
|
1260
1483
|
*/
|
|
1261
1484
|
async triggerPersistence() {
|
|
1262
|
-
this.log.info(`Manually triggering persistence for cache ${this.options.cache}, enabled: ${this._metricsOn()}, keyMetrics: ${this._keyMetricsOn()}`);
|
|
1485
|
+
this.log.info(`Manually triggering persistence for cache ${this.options.cache}, enabled: ${this._metricsOn()}, keyMetrics: ${this._keyMetricsOn()}, tagMetrics: ${this._tagMetricsOn()}`);
|
|
1263
1486
|
if (isMultitenantMode()) {
|
|
1264
1487
|
await this.persistAllTenantMetrics();
|
|
1265
|
-
} else if (this._metricsOn() || this._keyMetricsOn()) {
|
|
1488
|
+
} else if (this._metricsOn() || this._keyMetricsOn() || this._tagMetricsOn()) {
|
|
1266
1489
|
await this.persistMetrics();
|
|
1267
1490
|
} else {
|
|
1268
1491
|
this.log.info(`Persistence skipped - all statistics are disabled for cache ${this.options.cache}`);
|
|
@@ -1276,6 +1499,7 @@ class CacheStatisticsHandler {
|
|
|
1276
1499
|
return {
|
|
1277
1500
|
metricsEnabled: this._metricsOn(),
|
|
1278
1501
|
keyMetricsEnabled: this._keyMetricsOn(),
|
|
1502
|
+
tagMetricsEnabled: this._tagMetricsOn(),
|
|
1279
1503
|
intervalExists: this.persistInterval !== null,
|
|
1280
1504
|
lastPersisted: this.stats.lastPersisted,
|
|
1281
1505
|
persistenceInterval: this.options.persistenceInterval,
|
|
@@ -1297,6 +1521,19 @@ class CacheStatisticsHandler {
|
|
|
1297
1521
|
this.resetCurrentStats();
|
|
1298
1522
|
}
|
|
1299
1523
|
|
|
1524
|
+
async clearTagMetrics() {
|
|
1525
|
+
if (isPluginModelAvailable()) {
|
|
1526
|
+
await this.persistenceManager.deleteTagMetrics();
|
|
1527
|
+
}
|
|
1528
|
+
// Clear only in-memory tag buckets; leave key/cache counters intact
|
|
1529
|
+
for (const bucket of this._buckets.values()) {
|
|
1530
|
+
bucket.current.tagAccess = new Map();
|
|
1531
|
+
}
|
|
1532
|
+
if (this._discardBucket) {
|
|
1533
|
+
this._discardBucket.current.tagAccess = new Map();
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1300
1537
|
/**
|
|
1301
1538
|
* Delete metrics (alias for clearMetrics for compatibility)
|
|
1302
1539
|
* @returns {Promise<void>}
|
|
@@ -1312,6 +1549,14 @@ class CacheStatisticsHandler {
|
|
|
1312
1549
|
async deleteKeyMetrics() {
|
|
1313
1550
|
return this.clearKeyMetrics();
|
|
1314
1551
|
}
|
|
1552
|
+
|
|
1553
|
+
/**
|
|
1554
|
+
* Delete tag metrics (alias for clearTagMetrics for compatibility)
|
|
1555
|
+
* @returns {Promise<void>}
|
|
1556
|
+
*/
|
|
1557
|
+
async deleteTagMetrics() {
|
|
1558
|
+
return this.clearTagMetrics();
|
|
1559
|
+
}
|
|
1315
1560
|
}
|
|
1316
1561
|
|
|
1317
1562
|
module.exports = CacheStatisticsHandler;
|
|
@@ -115,6 +115,23 @@ class RuntimeConfigurationManager {
|
|
|
115
115
|
this.log.debug(`Key tracking ${enabled ? 'enabled' : 'disabled'} for cache ${this.cacheName}`);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Update tag metrics enabled status in database
|
|
120
|
+
* @param {boolean} enabled - Whether tag metrics should be enabled
|
|
121
|
+
*/
|
|
122
|
+
async setTagMetricsEnabled(enabled) {
|
|
123
|
+
if (!isPluginModelAvailable()) return;
|
|
124
|
+
if (isMultitenantMode() && !hasTenantContext()) {
|
|
125
|
+
this.log.debug(`Skipping setTagMetricsEnabled in MTX mode (no tenant context)`);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const db = await cds.connect.to('db');
|
|
129
|
+
const { Caches } = cds.entities('plugin.cds_caching');
|
|
130
|
+
await db.update(Caches, this.cacheName).with({ tagMetricsEnabled: enabled });
|
|
131
|
+
|
|
132
|
+
this.log.debug(`Tag tracking ${enabled ? 'enabled' : 'disabled'} for cache ${this.cacheName}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
118
135
|
|
|
119
136
|
|
|
120
137
|
/**
|
|
@@ -125,7 +142,7 @@ class RuntimeConfigurationManager {
|
|
|
125
142
|
const keyManagement = this._resolveKeyManagement();
|
|
126
143
|
|
|
127
144
|
if (!isPluginModelAvailable()) {
|
|
128
|
-
return { metricsEnabled: false, keyMetricsEnabled: false, keyManagement, throwOnErrors: this.options.throwOnErrors };
|
|
145
|
+
return { metricsEnabled: false, keyMetricsEnabled: false, tagMetricsEnabled: false, keyManagement, throwOnErrors: this.options.throwOnErrors };
|
|
129
146
|
}
|
|
130
147
|
|
|
131
148
|
// In MTX mode without tenant context, skip DB access — return defaults
|
|
@@ -133,6 +150,7 @@ class RuntimeConfigurationManager {
|
|
|
133
150
|
return {
|
|
134
151
|
metricsEnabled: false,
|
|
135
152
|
keyMetricsEnabled: false,
|
|
153
|
+
tagMetricsEnabled: false,
|
|
136
154
|
keyManagement,
|
|
137
155
|
throwOnErrors: this.options.throwOnErrors
|
|
138
156
|
};
|
|
@@ -146,6 +164,7 @@ class RuntimeConfigurationManager {
|
|
|
146
164
|
return {
|
|
147
165
|
metricsEnabled: cacheConfig?.metricsEnabled === true || cacheConfig?.metricsEnabled === 1 || false,
|
|
148
166
|
keyMetricsEnabled: cacheConfig?.keyMetricsEnabled === true || cacheConfig?.keyMetricsEnabled === 1 || false,
|
|
167
|
+
tagMetricsEnabled: cacheConfig?.tagMetricsEnabled === true || cacheConfig?.tagMetricsEnabled === 1 || false,
|
|
149
168
|
keyManagement,
|
|
150
169
|
throwOnErrors: this.options.throwOnErrors
|
|
151
170
|
};
|
|
@@ -154,6 +173,7 @@ class RuntimeConfigurationManager {
|
|
|
154
173
|
return {
|
|
155
174
|
metricsEnabled: false,
|
|
156
175
|
keyMetricsEnabled: false,
|
|
176
|
+
tagMetricsEnabled: false,
|
|
157
177
|
keyManagement,
|
|
158
178
|
throwOnErrors: this.options.throwOnErrors
|
|
159
179
|
};
|
|
@@ -77,6 +77,31 @@ class StatisticsPersistenceManager {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Persist tag metrics
|
|
82
|
+
* @param {Map} tagAccess - Tag access data
|
|
83
|
+
* @param {boolean} tagMetricsEnabled - Whether tag metrics are enabled
|
|
84
|
+
*/
|
|
85
|
+
async persistTagMetrics(tagAccess, tagMetricsEnabled) {
|
|
86
|
+
if (!tagMetricsEnabled || tagAccess.size === 0 || !isPluginModelAvailable()) return;
|
|
87
|
+
|
|
88
|
+
if (isMultitenantMode() && !hasTenantContext()) {
|
|
89
|
+
this.log.debug(`Skipping tag metrics persistence in MTX mode (no tenant context)`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.log.debug(`Persisting ${tagAccess.size} tag access records for cache ${this.cacheName}`);
|
|
94
|
+
|
|
95
|
+
const { TagMetrics } = cds.entities('plugin.cds_caching');
|
|
96
|
+
for (const [tag, tagStats] of tagAccess) {
|
|
97
|
+
try {
|
|
98
|
+
await this._persistTagMetric(TagMetrics, tag, tagStats);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
this.log.error(`Failed to persist tag metric for tag ${tag}:`, error);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
80
105
|
/**
|
|
81
106
|
* Delete metrics for this cache
|
|
82
107
|
*/
|
|
@@ -107,6 +132,21 @@ class StatisticsPersistenceManager {
|
|
|
107
132
|
this.log.debug(`Deleted key metrics for cache ${this.cacheName}`);
|
|
108
133
|
}
|
|
109
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Delete tag metrics for this cache
|
|
137
|
+
*/
|
|
138
|
+
async deleteTagMetrics() {
|
|
139
|
+
if (!isPluginModelAvailable()) return;
|
|
140
|
+
if (isMultitenantMode() && !hasTenantContext()) {
|
|
141
|
+
this.log.debug(`Skipping deleteTagMetrics in MTX mode (no tenant context)`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const db = await cds.connect.to('db');
|
|
145
|
+
const { TagMetrics } = cds.entities('plugin.cds_caching');
|
|
146
|
+
await db.delete(TagMetrics).where({ cache: this.cacheName });
|
|
147
|
+
this.log.debug(`Deleted tag metrics for cache ${this.cacheName}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
110
150
|
/**
|
|
111
151
|
* Create new hourly stats record
|
|
112
152
|
* @private
|
|
@@ -235,7 +275,7 @@ class StatisticsPersistenceManager {
|
|
|
235
275
|
// Calculate read-through performance metrics
|
|
236
276
|
if (totalRequests > 0) {
|
|
237
277
|
updatedStats.hitRatio = (updatedStats.hits / totalRequests) * 100;
|
|
238
|
-
updatedStats.throughput = totalRequests / (stats.uptimeMs / 1000);
|
|
278
|
+
updatedStats.throughput = stats.uptimeMs > 0 ? totalRequests / (stats.uptimeMs / 1000) : 0;
|
|
239
279
|
updatedStats.errorRate = (updatedStats.errors / totalRequests) * 100;
|
|
240
280
|
}
|
|
241
281
|
|
|
@@ -247,7 +287,7 @@ class StatisticsPersistenceManager {
|
|
|
247
287
|
// Calculate native function performance metrics
|
|
248
288
|
const totalNativeOps = updatedStats.totalNativeOperations;
|
|
249
289
|
if (totalNativeOps > 0) {
|
|
250
|
-
updatedStats.nativeThroughput = totalNativeOps / (stats.uptimeMs / 1000);
|
|
290
|
+
updatedStats.nativeThroughput = stats.uptimeMs > 0 ? totalNativeOps / (stats.uptimeMs / 1000) : 0;
|
|
251
291
|
updatedStats.nativeErrorRate = (updatedStats.nativeErrors / totalNativeOps) * 100;
|
|
252
292
|
}
|
|
253
293
|
|
|
@@ -380,9 +420,9 @@ class StatisticsPersistenceManager {
|
|
|
380
420
|
: 0,
|
|
381
421
|
|
|
382
422
|
// Update percentiles (use max/min of existing and current, with min using mergeMin)
|
|
383
|
-
minHitLatency: mergeMin(Number(existingKey.minHitLatency) || 0, keyStats.minHitLatency),
|
|
423
|
+
minHitLatency: mergeMin(Number(existingKey.minHitLatency) || 0, keyStats.minHitLatency === Infinity ? 0 : keyStats.minHitLatency),
|
|
384
424
|
maxHitLatency: Math.max(Number(existingKey.maxHitLatency) || 0, keyStats.maxHitLatency || 0),
|
|
385
|
-
minMissLatency: mergeMin(Number(existingKey.minMissLatency) || 0, keyStats.minMissLatency),
|
|
425
|
+
minMissLatency: mergeMin(Number(existingKey.minMissLatency) || 0, keyStats.minMissLatency === Infinity ? 0 : keyStats.minMissLatency),
|
|
386
426
|
maxMissLatency: Math.max(Number(existingKey.maxMissLatency) || 0, keyStats.maxMissLatency || 0)
|
|
387
427
|
};
|
|
388
428
|
|
|
@@ -399,6 +439,99 @@ class StatisticsPersistenceManager {
|
|
|
399
439
|
.set(updatedKeyStats)
|
|
400
440
|
.where({ ID: keyId, cache: this.cacheName, keyName: key });
|
|
401
441
|
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Persist a single tag metric
|
|
445
|
+
* @private
|
|
446
|
+
*/
|
|
447
|
+
async _persistTagMetric(TagMetrics, tag, tagStats) {
|
|
448
|
+
const tagId = `tag:${this.cacheName}:${tag}`;
|
|
449
|
+
|
|
450
|
+
const existingTag = await SELECT.one.from(TagMetrics)
|
|
451
|
+
.where({ ID: tagId, cache: this.cacheName, tag });
|
|
452
|
+
|
|
453
|
+
if (!existingTag) {
|
|
454
|
+
await this._createTagMetric(TagMetrics, tag, tagStats, tagId);
|
|
455
|
+
} else {
|
|
456
|
+
await this._updateTagMetric(TagMetrics, tag, tagStats, tagId, existingTag);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Create new tag metric record
|
|
462
|
+
* @private
|
|
463
|
+
*/
|
|
464
|
+
async _createTagMetric(TagMetrics, tag, tagStats, tagId) {
|
|
465
|
+
await INSERT.into(TagMetrics).entries([{
|
|
466
|
+
ID: tagId,
|
|
467
|
+
cache: this.cacheName,
|
|
468
|
+
tag,
|
|
469
|
+
lastAccess: new Date(tagStats.lastAccess).toISOString(),
|
|
470
|
+
period: 'current',
|
|
471
|
+
hits: tagStats.hits,
|
|
472
|
+
misses: tagStats.misses,
|
|
473
|
+
errors: tagStats.errors,
|
|
474
|
+
totalRequests: tagStats.totalRequests,
|
|
475
|
+
hitRatio: tagStats.hitRatio,
|
|
476
|
+
cacheEfficiency: tagStats.cacheEfficiency,
|
|
477
|
+
avgHitLatency: tagStats.avgHitLatency || 0,
|
|
478
|
+
minHitLatency: tagStats.minHitLatency === Infinity ? 0 : tagStats.minHitLatency,
|
|
479
|
+
maxHitLatency: tagStats.maxHitLatency || 0,
|
|
480
|
+
avgMissLatency: tagStats.avgMissLatency || 0,
|
|
481
|
+
minMissLatency: tagStats.minMissLatency === Infinity ? 0 : tagStats.minMissLatency,
|
|
482
|
+
maxMissLatency: tagStats.maxMissLatency || 0,
|
|
483
|
+
avgReadThroughLatency: tagStats.avgReadThroughLatency || 0,
|
|
484
|
+
throughput: tagStats.throughput || 0,
|
|
485
|
+
errorRate: tagStats.errorRate || 0,
|
|
486
|
+
nativeHits: tagStats.nativeHits || 0,
|
|
487
|
+
nativeMisses: tagStats.nativeMisses || 0,
|
|
488
|
+
nativeSets: tagStats.nativeSets || 0,
|
|
489
|
+
nativeDeletes: tagStats.nativeDeletes || 0,
|
|
490
|
+
nativeErrors: tagStats.nativeErrors || 0,
|
|
491
|
+
totalNativeOperations: tagStats.totalNativeOperations || 0,
|
|
492
|
+
nativeThroughput: tagStats.nativeThroughput || 0,
|
|
493
|
+
nativeErrorRate: tagStats.nativeErrorRate || 0,
|
|
494
|
+
timestamp: new Date(tagStats.timestamp).toISOString(),
|
|
495
|
+
}]);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Update existing tag metric record
|
|
500
|
+
* @private
|
|
501
|
+
*/
|
|
502
|
+
async _updateTagMetric(TagMetrics, tag, tagStats, tagId, existingTag) {
|
|
503
|
+
const totalHits = (Number(existingTag.hits) || 0) + tagStats.hits;
|
|
504
|
+
const totalMisses = (Number(existingTag.misses) || 0) + tagStats.misses;
|
|
505
|
+
|
|
506
|
+
const updatedTagStats = {
|
|
507
|
+
hits: totalHits,
|
|
508
|
+
misses: totalMisses,
|
|
509
|
+
errors: (Number(existingTag.errors) || 0) + (tagStats.errors || 0),
|
|
510
|
+
totalRequests: totalHits + totalMisses,
|
|
511
|
+
lastAccess: new Date(tagStats.lastAccess).toISOString(),
|
|
512
|
+
hitRatio: (totalHits + totalMisses) > 0 ? (totalHits / (totalHits + totalMisses)) * 100 : 0,
|
|
513
|
+
nativeHits: (Number(existingTag.nativeHits) || 0) + (tagStats.nativeHits || 0),
|
|
514
|
+
nativeMisses: (Number(existingTag.nativeMisses) || 0) + (tagStats.nativeMisses || 0),
|
|
515
|
+
nativeSets: (Number(existingTag.nativeSets) || 0) + (tagStats.nativeSets || 0),
|
|
516
|
+
nativeDeletes: (Number(existingTag.nativeDeletes) || 0) + (tagStats.nativeDeletes || 0),
|
|
517
|
+
nativeErrors: (Number(existingTag.nativeErrors) || 0) + (tagStats.nativeErrors || 0),
|
|
518
|
+
totalNativeOperations: (Number(existingTag.totalNativeOperations) || 0) + (tagStats.totalNativeOperations || 0),
|
|
519
|
+
avgHitLatency: totalHits > 0
|
|
520
|
+
? (((Number(existingTag.avgHitLatency) || 0) * (Number(existingTag.hits) || 0)) + (tagStats.avgHitLatency * tagStats.hits)) / totalHits
|
|
521
|
+
: 0,
|
|
522
|
+
avgMissLatency: totalMisses > 0
|
|
523
|
+
? (((Number(existingTag.avgMissLatency) || 0) * (Number(existingTag.misses) || 0)) + (tagStats.avgMissLatency * tagStats.misses)) / totalMisses
|
|
524
|
+
: 0,
|
|
525
|
+
minHitLatency: mergeMin(Number(existingTag.minHitLatency) || 0, tagStats.minHitLatency === Infinity ? 0 : tagStats.minHitLatency),
|
|
526
|
+
maxHitLatency: Math.max(Number(existingTag.maxHitLatency) || 0, tagStats.maxHitLatency || 0),
|
|
527
|
+
minMissLatency: mergeMin(Number(existingTag.minMissLatency) || 0, tagStats.minMissLatency === Infinity ? 0 : tagStats.minMissLatency),
|
|
528
|
+
maxMissLatency: Math.max(Number(existingTag.maxMissLatency) || 0, tagStats.maxMissLatency || 0),
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
await UPDATE(TagMetrics)
|
|
532
|
+
.set(updatedTagStats)
|
|
533
|
+
.where({ ID: tagId, cache: this.cacheName, tag });
|
|
534
|
+
}
|
|
402
535
|
}
|
|
403
536
|
|
|
404
537
|
module.exports = StatisticsPersistenceManager;
|