cds-caching 0.3.3 → 1.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 +691 -278
- package/cds-plugin.js +2 -2
- package/index.cds +165 -20
- package/lib/CachingService.d.ts +328 -0
- package/lib/CachingService.js +373 -0
- package/lib/operations/AsyncOperations.js +335 -0
- package/lib/operations/BasicOperations.js +211 -0
- package/lib/operations/CapOperations.js +540 -0
- package/lib/support/CacheStatisticsHandler.js +1124 -0
- package/lib/support/CacheStoreManager.js +120 -0
- package/lib/support/KeyManager.js +132 -0
- package/lib/support/RuntimeConfigurationManager.js +148 -0
- package/lib/support/StatisticsPersistenceManager.js +375 -0
- package/lib/support/TagResolver.js +125 -0
- package/lib/util.js +201 -0
- package/package.json +18 -13
- package/srv/caching-api-service.js +124 -0
- package/srv/CacheStatisticsHandler.js +0 -213
- package/srv/CachingService.js +0 -587
- package/srv/statistics-service.cds +0 -37
- package/srv/statistics-service.js +0 -72
- package/srv/util.js +0 -101
|
@@ -0,0 +1,1124 @@
|
|
|
1
|
+
const cds = require('@sap/cds');
|
|
2
|
+
const StatisticsPersistenceManager = require('./StatisticsPersistenceManager');
|
|
3
|
+
const { query } = require('@sap/cds/lib/env/defaults');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Class to handle all things caching statistics.
|
|
7
|
+
*/
|
|
8
|
+
class CacheStatisticsHandler {
|
|
9
|
+
|
|
10
|
+
log = cds.log('cds-caching');
|
|
11
|
+
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.options = {
|
|
14
|
+
persistenceInterval: 10 * 1000, // 10 seconds
|
|
15
|
+
maxLatencies: 2000,
|
|
16
|
+
maxKeyMetrics: 1000, // Track top accessed keys
|
|
17
|
+
keyMetricsEnabled: false,
|
|
18
|
+
metricsEnabled: false, // Main metrics enabled flag
|
|
19
|
+
...options
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
this.stats = {
|
|
23
|
+
current: {
|
|
24
|
+
// Read-through metrics (high value with latencies)
|
|
25
|
+
hits: 0,
|
|
26
|
+
misses: 0,
|
|
27
|
+
sets: 0,
|
|
28
|
+
deletes: 0,
|
|
29
|
+
errors: 0,
|
|
30
|
+
totalRequests: 0,
|
|
31
|
+
latencies: [],
|
|
32
|
+
hitLatencies: [], // Separate latency tracking for hits
|
|
33
|
+
missLatencies: [], // Separate latency tracking for misses
|
|
34
|
+
setLatencies: [], // Separate latency tracking for sets
|
|
35
|
+
deleteLatencies: [], // Separate latency tracking for deletes
|
|
36
|
+
|
|
37
|
+
// Native function metrics (basic counts only)
|
|
38
|
+
nativeSets: 0,
|
|
39
|
+
nativeGets: 0,
|
|
40
|
+
nativeDeletes: 0,
|
|
41
|
+
nativeClears: 0,
|
|
42
|
+
nativeDeleteByTags: 0,
|
|
43
|
+
nativeErrors: 0,
|
|
44
|
+
totalNativeOperations: 0,
|
|
45
|
+
|
|
46
|
+
keyAccess: new Map(), // Track key access patterns
|
|
47
|
+
startTime: Date.now(),
|
|
48
|
+
lastReset: Date.now()
|
|
49
|
+
},
|
|
50
|
+
historical: {
|
|
51
|
+
hourly: new Map(),
|
|
52
|
+
daily: new Map()
|
|
53
|
+
},
|
|
54
|
+
lastPersisted: Date.now()
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Initialize persistence manager
|
|
58
|
+
this.persistenceManager = new StatisticsPersistenceManager(this.options.cache, this.log);
|
|
59
|
+
|
|
60
|
+
// Always set up persistence interval, but control execution based on metricsEnabled flag
|
|
61
|
+
this.setupPersistenceInterval();
|
|
62
|
+
|
|
63
|
+
cds.on('shutdown', () => {
|
|
64
|
+
this.cleanupPersistenceInterval();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Set up the persistence interval
|
|
70
|
+
*/
|
|
71
|
+
setupPersistenceInterval() {
|
|
72
|
+
// Clear any existing interval
|
|
73
|
+
this.cleanupPersistenceInterval();
|
|
74
|
+
this.persistInterval = setInterval(async () => {
|
|
75
|
+
// Only persist if at least one type of metrics is enabled
|
|
76
|
+
if (this.options.metricsEnabled || this.options.keyMetricsEnabled) {
|
|
77
|
+
await this.persistMetrics();
|
|
78
|
+
}
|
|
79
|
+
}, this.options.persistenceInterval);
|
|
80
|
+
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Clean up the persistence interval
|
|
85
|
+
*/
|
|
86
|
+
cleanupPersistenceInterval() {
|
|
87
|
+
if (this.persistInterval) {
|
|
88
|
+
clearInterval(this.persistInterval);
|
|
89
|
+
this.persistInterval = null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
enableKeyMetrics(enabled) {
|
|
94
|
+
this.options.keyMetricsEnabled = enabled;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
enableMetrics(enabled) {
|
|
99
|
+
this.options.metricsEnabled = enabled;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Record a hit
|
|
104
|
+
* @param {number} latency - the latency of the hit
|
|
105
|
+
* @param {string} key - the key of the hit
|
|
106
|
+
* @param {object} metadata - the metadata of the hit
|
|
107
|
+
*/
|
|
108
|
+
recordHit(latency, key, metadata = {}) {
|
|
109
|
+
// Record basic metrics if enabled
|
|
110
|
+
if (this.options.metricsEnabled) {
|
|
111
|
+
this.stats.current.hits++;
|
|
112
|
+
this.recordLatency(latency);
|
|
113
|
+
this.recordHitLatency(latency);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Record key access if key metrics is enabled (independent of main metrics)
|
|
117
|
+
if (this.options.keyMetricsEnabled && key) {
|
|
118
|
+
this.recordKeyAccess(key, 'hit', { ...metadata, latency });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Log for debugging
|
|
122
|
+
this.log.debug(`Recorded HIT for key: ${key}, latency: ${latency}ms, enabled: ${this.options.metricsEnabled}, keyMetrics: ${this.options.keyMetricsEnabled}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Record a miss
|
|
127
|
+
* @param {number} latency - the latency of the miss
|
|
128
|
+
* @param {string} key - the key of the miss
|
|
129
|
+
* @param {object} metadata - the metadata of the miss
|
|
130
|
+
*/
|
|
131
|
+
recordMiss(latency, key, metadata = {}) {
|
|
132
|
+
// Record basic metrics if enabled
|
|
133
|
+
if (this.options.metricsEnabled) {
|
|
134
|
+
this.stats.current.misses++;
|
|
135
|
+
this.recordLatency(latency);
|
|
136
|
+
this.recordMissLatency(latency);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Record key access if key metrics is enabled (independent of main metrics)
|
|
140
|
+
if (this.options.keyMetricsEnabled && key) {
|
|
141
|
+
this.recordKeyAccess(key, 'miss', { ...metadata, latency });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Log for debugging
|
|
145
|
+
this.log.debug(`Recorded MISS for key: ${key}, latency: ${latency}ms, enabled: ${this.options.metricsEnabled}, keyMetrics: ${this.options.keyMetricsEnabled}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Record a set
|
|
150
|
+
* @param {number} latency - the latency of the set
|
|
151
|
+
* @param {string} key - the key of the set
|
|
152
|
+
* @param {object} metadata - the metadata of the set
|
|
153
|
+
*/
|
|
154
|
+
recordSet(latency, key, metadata = {}) {
|
|
155
|
+
// Record basic metrics if enabled
|
|
156
|
+
if (this.options.metricsEnabled) {
|
|
157
|
+
this.stats.current.sets++;
|
|
158
|
+
this.recordLatency(latency);
|
|
159
|
+
this.recordSetLatency(latency);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Record key access if key metrics is enabled (independent of main metrics)
|
|
163
|
+
if (this.options.keyMetricsEnabled && key) {
|
|
164
|
+
this.recordKeyAccess(key, 'set', { ...metadata, latency });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Log for debugging
|
|
168
|
+
this.log.debug(`Recorded SET for key: ${key}, latency: ${latency}ms, enabled: ${this.options.metricsEnabled}, keyMetrics: ${this.options.keyMetricsEnabled}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Record a delete
|
|
173
|
+
* @param {number} latency - the latency of the delete
|
|
174
|
+
* @param {string} key - the key of the delete
|
|
175
|
+
* @param {string} metadata - the metadata of the delete
|
|
176
|
+
*/
|
|
177
|
+
recordDelete(latency, key, metadata = {}) {
|
|
178
|
+
// Record basic metrics if enabled
|
|
179
|
+
if (this.options.metricsEnabled) {
|
|
180
|
+
this.stats.current.deletes++;
|
|
181
|
+
this.recordLatency(latency);
|
|
182
|
+
this.recordDeleteLatency(latency);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Record key access if key metrics is enabled (independent of main metrics)
|
|
186
|
+
if (this.options.keyMetricsEnabled && key) {
|
|
187
|
+
this.recordKeyAccess(key, 'delete', { ...metadata, latency });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Log for debugging
|
|
191
|
+
this.log.debug(`Recorded DELETE for key: ${key}, latency: ${latency}ms, enabled: ${this.options.metricsEnabled}, keyMetrics: ${this.options.keyMetricsEnabled}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Record a native set operation (cache-aside)
|
|
196
|
+
* @param {string} key - the key of the set
|
|
197
|
+
* @param {object} metadata - the metadata of the set
|
|
198
|
+
*/
|
|
199
|
+
recordNativeSet(key, metadata = {}) {
|
|
200
|
+
this.log.debug(`recordNativeSet called with key: ${key}, enabled: ${this.options.metricsEnabled}, keyMetrics: ${this.options.keyMetricsEnabled}`);
|
|
201
|
+
|
|
202
|
+
// Record basic metrics if enabled
|
|
203
|
+
if (this.options.metricsEnabled) {
|
|
204
|
+
this.stats.current.nativeSets++;
|
|
205
|
+
this.stats.current.totalNativeOperations++;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Record key access if key metrics is enabled (independent of main metrics)
|
|
209
|
+
if (this.options.keyMetricsEnabled && key) {
|
|
210
|
+
this.recordKeyAccess(key, 'nativeSet', metadata);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Log for debugging
|
|
214
|
+
this.log.debug(`Recorded NATIVE SET for key: ${key}, enabled: ${this.options.metricsEnabled}, keyMetrics: ${this.options.keyMetricsEnabled}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Record a native get operation (cache-aside)
|
|
219
|
+
* @param {string} key - the key of the get
|
|
220
|
+
* @param {boolean} hit - whether it was a hit or miss
|
|
221
|
+
* @param {object} metadata - the metadata of the get
|
|
222
|
+
*/
|
|
223
|
+
recordNativeGet(key, hit, metadata = {}) {
|
|
224
|
+
this.log.debug(`recordNativeGet called with key: ${key}, hit: ${hit}, enabled: ${this.options.metricsEnabled}, keyMetrics: ${this.options.keyMetricsEnabled}`);
|
|
225
|
+
|
|
226
|
+
// Record basic metrics if enabled
|
|
227
|
+
if (this.options.metricsEnabled) {
|
|
228
|
+
this.stats.current.nativeGets++;
|
|
229
|
+
this.stats.current.totalNativeOperations++;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Record key access if key metrics is enabled (independent of main metrics)
|
|
233
|
+
if (this.options.keyMetricsEnabled && key) {
|
|
234
|
+
this.recordKeyAccess(key, hit ? 'nativeHit' : 'nativeMiss', metadata);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Log for debugging
|
|
238
|
+
this.log.debug(`Recorded NATIVE GET for key: ${key}, hit: ${hit}, enabled: ${this.options.metricsEnabled}, keyMetrics: ${this.options.keyMetricsEnabled}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Record a native delete operation (cache-aside)
|
|
243
|
+
* @param {string} key - the key of the delete
|
|
244
|
+
* @param {object} metadata - the metadata of the delete
|
|
245
|
+
*/
|
|
246
|
+
recordNativeDelete(key, metadata = {}) {
|
|
247
|
+
this.log.debug(`recordNativeDelete called with key: ${key}, enabled: ${this.options.metricsEnabled}, keyMetrics: ${this.options.keyMetricsEnabled}`);
|
|
248
|
+
|
|
249
|
+
// Record basic metrics if enabled
|
|
250
|
+
if (this.options.metricsEnabled) {
|
|
251
|
+
this.stats.current.nativeDeletes++;
|
|
252
|
+
this.stats.current.totalNativeOperations++;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Record key access if key metrics is enabled (independent of main metrics)
|
|
256
|
+
if (this.options.keyMetricsEnabled && key) {
|
|
257
|
+
this.recordKeyAccess(key, 'nativeDelete', metadata);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Log for debugging
|
|
261
|
+
this.log.debug(`Recorded NATIVE DELETE for key: ${key}, enabled: ${this.options.metricsEnabled}, keyMetrics: ${this.options.keyMetricsEnabled}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Record a native clear operation (cache-aside)
|
|
266
|
+
* @param {object} metadata - the metadata of the clear
|
|
267
|
+
*/
|
|
268
|
+
recordNativeClear(metadata = {}) {
|
|
269
|
+
// Record basic metrics if enabled
|
|
270
|
+
if (this.options.metricsEnabled) {
|
|
271
|
+
this.stats.current.nativeClears++;
|
|
272
|
+
this.stats.current.totalNativeOperations++;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Log for debugging
|
|
276
|
+
this.log.debug(`Recorded NATIVE CLEAR, enabled: ${this.options.metricsEnabled}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Record a native deleteByTag operation (cache-aside)
|
|
281
|
+
* @param {string} tag - the tag used for deletion
|
|
282
|
+
* @param {object} metadata - the metadata of the deleteByTag
|
|
283
|
+
*/
|
|
284
|
+
recordNativeDeleteByTag(tag, metadata = {}) {
|
|
285
|
+
// Record basic metrics if enabled
|
|
286
|
+
if (this.options.metricsEnabled) {
|
|
287
|
+
this.stats.current.nativeDeleteByTags++;
|
|
288
|
+
this.stats.current.totalNativeOperations++;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Log for debugging
|
|
292
|
+
this.log.debug(`Recorded NATIVE DELETE BY TAG: ${tag}, enabled: ${this.options.metricsEnabled}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Record an error
|
|
297
|
+
* @param {Error} error - the error to record
|
|
298
|
+
*/
|
|
299
|
+
recordError(error) {
|
|
300
|
+
// Record basic metrics if enabled
|
|
301
|
+
if (this.options.metricsEnabled) {
|
|
302
|
+
this.stats.current.errors++;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Record a native error
|
|
308
|
+
* @param {Error} error - the error to record
|
|
309
|
+
*/
|
|
310
|
+
recordNativeError(error) {
|
|
311
|
+
// Record basic metrics if enabled
|
|
312
|
+
if (this.options.metricsEnabled) {
|
|
313
|
+
this.stats.current.nativeErrors++;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Record a latency
|
|
319
|
+
* @param {number} ms - the latency to record
|
|
320
|
+
*/
|
|
321
|
+
recordLatency(ms) {
|
|
322
|
+
// Only record latency if main metrics are enabled
|
|
323
|
+
if (this.options.metricsEnabled) {
|
|
324
|
+
this.stats.current.latencies.push(ms);
|
|
325
|
+
if (this.stats.current.latencies.length > this.options.maxLatencies) {
|
|
326
|
+
this.stats.current.latencies.shift();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Record a hit latency
|
|
333
|
+
* @param {number} ms - the latency to record
|
|
334
|
+
*/
|
|
335
|
+
recordHitLatency(ms) {
|
|
336
|
+
// Only record latency if main metrics are enabled
|
|
337
|
+
if (this.options.metricsEnabled) {
|
|
338
|
+
this.stats.current.hitLatencies.push(ms);
|
|
339
|
+
if (this.stats.current.hitLatencies.length > this.options.maxLatencies) {
|
|
340
|
+
this.stats.current.hitLatencies.shift();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Record a miss latency
|
|
347
|
+
* @param {number} ms - the latency to record
|
|
348
|
+
*/
|
|
349
|
+
recordMissLatency(ms) {
|
|
350
|
+
// Only record latency if main metrics are enabled
|
|
351
|
+
if (this.options.metricsEnabled) {
|
|
352
|
+
this.stats.current.missLatencies.push(ms);
|
|
353
|
+
if (this.stats.current.missLatencies.length > this.options.maxLatencies) {
|
|
354
|
+
this.stats.current.missLatencies.shift();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
recordSetLatency(ms) {
|
|
360
|
+
// Only record latency if main metrics are enabled
|
|
361
|
+
if (this.options.metricsEnabled) {
|
|
362
|
+
this.stats.current.setLatencies.push(ms);
|
|
363
|
+
if (this.stats.current.setLatencies.length > this.options.maxLatencies) {
|
|
364
|
+
this.stats.current.setLatencies.shift();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Record a delete latency
|
|
371
|
+
* @param {number} ms - the latency to record
|
|
372
|
+
*/
|
|
373
|
+
recordDeleteLatency(ms) {
|
|
374
|
+
// Only record latency if main metrics are enabled
|
|
375
|
+
if (this.options.metricsEnabled) {
|
|
376
|
+
this.stats.current.deleteLatencies.push(ms);
|
|
377
|
+
if (this.stats.current.deleteLatencies.length > this.options.maxLatencies) {
|
|
378
|
+
this.stats.current.deleteLatencies.shift();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Record key access with enhanced latency tracking and context
|
|
385
|
+
* @param {string} key - the key to record
|
|
386
|
+
* @param {string} operation - the operation to record
|
|
387
|
+
* @param {object} metadata - the metadata to record
|
|
388
|
+
*/
|
|
389
|
+
recordKeyAccess(key, operation, metadata = {}) {
|
|
390
|
+
this.log.debug(`recordKeyAccess called with key: ${key}, operation: ${operation}, enabled: ${this.options.keyMetricsEnabled}`);
|
|
391
|
+
|
|
392
|
+
if (!key) {
|
|
393
|
+
this.log.warn('recordKeyAccess called with null/undefined key');
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!this.stats.current.keyAccess.has(key)) {
|
|
398
|
+
this.log.debug(`Creating new key stats for: ${key}`);
|
|
399
|
+
this.stats.current.keyAccess.set(key, {
|
|
400
|
+
key: key,
|
|
401
|
+
// Read-through metrics (with latencies)
|
|
402
|
+
hits: 0,
|
|
403
|
+
misses: 0,
|
|
404
|
+
sets: 0,
|
|
405
|
+
deletes: 0,
|
|
406
|
+
errors: 0,
|
|
407
|
+
totalRequests: 0,
|
|
408
|
+
hitRatio: 0,
|
|
409
|
+
cacheEfficiency: 0,
|
|
410
|
+
// Read-through latency tracking
|
|
411
|
+
hitLatencies: [],
|
|
412
|
+
missLatencies: [],
|
|
413
|
+
setLatencies: [],
|
|
414
|
+
deleteLatencies: [],
|
|
415
|
+
avgHitLatency: 0,
|
|
416
|
+
avgMissLatency: 0,
|
|
417
|
+
avgReadThroughLatency: 0,
|
|
418
|
+
minHitLatency: Infinity,
|
|
419
|
+
maxHitLatency: 0,
|
|
420
|
+
minMissLatency: Infinity,
|
|
421
|
+
maxMissLatency: 0,
|
|
422
|
+
// Read-through performance metrics
|
|
423
|
+
throughput: 0,
|
|
424
|
+
errorRate: 0,
|
|
425
|
+
// Native function metrics (counts only)
|
|
426
|
+
nativeHits: 0,
|
|
427
|
+
nativeMisses: 0,
|
|
428
|
+
nativeSets: 0,
|
|
429
|
+
nativeDeletes: 0,
|
|
430
|
+
nativeClears: 0,
|
|
431
|
+
nativeDeleteByTags: 0,
|
|
432
|
+
nativeErrors: 0,
|
|
433
|
+
totalNativeOperations: 0,
|
|
434
|
+
// Native function performance metrics
|
|
435
|
+
nativeThroughput: 0,
|
|
436
|
+
nativeErrorRate: 0,
|
|
437
|
+
// Operation type tracking
|
|
438
|
+
operationType: metadata.operationType || 'UNKNOWN',
|
|
439
|
+
lastAccess: Date.now(),
|
|
440
|
+
timestamp: Date.now(),
|
|
441
|
+
// Enhanced metadata
|
|
442
|
+
dataType: metadata.dataType || 'custom',
|
|
443
|
+
serviceName: metadata.serviceName || '',
|
|
444
|
+
entityName: metadata.entityName || '',
|
|
445
|
+
operation: metadata.operation || '',
|
|
446
|
+
metadata: metadata.metadata || '',
|
|
447
|
+
// Enhanced context information
|
|
448
|
+
context: metadata.context || '',
|
|
449
|
+
query: metadata.query || '',
|
|
450
|
+
subject: metadata.subject || '',
|
|
451
|
+
target: metadata.target || '',
|
|
452
|
+
tenant: metadata.tenant || '',
|
|
453
|
+
user: metadata.user || '',
|
|
454
|
+
locale: metadata.locale || '',
|
|
455
|
+
cacheOptions: metadata.cacheOptions || ''
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const keyStats = this.stats.current.keyAccess.get(key);
|
|
460
|
+
keyStats.lastAccess = Date.now();
|
|
461
|
+
|
|
462
|
+
// Handle different operation types
|
|
463
|
+
switch (operation) {
|
|
464
|
+
case 'hit':
|
|
465
|
+
case 'miss':
|
|
466
|
+
case 'set':
|
|
467
|
+
case 'delete':
|
|
468
|
+
// Read-through operations
|
|
469
|
+
const pluralMap = { hit: 'hits', miss: 'misses', set: 'sets', delete: 'deletes' };
|
|
470
|
+
const prop = pluralMap[operation];
|
|
471
|
+
if (prop) keyStats[prop]++;
|
|
472
|
+
// Recalculate totalRequests to ensure accuracy
|
|
473
|
+
keyStats.totalRequests = keyStats.hits + keyStats.misses;
|
|
474
|
+
|
|
475
|
+
this.log.debug(`Read-through operation ${operation} for key ${key}: ${prop}=${keyStats[prop]}, totalRequests=${keyStats.totalRequests}`);
|
|
476
|
+
|
|
477
|
+
// Record latency for read-through operations
|
|
478
|
+
if (metadata.latency !== undefined) {
|
|
479
|
+
this.recordKeyLatency(keyStats, operation, metadata.latency);
|
|
480
|
+
}
|
|
481
|
+
break;
|
|
482
|
+
|
|
483
|
+
case 'nativeHit':
|
|
484
|
+
case 'nativeMiss':
|
|
485
|
+
case 'nativeSet':
|
|
486
|
+
case 'nativeDelete':
|
|
487
|
+
// Native operations (separate from read-through, no double counting)
|
|
488
|
+
// Use a more robust approach to avoid string manipulation issues
|
|
489
|
+
let nativeProperty;
|
|
490
|
+
switch (operation) {
|
|
491
|
+
case 'nativeHit':
|
|
492
|
+
nativeProperty = 'nativeHits';
|
|
493
|
+
break;
|
|
494
|
+
case 'nativeMiss':
|
|
495
|
+
nativeProperty = 'nativeMisses';
|
|
496
|
+
break;
|
|
497
|
+
case 'nativeSet':
|
|
498
|
+
nativeProperty = 'nativeSets';
|
|
499
|
+
break;
|
|
500
|
+
case 'nativeDelete':
|
|
501
|
+
nativeProperty = 'nativeDeletes';
|
|
502
|
+
break;
|
|
503
|
+
default:
|
|
504
|
+
this.log.warn(`Unknown native operation: ${operation}`);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
keyStats[nativeProperty]++;
|
|
509
|
+
// Recalculate totalNativeOperations to ensure accuracy
|
|
510
|
+
keyStats.totalNativeOperations = keyStats.nativeHits + keyStats.nativeMisses + keyStats.nativeSets + keyStats.nativeDeletes;
|
|
511
|
+
|
|
512
|
+
this.log.debug(`Native operation ${operation} for key ${key}: ${nativeProperty}=${keyStats[nativeProperty]}, totalNativeOperations=${keyStats.totalNativeOperations}`);
|
|
513
|
+
|
|
514
|
+
break;
|
|
515
|
+
|
|
516
|
+
default:
|
|
517
|
+
this.log.warn(`Unknown operation type: ${operation}`);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Calculate hit ratio for read-through operations
|
|
522
|
+
const totalReadThrough = keyStats.hits + keyStats.misses;
|
|
523
|
+
if (totalReadThrough > 0) {
|
|
524
|
+
keyStats.hitRatio = (keyStats.hits / totalReadThrough) * 100;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Calculate performance metrics
|
|
528
|
+
const uptimeMs = Date.now() - keyStats.timestamp;
|
|
529
|
+
if (uptimeMs > 0) {
|
|
530
|
+
keyStats.throughput = (keyStats.totalRequests / uptimeMs) * 1000;
|
|
531
|
+
keyStats.nativeThroughput = (keyStats.totalNativeOperations / uptimeMs) * 1000;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (keyStats.totalRequests > 0) {
|
|
535
|
+
keyStats.errorRate = (keyStats.errors / keyStats.totalRequests) * 100;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (keyStats.totalNativeOperations > 0) {
|
|
539
|
+
keyStats.nativeErrorRate = (keyStats.nativeErrors / keyStats.totalNativeOperations) * 100;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Calculate cache efficiency
|
|
543
|
+
if (keyStats.avgHitLatency > 0 && keyStats.avgMissLatency > 0) {
|
|
544
|
+
keyStats.cacheEfficiency = keyStats.avgMissLatency / keyStats.avgHitLatency;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Update context information if provided
|
|
548
|
+
if (metadata.context) {
|
|
549
|
+
keyStats.context = metadata.context;
|
|
550
|
+
}
|
|
551
|
+
if (metadata.query) {
|
|
552
|
+
keyStats.query = metadata.query;
|
|
553
|
+
}
|
|
554
|
+
if (metadata.subject) {
|
|
555
|
+
keyStats.subject = metadata.subject;
|
|
556
|
+
}
|
|
557
|
+
if (metadata.tenant) {
|
|
558
|
+
keyStats.tenant = metadata.tenant;
|
|
559
|
+
}
|
|
560
|
+
if (metadata.user) {
|
|
561
|
+
keyStats.user = metadata.user;
|
|
562
|
+
}
|
|
563
|
+
if (metadata.locale) {
|
|
564
|
+
keyStats.locale = metadata.locale;
|
|
565
|
+
}
|
|
566
|
+
if (metadata.target) {
|
|
567
|
+
keyStats.target = metadata.target;
|
|
568
|
+
}
|
|
569
|
+
if (metadata.cacheOptions) {
|
|
570
|
+
keyStats.cacheOptions = metadata.cacheOptions;
|
|
571
|
+
}
|
|
572
|
+
// Keep only top accessed keys
|
|
573
|
+
if (this.stats.current.keyAccess.size > this.options.maxKeyMetrics) {
|
|
574
|
+
const entries = Array.from(this.stats.current.keyAccess.entries());
|
|
575
|
+
entries.sort((a, b) => {
|
|
576
|
+
const totalA = a[1].totalRequests + a[1].totalNativeOperations;
|
|
577
|
+
const totalB = b[1].totalRequests + b[1].totalNativeOperations;
|
|
578
|
+
return totalB - totalA;
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
this.stats.current.keyAccess = new Map(entries.slice(0, this.options.maxKeyMetrics));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Record latency for a specific key operation
|
|
587
|
+
* @param {object} keyStats - the key statistics object
|
|
588
|
+
* @param {string} operation - the operation type
|
|
589
|
+
* @param {number} latency - the latency to record
|
|
590
|
+
*/
|
|
591
|
+
recordKeyLatency(keyStats, operation, latency) {
|
|
592
|
+
const latencyArray = keyStats[operation + 'Latencies'];
|
|
593
|
+
|
|
594
|
+
if (latencyArray) {
|
|
595
|
+
latencyArray.push(latency);
|
|
596
|
+
|
|
597
|
+
// Keep only recent latencies (last 100)
|
|
598
|
+
if (latencyArray.length > 100) {
|
|
599
|
+
latencyArray.shift();
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Calculate statistics for this operation
|
|
603
|
+
const sortedLatencies = [...latencyArray].sort((a, b) => a - b);
|
|
604
|
+
const avgKey = `avg${operation.charAt(0).toUpperCase() + operation.slice(1)}Latency`;
|
|
605
|
+
const minKey = `min${operation.charAt(0).toUpperCase() + operation.slice(1)}Latency`;
|
|
606
|
+
const maxKey = `max${operation.charAt(0).toUpperCase() + operation.slice(1)}Latency`;
|
|
607
|
+
|
|
608
|
+
keyStats[avgKey] = latencyArray.reduce((sum, l) => sum + l, 0) / latencyArray.length;
|
|
609
|
+
keyStats[minKey] = Math.min(keyStats[minKey], latency);
|
|
610
|
+
keyStats[maxKey] = Math.max(keyStats[maxKey], latency);
|
|
611
|
+
|
|
612
|
+
// No percentiles needed - just min/max tracking
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Reset current statistics
|
|
620
|
+
*/
|
|
621
|
+
resetCurrentStats() {
|
|
622
|
+
this.stats.current = {
|
|
623
|
+
// Read-through metrics (hits and misses only)
|
|
624
|
+
hits: 0,
|
|
625
|
+
misses: 0,
|
|
626
|
+
errors: 0,
|
|
627
|
+
totalRequests: 0,
|
|
628
|
+
latencies: [],
|
|
629
|
+
hitLatencies: [],
|
|
630
|
+
missLatencies: [],
|
|
631
|
+
|
|
632
|
+
// Native function metrics
|
|
633
|
+
nativeSets: 0,
|
|
634
|
+
nativeGets: 0,
|
|
635
|
+
nativeDeletes: 0,
|
|
636
|
+
nativeClears: 0,
|
|
637
|
+
nativeDeleteByTags: 0,
|
|
638
|
+
nativeErrors: 0,
|
|
639
|
+
totalNativeOperations: 0,
|
|
640
|
+
|
|
641
|
+
keyAccess: new Map(),
|
|
642
|
+
startTime: Date.now(),
|
|
643
|
+
lastReset: Date.now()
|
|
644
|
+
};
|
|
645
|
+
this.stats.lastPersisted = Date.now();
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Get all tracked keys from database or memory
|
|
652
|
+
* @returns {Array} - array of all tracked keys with enhanced data
|
|
653
|
+
*/
|
|
654
|
+
async getAllTrackedKeys() {
|
|
655
|
+
// If key metrics is enabled, try to get from database first
|
|
656
|
+
if (this.options.keyMetricsEnabled) {
|
|
657
|
+
try {
|
|
658
|
+
const dbKeys = await SELECT.from("plugin_cds_caching_KeyMetrics")
|
|
659
|
+
.where({ cache: this.options.cache });
|
|
660
|
+
|
|
661
|
+
if (dbKeys && dbKeys.length > 0) {
|
|
662
|
+
return dbKeys.map(key => ({
|
|
663
|
+
key: key.keyName,
|
|
664
|
+
hits: key.hits,
|
|
665
|
+
misses: key.misses,
|
|
666
|
+
totalRequests: key.totalRequests,
|
|
667
|
+
lastAccess: new Date(key.lastAccess),
|
|
668
|
+
dataType: key.dataType,
|
|
669
|
+
operation: key.operation,
|
|
670
|
+
metadata: key.metadata,
|
|
671
|
+
// Enhanced latency data
|
|
672
|
+
avgHitLatency: key.avgHitLatency,
|
|
673
|
+
avgMissLatency: key.avgMissLatency,
|
|
674
|
+
minHitLatency: key.minHitLatency,
|
|
675
|
+
maxHitLatency: key.maxHitLatency,
|
|
676
|
+
minMissLatency: key.minMissLatency,
|
|
677
|
+
maxMissLatency: key.maxMissLatency,
|
|
678
|
+
// Enhanced context data
|
|
679
|
+
context: key.context,
|
|
680
|
+
query: key.query,
|
|
681
|
+
subject: key.subject,
|
|
682
|
+
target: key.target,
|
|
683
|
+
tenant: key.tenant,
|
|
684
|
+
user: key.user,
|
|
685
|
+
locale: key.locale,
|
|
686
|
+
timestamp: new Date(key.timestamp),
|
|
687
|
+
cacheOptions: key.cacheOptions
|
|
688
|
+
}));
|
|
689
|
+
}
|
|
690
|
+
} catch (error) {
|
|
691
|
+
this.log.warn(`Failed to get all keys from database, falling back to memory: ${error.message}`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Fallback to memory-based data
|
|
696
|
+
const entries = Array.from(this.stats.current.keyAccess.entries());
|
|
697
|
+
return entries.map(([key, stats]) => ({
|
|
698
|
+
key: key,
|
|
699
|
+
hits: stats.hits,
|
|
700
|
+
misses: stats.misses,
|
|
701
|
+
totalRequests: stats.totalRequests,
|
|
702
|
+
lastAccess: new Date(stats.lastAccess),
|
|
703
|
+
dataType: stats.dataType,
|
|
704
|
+
operation: stats.operation,
|
|
705
|
+
metadata: stats.metadata,
|
|
706
|
+
// Enhanced latency data
|
|
707
|
+
avgHitLatency: stats.avgHitLatency,
|
|
708
|
+
avgMissLatency: stats.avgMissLatency,
|
|
709
|
+
minHitLatency: stats.minHitLatency === Infinity ? 0 : stats.minHitLatency,
|
|
710
|
+
maxHitLatency: stats.maxHitLatency,
|
|
711
|
+
minMissLatency: stats.minMissLatency === Infinity ? 0 : stats.minMissLatency,
|
|
712
|
+
maxMissLatency: stats.maxMissLatency,
|
|
713
|
+
// Enhanced context data
|
|
714
|
+
context: stats.context,
|
|
715
|
+
query: stats.query,
|
|
716
|
+
subject: stats.subject,
|
|
717
|
+
target: stats.target,
|
|
718
|
+
tenant: stats.tenant,
|
|
719
|
+
user: stats.user,
|
|
720
|
+
locale: stats.locale,
|
|
721
|
+
timestamp: new Date(stats.timestamp),
|
|
722
|
+
cacheOptions: stats.cacheOptions
|
|
723
|
+
}));
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Get detailed information for a specific key from database or memory
|
|
728
|
+
* @param {string} keyName - the key to get details for
|
|
729
|
+
* @returns {object|null} - detailed key information or null if not found
|
|
730
|
+
*/
|
|
731
|
+
async getKeyDetails(keyName) {
|
|
732
|
+
// If key metrics is enabled, try to get from database first
|
|
733
|
+
if (this.options.keyMetricsEnabled) {
|
|
734
|
+
try {
|
|
735
|
+
const keyDetails = await SELECT.one.from("plugin_cds_caching_KeyMetrics")
|
|
736
|
+
.where({ cache: this.options.cache, keyName: keyName });
|
|
737
|
+
|
|
738
|
+
if (keyDetails) {
|
|
739
|
+
return {
|
|
740
|
+
key: keyDetails.keyName,
|
|
741
|
+
hits: keyDetails.hits,
|
|
742
|
+
misses: keyDetails.misses,
|
|
743
|
+
sets: keyDetails.sets,
|
|
744
|
+
deletes: keyDetails.deletes,
|
|
745
|
+
totalRequests: keyDetails.totalRequests,
|
|
746
|
+
lastAccess: new Date(keyDetails.lastAccess),
|
|
747
|
+
dataType: keyDetails.dataType,
|
|
748
|
+
operation: keyDetails.operation,
|
|
749
|
+
metadata: keyDetails.metadata,
|
|
750
|
+
// Enhanced latency data
|
|
751
|
+
avgHitLatency: keyDetails.avgHitLatency,
|
|
752
|
+
avgMissLatency: keyDetails.avgMissLatency,
|
|
753
|
+
minHitLatency: keyDetails.minHitLatency,
|
|
754
|
+
maxHitLatency: keyDetails.maxHitLatency,
|
|
755
|
+
minMissLatency: keyDetails.minMissLatency,
|
|
756
|
+
maxMissLatency: keyDetails.maxMissLatency,
|
|
757
|
+
// Enhanced context data
|
|
758
|
+
context: keyDetails.context,
|
|
759
|
+
query: keyDetails.query,
|
|
760
|
+
subject: keyDetails.subject,
|
|
761
|
+
target: keyDetails.target,
|
|
762
|
+
tenant: keyDetails.tenant,
|
|
763
|
+
user: keyDetails.user,
|
|
764
|
+
locale: keyDetails.locale,
|
|
765
|
+
timestamp: new Date(keyDetails.timestamp),
|
|
766
|
+
cacheOptions: keyDetails.cacheOptions
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
} catch (error) {
|
|
770
|
+
this.log.warn(`Failed to get key details from database, falling back to memory: ${error.message}`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Fallback to memory-based data
|
|
775
|
+
const keyStats = this.stats.current.keyAccess.get(keyName);
|
|
776
|
+
if (!keyStats) {
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
return {
|
|
781
|
+
key: keyName,
|
|
782
|
+
hits: keyStats.hits,
|
|
783
|
+
misses: keyStats.misses,
|
|
784
|
+
totalRequests: keyStats.totalRequests,
|
|
785
|
+
lastAccess: new Date(keyStats.lastAccess),
|
|
786
|
+
dataType: keyStats.dataType,
|
|
787
|
+
operation: keyStats.operation,
|
|
788
|
+
metadata: keyStats.metadata,
|
|
789
|
+
// Enhanced latency data
|
|
790
|
+
avgHitLatency: keyStats.avgHitLatency,
|
|
791
|
+
avgMissLatency: keyStats.avgMissLatency,
|
|
792
|
+
minHitLatency: keyStats.minHitLatency === Infinity ? 0 : keyStats.minHitLatency,
|
|
793
|
+
maxHitLatency: keyStats.maxHitLatency,
|
|
794
|
+
minMissLatency: keyStats.minMissLatency === Infinity ? 0 : keyStats.minMissLatency,
|
|
795
|
+
maxMissLatency: keyStats.maxMissLatency,
|
|
796
|
+
// Enhanced context data
|
|
797
|
+
context: keyStats.context,
|
|
798
|
+
query: keyStats.query,
|
|
799
|
+
subject: keyStats.subject,
|
|
800
|
+
target: keyStats.target,
|
|
801
|
+
tenant: keyStats.tenant,
|
|
802
|
+
user: keyStats.user,
|
|
803
|
+
locale: keyStats.locale,
|
|
804
|
+
timestamp: new Date(keyStats.timestamp),
|
|
805
|
+
cacheOptions: keyStats.cacheOptions
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Persist statistics
|
|
811
|
+
*/
|
|
812
|
+
async persistMetrics() {
|
|
813
|
+
// Only persist if at least one type of metrics is enabled
|
|
814
|
+
if (!this.options.metricsEnabled && !this.options.keyMetricsEnabled) {
|
|
815
|
+
this.log.debug(`PersistStats called but all statistics are disabled for cache ${this.options.cache}`);
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
this.log.debug(`Starting persistStats for cache ${this.options.cache}, enabled: ${this.options.metricsEnabled}`);
|
|
820
|
+
|
|
821
|
+
const now = new Date();
|
|
822
|
+
|
|
823
|
+
// Calculate beginning of hour for hourly stats
|
|
824
|
+
const hourlyTimestamp = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), 0, 0, 0).toISOString();
|
|
825
|
+
|
|
826
|
+
// Calculate beginning of day for daily stats
|
|
827
|
+
const dailyTimestamp = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0).toISOString();
|
|
828
|
+
|
|
829
|
+
const hourlyId = `hourly:${hourlyTimestamp.slice(0, 13)}`;
|
|
830
|
+
const dailyId = `daily:${dailyTimestamp.slice(0, 10)}`;
|
|
831
|
+
|
|
832
|
+
const stats = await this.calculateStats();
|
|
833
|
+
this.log.debug(`Calculated stats for cache ${this.options.cache}:`);
|
|
834
|
+
|
|
835
|
+
try {
|
|
836
|
+
// Use persistence manager to handle database operations
|
|
837
|
+
await this.persistenceManager.persistHourlyStats(stats, hourlyId, hourlyTimestamp, this.options.metricsEnabled);
|
|
838
|
+
await this.persistenceManager.persistKeyMetrics(this.stats.current.keyAccess, this.options.keyMetricsEnabled);
|
|
839
|
+
|
|
840
|
+
// Reset current stats after successful persistence
|
|
841
|
+
this.resetCurrentStats();
|
|
842
|
+
this.stats.lastPersisted = Date.now();
|
|
843
|
+
|
|
844
|
+
this.log.debug(`Successfully persisted stats for cache ${this.options.cache}`);
|
|
845
|
+
} catch (error) {
|
|
846
|
+
this.log.error(`Failed to persist stats for cache ${this.options.cache}:`, error);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async calculateStats() {
|
|
851
|
+
const latencies = this.stats.current.latencies;
|
|
852
|
+
const hitLatencies = this.stats.current.hitLatencies;
|
|
853
|
+
const missLatencies = this.stats.current.missLatencies;
|
|
854
|
+
const totalRequests = this.stats.current.hits + this.stats.current.misses;
|
|
855
|
+
const uptimeMs = Date.now() - this.stats.current.startTime;
|
|
856
|
+
|
|
857
|
+
// Read-through latency calculations
|
|
858
|
+
const avgHitLatency = hitLatencies.length > 0
|
|
859
|
+
? hitLatencies.reduce((a, b) => a + b, 0) / hitLatencies.length
|
|
860
|
+
: 0;
|
|
861
|
+
|
|
862
|
+
const avgMissLatency = missLatencies.length > 0
|
|
863
|
+
? missLatencies.reduce((a, b) => a + b, 0) / missLatencies.length
|
|
864
|
+
: 0;
|
|
865
|
+
|
|
866
|
+
let minHitLatency = 0, maxHitLatency = 0;
|
|
867
|
+
let minMissLatency = 0, maxMissLatency = 0;
|
|
868
|
+
|
|
869
|
+
if (hitLatencies.length > 0) {
|
|
870
|
+
const sortedHitLatencies = [...hitLatencies].sort((a, b) => a - b);
|
|
871
|
+
minHitLatency = sortedHitLatencies[0];
|
|
872
|
+
maxHitLatency = sortedHitLatencies[sortedHitLatencies.length - 1];
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
if (missLatencies.length > 0) {
|
|
876
|
+
const sortedMissLatencies = [...missLatencies].sort((a, b) => a - b);
|
|
877
|
+
minMissLatency = sortedMissLatencies[0];
|
|
878
|
+
maxMissLatency = sortedMissLatencies[sortedMissLatencies.length - 1];
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Cache efficiency metrics
|
|
882
|
+
const cacheEfficiency = (hitLatencies.length > 0 && missLatencies.length > 0 && avgHitLatency > 0)
|
|
883
|
+
? avgMissLatency / avgHitLatency // How much faster hits are than misses
|
|
884
|
+
: 0;
|
|
885
|
+
|
|
886
|
+
// Calculate average read-through latency (combined hits and misses)
|
|
887
|
+
const allReadThroughLatencies = [...hitLatencies, ...missLatencies];
|
|
888
|
+
const avgReadThroughLatency = allReadThroughLatencies.length > 0
|
|
889
|
+
? allReadThroughLatencies.reduce((a, b) => a + b, 0) / allReadThroughLatencies.length
|
|
890
|
+
: 0;
|
|
891
|
+
|
|
892
|
+
// Native function throughput calculation
|
|
893
|
+
const nativeThroughput = uptimeMs > 0 ? (this.stats.current.totalNativeOperations / uptimeMs) * 1000 : 0;
|
|
894
|
+
const nativeErrorRate = this.stats.current.totalNativeOperations > 0
|
|
895
|
+
? (this.stats.current.nativeErrors / this.stats.current.totalNativeOperations) * 100
|
|
896
|
+
: 0;
|
|
897
|
+
|
|
898
|
+
return {
|
|
899
|
+
// Read-through metrics (hits and misses only)
|
|
900
|
+
hits: this.stats.current.hits,
|
|
901
|
+
misses: this.stats.current.misses,
|
|
902
|
+
errors: this.stats.current.errors,
|
|
903
|
+
totalRequests,
|
|
904
|
+
|
|
905
|
+
// Read-through latency metrics (hits and misses only)
|
|
906
|
+
avgHitLatency,
|
|
907
|
+
minHitLatency,
|
|
908
|
+
maxHitLatency,
|
|
909
|
+
avgMissLatency,
|
|
910
|
+
minMissLatency,
|
|
911
|
+
maxMissLatency,
|
|
912
|
+
avgReadThroughLatency,
|
|
913
|
+
|
|
914
|
+
// Read-through performance metrics
|
|
915
|
+
hitRatio: totalRequests > 0 ? this.stats.current.hits / totalRequests : 0,
|
|
916
|
+
throughput: uptimeMs > 0 ? (totalRequests / uptimeMs) * 1000 : 0, // requests per second
|
|
917
|
+
errorRate: totalRequests > 0 ? this.stats.current.errors / totalRequests : 0,
|
|
918
|
+
cacheEfficiency,
|
|
919
|
+
|
|
920
|
+
// Native function metrics (basic counts only)
|
|
921
|
+
nativeSets: this.stats.current.nativeSets,
|
|
922
|
+
nativeGets: this.stats.current.nativeGets,
|
|
923
|
+
nativeDeletes: this.stats.current.nativeDeletes,
|
|
924
|
+
nativeClears: this.stats.current.nativeClears,
|
|
925
|
+
nativeDeleteByTags: this.stats.current.nativeDeleteByTags,
|
|
926
|
+
nativeErrors: this.stats.current.nativeErrors,
|
|
927
|
+
totalNativeOperations: this.stats.current.totalNativeOperations,
|
|
928
|
+
|
|
929
|
+
// Native function performance metrics
|
|
930
|
+
nativeThroughput,
|
|
931
|
+
nativeErrorRate,
|
|
932
|
+
|
|
933
|
+
// Common metrics
|
|
934
|
+
memoryUsage: process.memoryUsage().heapUsed,
|
|
935
|
+
itemCount: await this.options.getItemCount?.() || 0,
|
|
936
|
+
uptimeMs
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
async getMetrics(period = 'hourly', from, to) {
|
|
941
|
+
if (!this.options.metricsEnabled) return null;
|
|
942
|
+
|
|
943
|
+
const query = SELECT.from("plugin_cds_caching_Metrics")
|
|
944
|
+
.where({ period: period });
|
|
945
|
+
|
|
946
|
+
if (from) query.and({ timestamp: { '>=': from.toISOString() } });
|
|
947
|
+
if (to) query.and({ timestamp: { '<=': to.toISOString() } });
|
|
948
|
+
|
|
949
|
+
query.orderBy({ timestamp: 'desc' });
|
|
950
|
+
|
|
951
|
+
return await query;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
async getKeyMetrics(key, from, to) {
|
|
955
|
+
if (!this.options.keyMetricsEnabled) return null;
|
|
956
|
+
const query = SELECT.from("plugin_cds_caching_KeyMetrics")
|
|
957
|
+
.where({ cache: this.options.cache, keyName: key });
|
|
958
|
+
|
|
959
|
+
if (from) query.and({ timestamp: { '>=': from.toISOString() } });
|
|
960
|
+
if (to) query.and({ timestamp: { '<=': to.toISOString() } });
|
|
961
|
+
|
|
962
|
+
query.orderBy({ timestamp: 'desc' });
|
|
963
|
+
|
|
964
|
+
return await query;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
async getCurrentKeyMetrics() {
|
|
968
|
+
if (!this.options.keyMetricsEnabled) return null;
|
|
969
|
+
return this.stats.current.keyAccess;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
async getCurrentStats() {
|
|
973
|
+
if (!this.options.metricsEnabled) return null;
|
|
974
|
+
|
|
975
|
+
const { current, lastPersisted } = this.stats;
|
|
976
|
+
const hitLatencies = current.hitLatencies;
|
|
977
|
+
const missLatencies = current.missLatencies;
|
|
978
|
+
const totalRequests = current.hits + current.misses;
|
|
979
|
+
const uptimeMs = Date.now() - current.startTime;
|
|
980
|
+
|
|
981
|
+
// Cache-through latency calculations
|
|
982
|
+
const avgHitLatency = hitLatencies.length > 0
|
|
983
|
+
? hitLatencies.reduce((a, b) => a + b, 0) / hitLatencies.length
|
|
984
|
+
: 0;
|
|
985
|
+
|
|
986
|
+
const avgMissLatency = missLatencies.length > 0
|
|
987
|
+
? missLatencies.reduce((a, b) => a + b, 0) / missLatencies.length
|
|
988
|
+
: 0;
|
|
989
|
+
|
|
990
|
+
let minHitLatency = 0, maxHitLatency = 0;
|
|
991
|
+
let minMissLatency = 0, maxMissLatency = 0;
|
|
992
|
+
|
|
993
|
+
if (hitLatencies.length > 0) {
|
|
994
|
+
const sortedHitLatencies = [...hitLatencies].sort((a, b) => a - b);
|
|
995
|
+
minHitLatency = sortedHitLatencies[0];
|
|
996
|
+
maxHitLatency = sortedHitLatencies[sortedHitLatencies.length - 1];
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if (missLatencies.length > 0) {
|
|
1000
|
+
const sortedMissLatencies = [...missLatencies].sort((a, b) => a - b);
|
|
1001
|
+
minMissLatency = sortedMissLatencies[0];
|
|
1002
|
+
maxMissLatency = sortedMissLatencies[sortedMissLatencies.length - 1];
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Cache efficiency metrics
|
|
1006
|
+
const cacheEfficiency = (hitLatencies.length > 0 && missLatencies.length > 0 && avgHitLatency > 0)
|
|
1007
|
+
? avgMissLatency / avgHitLatency // How much faster hits are than misses
|
|
1008
|
+
: 0;
|
|
1009
|
+
|
|
1010
|
+
// Calculate average read-through latency (combined hits and misses)
|
|
1011
|
+
const allReadThroughLatencies = [...hitLatencies, ...missLatencies];
|
|
1012
|
+
const avgReadThroughLatency = allReadThroughLatencies.length > 0
|
|
1013
|
+
? allReadThroughLatencies.reduce((a, b) => a + b, 0) / allReadThroughLatencies.length
|
|
1014
|
+
: 0;
|
|
1015
|
+
|
|
1016
|
+
// Native function performance metrics
|
|
1017
|
+
const nativeThroughput = uptimeMs > 0 ? (current.totalNativeOperations / uptimeMs) * 1000 : 0;
|
|
1018
|
+
const nativeErrorRate = current.totalNativeOperations > 0
|
|
1019
|
+
? (current.nativeErrors / current.totalNativeOperations) * 100
|
|
1020
|
+
: 0;
|
|
1021
|
+
|
|
1022
|
+
return {
|
|
1023
|
+
// Read-through metrics (hits and misses only - sets/deletes are redundant)
|
|
1024
|
+
hits: current.hits,
|
|
1025
|
+
misses: current.misses,
|
|
1026
|
+
errors: current.errors,
|
|
1027
|
+
totalRequests,
|
|
1028
|
+
|
|
1029
|
+
// Read-through latency metrics (hits and misses only)
|
|
1030
|
+
avgHitLatency,
|
|
1031
|
+
minHitLatency,
|
|
1032
|
+
maxHitLatency,
|
|
1033
|
+
avgMissLatency,
|
|
1034
|
+
minMissLatency,
|
|
1035
|
+
maxMissLatency,
|
|
1036
|
+
avgReadThroughLatency,
|
|
1037
|
+
|
|
1038
|
+
// Read-through performance metrics
|
|
1039
|
+
hitRatio: totalRequests > 0 ? current.hits / totalRequests : 0,
|
|
1040
|
+
throughput: uptimeMs > 0 ? (totalRequests / uptimeMs) * 1000 : 0,
|
|
1041
|
+
errorRate: totalRequests > 0 ? current.errors / totalRequests : 0,
|
|
1042
|
+
cacheEfficiency,
|
|
1043
|
+
|
|
1044
|
+
// Native function metrics
|
|
1045
|
+
nativeSets: current.nativeSets,
|
|
1046
|
+
nativeGets: current.nativeGets,
|
|
1047
|
+
nativeDeletes: current.nativeDeletes,
|
|
1048
|
+
nativeClears: current.nativeClears,
|
|
1049
|
+
nativeDeleteByTags: current.nativeDeleteByTags,
|
|
1050
|
+
nativeErrors: current.nativeErrors,
|
|
1051
|
+
totalNativeOperations: current.totalNativeOperations,
|
|
1052
|
+
|
|
1053
|
+
// Native function performance metrics
|
|
1054
|
+
nativeThroughput,
|
|
1055
|
+
nativeErrorRate,
|
|
1056
|
+
|
|
1057
|
+
// Common metrics
|
|
1058
|
+
memoryUsage: process.memoryUsage().heapUsed,
|
|
1059
|
+
itemCount: await this.options.getItemCount?.() || 0,
|
|
1060
|
+
uptimeMs
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
dispose() {
|
|
1065
|
+
if (this.persistInterval) {
|
|
1066
|
+
clearInterval(this.persistInterval);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Manually trigger persistence (for testing and debugging)
|
|
1072
|
+
*/
|
|
1073
|
+
async triggerPersistence() {
|
|
1074
|
+
this.log.info(`Manually triggering persistence for cache ${this.options.cache}, enabled: ${this.options.metricsEnabled}, keyMetrics: ${this.options.keyMetricsEnabled}`);
|
|
1075
|
+
if (this.options.metricsEnabled || this.options.keyMetricsEnabled) {
|
|
1076
|
+
await this.persistMetrics();
|
|
1077
|
+
} else {
|
|
1078
|
+
this.log.info(`Persistence skipped - all statistics are disabled for cache ${this.options.cache}`);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* Get current persistence interval status
|
|
1084
|
+
*/
|
|
1085
|
+
getPersistenceStatus() {
|
|
1086
|
+
return {
|
|
1087
|
+
metricsEnabled: this.options.metricsEnabled,
|
|
1088
|
+
keyMetricsEnabled: this.options.keyMetricsEnabled,
|
|
1089
|
+
intervalExists: this.persistInterval !== null,
|
|
1090
|
+
lastPersisted: this.stats.lastPersisted,
|
|
1091
|
+
persistenceInterval: this.options.persistenceInterval
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
async clearMetrics() {
|
|
1096
|
+
await this.persistenceManager.deleteMetrics();
|
|
1097
|
+
// Also reset the current stats
|
|
1098
|
+
this.resetCurrentStats();
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
async clearKeyMetrics() {
|
|
1102
|
+
await this.persistenceManager.deleteKeyMetrics();
|
|
1103
|
+
// Also reset the current stats
|
|
1104
|
+
this.resetCurrentStats();
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Delete metrics (alias for clearMetrics for compatibility)
|
|
1109
|
+
* @returns {Promise<void>}
|
|
1110
|
+
*/
|
|
1111
|
+
async deleteMetrics() {
|
|
1112
|
+
return this.clearMetrics();
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/**
|
|
1116
|
+
* Delete key metrics (alias for clearKeyMetrics for compatibility)
|
|
1117
|
+
* @returns {Promise<void>}
|
|
1118
|
+
*/
|
|
1119
|
+
async deleteKeyMetrics() {
|
|
1120
|
+
return this.clearKeyMetrics();
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
module.exports = CacheStatisticsHandler;
|