@vtex/api 7.5.1-beta.0 → 7.5.1-beta.2
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/CHANGELOG.md +18 -0
- package/lib/caches/DiskCache.d.ts +1 -3
- package/lib/caches/DiskCache.js +13 -18
- package/lib/caches/LRUCache.d.ts +1 -4
- package/lib/caches/LRUCache.js +23 -21
- package/lib/caches/LRUDiskCache.d.ts +1 -4
- package/lib/caches/LRUDiskCache.js +25 -26
- package/lib/caches/MultilayeredCache.d.ts +1 -3
- package/lib/caches/MultilayeredCache.js +11 -14
- package/lib/caches/WindowedCounters.d.ts +35 -0
- package/lib/caches/WindowedCounters.js +45 -0
- package/lib/caches/typings.d.ts +3 -2
- package/lib/metrics/DiagnosticsMetrics.d.ts +32 -23
- package/lib/metrics/DiagnosticsMetrics.js +166 -95
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
6
6
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [7.5.1-beta.2]
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `DiagnosticsMetrics.trackCache(name, cache)`: observable cache metrics (`io_app_cache_*`), safe alongside the legacy `trackCache`
|
|
13
|
+
- `DiagnosticsMetrics.registerObservableGauge/Counter(name, observe, options?)`: pull-based instruments
|
|
14
|
+
- `getCumulativeStats()` on the four cache classes: process-lifetime totals, side-effect free
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- `io_app_cache_operations_total{cache_state="miss"}` counts misses when a read resolves, so the
|
|
19
|
+
series stays monotonic for the async caches (`LRUDiskCache`, `DiskCache`)
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- `@vtex/diagnostics-nodejs` pinned to `0.2.0-io`, the IO line's current tag, replacing the
|
|
24
|
+
`0.1.8-io-beta.4` beta
|
|
25
|
+
|
|
8
26
|
## [7.5.0]
|
|
9
27
|
|
|
10
28
|
### Added
|
|
@@ -5,10 +5,8 @@ export declare class DiskCache<V> implements CacheLayer<string, V> {
|
|
|
5
5
|
private cachePath;
|
|
6
6
|
private readFile;
|
|
7
7
|
private writeFile;
|
|
8
|
-
private
|
|
9
|
-
private total;
|
|
8
|
+
private readonly counters;
|
|
10
9
|
private lock;
|
|
11
|
-
private reported;
|
|
12
10
|
constructor(cachePath: string, readFile?: typeof readJSON, writeFile?: typeof outputJSON);
|
|
13
11
|
has: (key: string) => boolean;
|
|
14
12
|
getStats: (name?: string) => DiskStats;
|
package/lib/caches/DiskCache.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.DiskCache = void 0;
|
|
7
|
+
const WindowedCounters_1 = require("./WindowedCounters");
|
|
7
8
|
const fs_extra_1 = require("fs-extra");
|
|
8
9
|
const path_1 = require("path");
|
|
9
10
|
const rwlock_1 = __importDefault(require("rwlock"));
|
|
@@ -12,39 +13,33 @@ class DiskCache {
|
|
|
12
13
|
this.cachePath = cachePath;
|
|
13
14
|
this.readFile = readFile;
|
|
14
15
|
this.writeFile = writeFile;
|
|
15
|
-
this.
|
|
16
|
-
this.total = 0;
|
|
17
|
-
this.reported = { hits: 0, total: 0 };
|
|
16
|
+
this.counters = new WindowedCounters_1.WindowedCounters();
|
|
18
17
|
this.has = (key) => {
|
|
19
18
|
const pathKey = this.getPathKey(key);
|
|
20
19
|
return (0, fs_extra_1.pathExistsSync)(pathKey);
|
|
21
20
|
};
|
|
22
21
|
this.getStats = (name = 'disk-cache') => {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
return stats;
|
|
22
|
+
const { hits, total } = this.counters.windowed();
|
|
23
|
+
return { hits, name, total };
|
|
24
|
+
};
|
|
25
|
+
this.getCumulativeStats = () => {
|
|
26
|
+
const { hits, misses, total } = this.counters.cumulative();
|
|
27
|
+
return { hits, misses, total };
|
|
30
28
|
};
|
|
31
|
-
this.getCumulativeStats = () => ({
|
|
32
|
-
hits: this.hits,
|
|
33
|
-
total: this.total,
|
|
34
|
-
});
|
|
35
29
|
this.get = async (key) => {
|
|
36
30
|
const pathKey = this.getPathKey(key);
|
|
37
|
-
this.
|
|
31
|
+
this.counters.countRead();
|
|
38
32
|
const data = await new Promise(resolve => {
|
|
39
33
|
this.lock.readLock(key, async (release) => {
|
|
40
34
|
try {
|
|
41
35
|
const fileData = await this.readFile(pathKey);
|
|
42
36
|
release();
|
|
43
|
-
this.
|
|
37
|
+
this.counters.countHit();
|
|
44
38
|
resolve(fileData);
|
|
45
39
|
}
|
|
46
|
-
catch
|
|
40
|
+
catch {
|
|
47
41
|
release();
|
|
42
|
+
this.counters.countMiss(); // a missing or unreadable file is a miss
|
|
48
43
|
resolve(null);
|
|
49
44
|
}
|
|
50
45
|
});
|
|
@@ -60,7 +55,7 @@ class DiskCache {
|
|
|
60
55
|
release();
|
|
61
56
|
resolve(writePromise);
|
|
62
57
|
}
|
|
63
|
-
catch
|
|
58
|
+
catch {
|
|
64
59
|
release();
|
|
65
60
|
resolve(true);
|
|
66
61
|
}
|
package/lib/caches/LRUCache.d.ts
CHANGED
|
@@ -4,10 +4,7 @@ import { CumulativeStats, FetchResult, LRUStats } from './typings';
|
|
|
4
4
|
export declare class LRUCache<K, V> implements CacheLayer<K, V> {
|
|
5
5
|
private multilayer;
|
|
6
6
|
private storage;
|
|
7
|
-
private
|
|
8
|
-
private total;
|
|
9
|
-
private disposed;
|
|
10
|
-
private reported;
|
|
7
|
+
private readonly counters;
|
|
11
8
|
constructor(options: LRU.Options<K, V>);
|
|
12
9
|
get: (key: K) => V | void;
|
|
13
10
|
getOrSet: (key: K, fetcher?: () => Promise<FetchResult<V>>) => Promise<V | void>;
|
package/lib/caches/LRUCache.js
CHANGED
|
@@ -6,24 +6,27 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.LRUCache = void 0;
|
|
7
7
|
const lru_cache_1 = __importDefault(require("lru-cache"));
|
|
8
8
|
const MultilayeredCache_1 = require("./MultilayeredCache");
|
|
9
|
+
const WindowedCounters_1 = require("./WindowedCounters");
|
|
9
10
|
class LRUCache {
|
|
10
11
|
constructor(options) {
|
|
11
12
|
this.get = (key) => {
|
|
12
13
|
const value = this.storage.get(key);
|
|
13
14
|
if (this.storage.has(key)) {
|
|
14
|
-
this.
|
|
15
|
+
this.counters.countHit();
|
|
15
16
|
}
|
|
16
|
-
|
|
17
|
+
else {
|
|
18
|
+
this.counters.countMiss();
|
|
19
|
+
}
|
|
20
|
+
this.counters.countRead();
|
|
17
21
|
return value;
|
|
18
22
|
};
|
|
19
23
|
this.getOrSet = async (key, fetcher) => this.multilayer.get(key, fetcher);
|
|
20
24
|
this.set = (key, value, maxAge) => this.storage.set(key, value, maxAge);
|
|
21
25
|
this.has = (key) => this.storage.has(key);
|
|
22
26
|
this.getStats = (name = 'lru-cache') => {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
disposedItems: this.disposed - this.reported.disposed,
|
|
27
|
+
const { disposed, hits, total } = this.counters.windowed();
|
|
28
|
+
return {
|
|
29
|
+
disposedItems: disposed,
|
|
27
30
|
hitRate: total > 0 ? hits / total : undefined,
|
|
28
31
|
hits,
|
|
29
32
|
itemCount: this.storage.itemCount,
|
|
@@ -32,24 +35,23 @@ class LRUCache {
|
|
|
32
35
|
name,
|
|
33
36
|
total,
|
|
34
37
|
};
|
|
35
|
-
this.reported = { disposed: this.disposed, hits: this.hits, total: this.total };
|
|
36
|
-
return stats;
|
|
37
38
|
};
|
|
38
|
-
this.getCumulativeStats = () =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
39
|
+
this.getCumulativeStats = () => {
|
|
40
|
+
const { disposed, hits, misses, total } = this.counters.cumulative();
|
|
41
|
+
return {
|
|
42
|
+
disposedItems: disposed,
|
|
43
|
+
hits,
|
|
44
|
+
itemCount: this.storage.itemCount,
|
|
45
|
+
length: this.storage.length,
|
|
46
|
+
max: this.storage.max,
|
|
47
|
+
misses,
|
|
48
|
+
total,
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
this.counters = new WindowedCounters_1.WindowedCounters();
|
|
50
52
|
this.storage = new lru_cache_1.default({
|
|
51
53
|
...options,
|
|
52
|
-
dispose: () => this.
|
|
54
|
+
dispose: () => this.counters.countDisposed(),
|
|
53
55
|
noDisposeOnSet: true,
|
|
54
56
|
});
|
|
55
57
|
this.multilayer = new MultilayeredCache_1.MultilayeredCache([this]);
|
|
@@ -6,12 +6,9 @@ export declare class LRUDiskCache<V> implements CacheLayer<string, V> {
|
|
|
6
6
|
private readFile;
|
|
7
7
|
private writeFile;
|
|
8
8
|
private lock;
|
|
9
|
-
private
|
|
10
|
-
private hits;
|
|
11
|
-
private total;
|
|
9
|
+
private readonly counters;
|
|
12
10
|
private lruStorage;
|
|
13
11
|
private keyToBeDeleted;
|
|
14
|
-
private reported;
|
|
15
12
|
constructor(cachePath: string, options: LRUDiskCacheOptions, readFile?: typeof readJSON, writeFile?: typeof outputJSON);
|
|
16
13
|
has: (key: string) => boolean;
|
|
17
14
|
getStats: (name?: string) => LRUStats;
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.LRUDiskCache = void 0;
|
|
7
|
+
const WindowedCounters_1 = require("./WindowedCounters");
|
|
7
8
|
const fs_extra_1 = require("fs-extra");
|
|
8
9
|
const lru_cache_1 = __importDefault(require("lru-cache"));
|
|
9
10
|
const path_1 = require("path");
|
|
@@ -13,15 +14,11 @@ class LRUDiskCache {
|
|
|
13
14
|
this.cachePath = cachePath;
|
|
14
15
|
this.readFile = readFile;
|
|
15
16
|
this.writeFile = writeFile;
|
|
16
|
-
this.hits = 0;
|
|
17
|
-
this.total = 0;
|
|
18
|
-
this.reported = { disposed: 0, hits: 0, total: 0 };
|
|
19
17
|
this.has = (key) => this.lruStorage.has(key);
|
|
20
18
|
this.getStats = (name = 'disk-lru-cache') => {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
disposedItems: this.disposed - this.reported.disposed,
|
|
19
|
+
const { disposed, hits, total } = this.counters.windowed();
|
|
20
|
+
return {
|
|
21
|
+
disposedItems: disposed,
|
|
25
22
|
hitRate: total > 0 ? hits / total : undefined,
|
|
26
23
|
hits,
|
|
27
24
|
itemCount: this.lruStorage.itemCount,
|
|
@@ -30,21 +27,24 @@ class LRUDiskCache {
|
|
|
30
27
|
name,
|
|
31
28
|
total,
|
|
32
29
|
};
|
|
33
|
-
this.reported = { disposed: this.disposed, hits: this.hits, total: this.total };
|
|
34
|
-
return stats;
|
|
35
30
|
};
|
|
36
|
-
this.getCumulativeStats = () =>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
31
|
+
this.getCumulativeStats = () => {
|
|
32
|
+
const { disposed, hits, misses, total } = this.counters.cumulative();
|
|
33
|
+
return {
|
|
34
|
+
disposedItems: disposed,
|
|
35
|
+
hits,
|
|
36
|
+
itemCount: this.lruStorage.itemCount,
|
|
37
|
+
length: this.lruStorage.length,
|
|
38
|
+
max: this.lruStorage.max,
|
|
39
|
+
misses,
|
|
40
|
+
total,
|
|
41
|
+
};
|
|
42
|
+
};
|
|
44
43
|
this.get = async (key) => {
|
|
45
44
|
const timeOfDeath = this.lruStorage.get(key);
|
|
46
|
-
this.
|
|
45
|
+
this.counters.countRead();
|
|
47
46
|
if (timeOfDeath === undefined) {
|
|
47
|
+
this.counters.countMiss();
|
|
48
48
|
// if it is an outdated file when stale=false
|
|
49
49
|
if (this.keyToBeDeleted) {
|
|
50
50
|
await this.deleteFile(key);
|
|
@@ -57,11 +57,12 @@ class LRUDiskCache {
|
|
|
57
57
|
try {
|
|
58
58
|
const fileData = await this.readFile(pathKey);
|
|
59
59
|
release();
|
|
60
|
-
this.
|
|
60
|
+
this.counters.countHit();
|
|
61
61
|
resolve(fileData);
|
|
62
62
|
}
|
|
63
|
-
catch
|
|
63
|
+
catch {
|
|
64
64
|
release();
|
|
65
|
+
this.counters.countMiss(); // a missing or unreadable file is a miss
|
|
65
66
|
resolve(null);
|
|
66
67
|
}
|
|
67
68
|
});
|
|
@@ -93,7 +94,7 @@ class LRUDiskCache {
|
|
|
93
94
|
release();
|
|
94
95
|
resolve(writePromise);
|
|
95
96
|
}
|
|
96
|
-
catch
|
|
97
|
+
catch {
|
|
97
98
|
release();
|
|
98
99
|
resolve(true);
|
|
99
100
|
}
|
|
@@ -114,7 +115,7 @@ class LRUDiskCache {
|
|
|
114
115
|
release();
|
|
115
116
|
resolve(removePromise);
|
|
116
117
|
}
|
|
117
|
-
catch
|
|
118
|
+
catch {
|
|
118
119
|
release();
|
|
119
120
|
resolve(true);
|
|
120
121
|
}
|
|
@@ -122,14 +123,12 @@ class LRUDiskCache {
|
|
|
122
123
|
});
|
|
123
124
|
return !failure;
|
|
124
125
|
};
|
|
125
|
-
this.
|
|
126
|
-
this.total = 0;
|
|
127
|
-
this.disposed = 0;
|
|
126
|
+
this.counters = new WindowedCounters_1.WindowedCounters();
|
|
128
127
|
this.keyToBeDeleted = '';
|
|
129
128
|
this.lock = new rwlock_1.default();
|
|
130
129
|
const dispose = (key) => {
|
|
131
130
|
this.keyToBeDeleted = key;
|
|
132
|
-
this.
|
|
131
|
+
this.counters.countDisposed();
|
|
133
132
|
};
|
|
134
133
|
const lruOptions = {
|
|
135
134
|
...options,
|
|
@@ -2,9 +2,7 @@ import { CacheLayer } from './CacheLayer';
|
|
|
2
2
|
import { CumulativeStats, FetchResult, MultilayerStats } from './typings';
|
|
3
3
|
export declare class MultilayeredCache<K, V> implements CacheLayer<K, V> {
|
|
4
4
|
private caches;
|
|
5
|
-
private
|
|
6
|
-
private total;
|
|
7
|
-
private reported;
|
|
5
|
+
private readonly counters;
|
|
8
6
|
constructor(caches: Array<CacheLayer<K, V>>);
|
|
9
7
|
get: (key: K, fetcher?: () => Promise<FetchResult<V>>) => Promise<V | void>;
|
|
10
8
|
set: (key: K, value: V, maxAge?: number) => Promise<boolean>;
|
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MultilayeredCache = void 0;
|
|
4
4
|
const ramda_1 = require("ramda");
|
|
5
|
+
const WindowedCounters_1 = require("./WindowedCounters");
|
|
5
6
|
class MultilayeredCache {
|
|
6
7
|
constructor(caches) {
|
|
7
8
|
this.caches = caches;
|
|
8
|
-
this.
|
|
9
|
-
this.total = 0;
|
|
10
|
-
this.reported = { hits: 0, total: 0 };
|
|
9
|
+
this.counters = new WindowedCounters_1.WindowedCounters();
|
|
11
10
|
this.get = async (key, fetcher) => {
|
|
12
11
|
let value = undefined;
|
|
13
12
|
let maxAge;
|
|
@@ -41,30 +40,28 @@ class MultilayeredCache {
|
|
|
41
40
|
return (0, ramda_1.any)(item => item, hasList);
|
|
42
41
|
};
|
|
43
42
|
this.getStats = (name = 'multilayred-cache') => {
|
|
44
|
-
const hits
|
|
45
|
-
|
|
46
|
-
const multilayerStats = {
|
|
43
|
+
const { hits, total } = this.counters.windowed();
|
|
44
|
+
return {
|
|
47
45
|
hitRate: total > 0 ? hits / total : undefined,
|
|
48
46
|
hits,
|
|
49
47
|
name,
|
|
50
48
|
total,
|
|
51
49
|
};
|
|
52
|
-
this.reported = { hits: this.hits, total: this.total };
|
|
53
|
-
return multilayerStats;
|
|
54
50
|
};
|
|
55
|
-
this.getCumulativeStats = () =>
|
|
56
|
-
hits
|
|
57
|
-
total
|
|
58
|
-
}
|
|
51
|
+
this.getCumulativeStats = () => {
|
|
52
|
+
const { hits, misses, total } = this.counters.cumulative();
|
|
53
|
+
return { hits, misses, total };
|
|
54
|
+
};
|
|
59
55
|
this.findIndex = async (func, array) => {
|
|
60
|
-
this.
|
|
56
|
+
this.counters.countRead();
|
|
61
57
|
for (let index = 0; index < array.length; index++) {
|
|
62
58
|
const hasKey = await func(array[index]);
|
|
63
59
|
if (hasKey) {
|
|
64
|
-
this.
|
|
60
|
+
this.counters.countHit();
|
|
65
61
|
return index;
|
|
66
62
|
}
|
|
67
63
|
}
|
|
64
|
+
this.counters.countMiss();
|
|
68
65
|
return -1;
|
|
69
66
|
};
|
|
70
67
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counters with a windowed read. `windowed()` reports the delta since the previous read and
|
|
3
|
+
* `cumulative()` the process-lifetime total, so a windowed reader (the legacy flush) and a
|
|
4
|
+
* cumulative one (the observable reader) can share a cache without consuming each other's
|
|
5
|
+
* counts. Shared by the four cache classes so a fix to the delta/rollover behaviour cannot
|
|
6
|
+
* land in one and be forgotten in the others.
|
|
7
|
+
*
|
|
8
|
+
* Misses are counted when a read resolves, never when it starts. The observable reader
|
|
9
|
+
* publishes them as a monotonic counter, so a value derived from `total - hits` would dip
|
|
10
|
+
* while a read is in flight — and the SDK reports a dip on a monotonic counter as a reset,
|
|
11
|
+
* publishing the whole running total as one cycle's delta.
|
|
12
|
+
*/
|
|
13
|
+
export declare class WindowedCounters {
|
|
14
|
+
private hits;
|
|
15
|
+
private misses;
|
|
16
|
+
private total;
|
|
17
|
+
private disposed;
|
|
18
|
+
private reported;
|
|
19
|
+
countHit: () => void;
|
|
20
|
+
countMiss: () => void;
|
|
21
|
+
countRead: () => void;
|
|
22
|
+
countDisposed: () => void;
|
|
23
|
+
windowed: () => {
|
|
24
|
+
disposed: number;
|
|
25
|
+
hits: number;
|
|
26
|
+
misses: number;
|
|
27
|
+
total: number;
|
|
28
|
+
};
|
|
29
|
+
cumulative: () => {
|
|
30
|
+
disposed: number;
|
|
31
|
+
hits: number;
|
|
32
|
+
misses: number;
|
|
33
|
+
total: number;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WindowedCounters = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Counters with a windowed read. `windowed()` reports the delta since the previous read and
|
|
6
|
+
* `cumulative()` the process-lifetime total, so a windowed reader (the legacy flush) and a
|
|
7
|
+
* cumulative one (the observable reader) can share a cache without consuming each other's
|
|
8
|
+
* counts. Shared by the four cache classes so a fix to the delta/rollover behaviour cannot
|
|
9
|
+
* land in one and be forgotten in the others.
|
|
10
|
+
*
|
|
11
|
+
* Misses are counted when a read resolves, never when it starts. The observable reader
|
|
12
|
+
* publishes them as a monotonic counter, so a value derived from `total - hits` would dip
|
|
13
|
+
* while a read is in flight — and the SDK reports a dip on a monotonic counter as a reset,
|
|
14
|
+
* publishing the whole running total as one cycle's delta.
|
|
15
|
+
*/
|
|
16
|
+
class WindowedCounters {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.hits = 0;
|
|
19
|
+
this.misses = 0;
|
|
20
|
+
this.total = 0;
|
|
21
|
+
this.disposed = 0;
|
|
22
|
+
this.reported = { disposed: 0, hits: 0, misses: 0, total: 0 };
|
|
23
|
+
this.countHit = () => { this.hits += 1; };
|
|
24
|
+
this.countMiss = () => { this.misses += 1; };
|
|
25
|
+
this.countRead = () => { this.total += 1; };
|
|
26
|
+
this.countDisposed = () => { this.disposed += 1; };
|
|
27
|
+
this.windowed = () => {
|
|
28
|
+
const window = {
|
|
29
|
+
disposed: this.disposed - this.reported.disposed,
|
|
30
|
+
hits: this.hits - this.reported.hits,
|
|
31
|
+
misses: this.misses - this.reported.misses,
|
|
32
|
+
total: this.total - this.reported.total,
|
|
33
|
+
};
|
|
34
|
+
this.reported = { disposed: this.disposed, hits: this.hits, misses: this.misses, total: this.total };
|
|
35
|
+
return window;
|
|
36
|
+
};
|
|
37
|
+
this.cumulative = () => ({
|
|
38
|
+
disposed: this.disposed,
|
|
39
|
+
hits: this.hits,
|
|
40
|
+
misses: this.misses,
|
|
41
|
+
total: this.total,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
exports.WindowedCounters = WindowedCounters;
|
package/lib/caches/typings.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { CumulativeStats } from '../caches/typings';
|
|
|
3
3
|
export interface TrackedCache {
|
|
4
4
|
getCumulativeStats(): CumulativeStats;
|
|
5
5
|
}
|
|
6
|
+
type ObservableMetricOptions = Pick<MetricOptions, 'description' | 'unit'>;
|
|
6
7
|
/**
|
|
7
8
|
* DiagnosticsMetrics provides a high-level API for recording metrics using
|
|
8
9
|
* the @vtex/diagnostics-nodejs library. It completely abstracts instrument
|
|
@@ -72,6 +73,7 @@ export declare class DiagnosticsMetrics {
|
|
|
72
73
|
* Called automatically in constructor.
|
|
73
74
|
*/
|
|
74
75
|
private initMetricClient;
|
|
76
|
+
private warnPendingRegistrations;
|
|
75
77
|
/**
|
|
76
78
|
* Create the single shared histogram for all latency measurements.
|
|
77
79
|
* Called after metric client is initialized.
|
|
@@ -126,20 +128,34 @@ export declare class DiagnosticsMetrics {
|
|
|
126
128
|
* @returns Merged attributes (base + non-conflicting limited custom) or undefined if both are empty
|
|
127
129
|
*/
|
|
128
130
|
private mergeAttributes;
|
|
131
|
+
/**
|
|
132
|
+
* The reason `name` cannot carry a new instrument, or undefined when it can. The SDK
|
|
133
|
+
* accepts two same-named streams silently and the collector then rejects them, so a name
|
|
134
|
+
* is owned by whichever instrument took it first: reusing it through the other API, or
|
|
135
|
+
* under the other kind, is a conflict. Reusing it in place — same API, same kind — is a
|
|
136
|
+
* replace, not a conflict.
|
|
137
|
+
*
|
|
138
|
+
* Disposing leaves a gap: remove() only detaches the callback, so the meter keeps the
|
|
139
|
+
* instrument and this check stops seeing the name. Reusing it under the other kind from
|
|
140
|
+
* then on would duplicate the stream, which only bites under cumulative temporality (a
|
|
141
|
+
* delta export drops the orphan). Re-registering the same kind after dispose is safe.
|
|
142
|
+
*/
|
|
143
|
+
private nameConflict;
|
|
144
|
+
private pushInstruments;
|
|
145
|
+
/**
|
|
146
|
+
* Counter and gauge instruments are created once per name and reused.
|
|
147
|
+
*/
|
|
148
|
+
private instrumentFor;
|
|
129
149
|
/**
|
|
130
150
|
* Record a latency measurement using the single shared histogram.
|
|
131
151
|
* Accepts either an hrtime tuple from process.hrtime() or milliseconds as a number.
|
|
132
152
|
* Use attributes to differentiate between different operations.
|
|
133
153
|
*
|
|
134
|
-
* Base attributes from
|
|
135
|
-
*
|
|
136
|
-
* precedence - if a custom attribute key conflicts with a base attribute key,
|
|
137
|
-
* the custom attribute is silently dropped.
|
|
138
|
-
*
|
|
139
|
-
* Custom attributes are limited to MAX_CUSTOM_ATTRIBUTES (5). Base attributes are not limited.
|
|
154
|
+
* Base attributes from `runWithBaseAttributes` are merged in automatically and take
|
|
155
|
+
* precedence; custom attributes are capped at MAX_CUSTOM_ATTRIBUTES (7). See the class doc.
|
|
140
156
|
*
|
|
141
157
|
* @param value Either [seconds, nanoseconds] from process.hrtime() or milliseconds
|
|
142
|
-
* @param attributes Custom attributes including 'operation' to identify the operation type (max
|
|
158
|
+
* @param attributes Custom attributes including 'operation' to identify the operation type (max 7 custom attributes)
|
|
143
159
|
*
|
|
144
160
|
* @example
|
|
145
161
|
* ```typescript
|
|
@@ -156,16 +172,12 @@ export declare class DiagnosticsMetrics {
|
|
|
156
172
|
* Increment a counter by a specific value.
|
|
157
173
|
* Multiple counters are stored by name since counters represent different types of events.
|
|
158
174
|
*
|
|
159
|
-
* Base attributes from
|
|
160
|
-
*
|
|
161
|
-
* precedence - if a custom attribute key conflicts with a base attribute key,
|
|
162
|
-
* the custom attribute is silently dropped.
|
|
163
|
-
*
|
|
164
|
-
* Custom attributes are limited to MAX_CUSTOM_ATTRIBUTES (5). Base attributes are not limited.
|
|
175
|
+
* Base attributes from `runWithBaseAttributes` are merged in automatically and take
|
|
176
|
+
* precedence; custom attributes are capped at MAX_CUSTOM_ATTRIBUTES (7). See the class doc.
|
|
165
177
|
*
|
|
166
178
|
* @param name Counter name (e.g., 'http_requests_total', 'cache_hits_total')
|
|
167
179
|
* @param value Amount to increment by (typically 1)
|
|
168
|
-
* @param attributes Optional custom attributes for the counter (max
|
|
180
|
+
* @param attributes Optional custom attributes for the counter (max 7 custom attributes, e.g., { method: 'GET', status: '2xx' })
|
|
169
181
|
*
|
|
170
182
|
* @example
|
|
171
183
|
* ```typescript
|
|
@@ -177,16 +189,12 @@ export declare class DiagnosticsMetrics {
|
|
|
177
189
|
* Set a gauge to a specific value (current state).
|
|
178
190
|
* Multiple gauges are stored by name since gauges represent different types of measurements.
|
|
179
191
|
*
|
|
180
|
-
* Base attributes from
|
|
181
|
-
*
|
|
182
|
-
* precedence - if a custom attribute key conflicts with a base attribute key,
|
|
183
|
-
* the custom attribute is silently dropped.
|
|
184
|
-
*
|
|
185
|
-
* Custom attributes are limited to MAX_CUSTOM_ATTRIBUTES (5). Base attributes are not limited.
|
|
192
|
+
* Base attributes from `runWithBaseAttributes` are merged in automatically and take
|
|
193
|
+
* precedence; custom attributes are capped at MAX_CUSTOM_ATTRIBUTES (7). See the class doc.
|
|
186
194
|
*
|
|
187
195
|
* @param name Gauge name (e.g., 'cache_items_current', 'memory_usage_bytes')
|
|
188
196
|
* @param value Current value
|
|
189
|
-
* @param attributes Optional custom attributes for the gauge (max
|
|
197
|
+
* @param attributes Optional custom attributes for the gauge (max 7 custom attributes, e.g., { cache: 'pages' })
|
|
190
198
|
*
|
|
191
199
|
* @example
|
|
192
200
|
* ```typescript
|
|
@@ -194,8 +202,8 @@ export declare class DiagnosticsMetrics {
|
|
|
194
202
|
* ```
|
|
195
203
|
*/
|
|
196
204
|
setGauge(name: string, value: number, attributes?: Attributes): void;
|
|
197
|
-
registerObservableGauge(name: string, observe: ObservableCallback, options?:
|
|
198
|
-
registerObservableCounter(name: string, observe: ObservableCallback, options?:
|
|
205
|
+
registerObservableGauge(name: string, observe: ObservableCallback, options?: ObservableMetricOptions): () => void;
|
|
206
|
+
registerObservableCounter(name: string, observe: ObservableCallback, options?: ObservableMetricOptions): () => void;
|
|
199
207
|
private registerObservable;
|
|
200
208
|
private syncObservables;
|
|
201
209
|
private flushPendingObservables;
|
|
@@ -203,3 +211,4 @@ export declare class DiagnosticsMetrics {
|
|
|
203
211
|
private ensureCacheInstruments;
|
|
204
212
|
private observeCaches;
|
|
205
213
|
}
|
|
214
|
+
export {};
|
|
@@ -22,6 +22,14 @@ const CACHE_OPERATIONS_METRIC = 'io_app_cache_operations_total';
|
|
|
22
22
|
const CACHE_ITEMS_METRIC = 'io_app_cache_items_current';
|
|
23
23
|
const CACHE_CAPACITY_METRIC = 'io_app_cache_capacity';
|
|
24
24
|
const CACHE_DISPOSED_METRIC = 'io_app_cache_disposed_total';
|
|
25
|
+
// Names owned by trackCache(), which creates them straight on the client. An app
|
|
26
|
+
// registering one of them as its own observable would publish a duplicate stream.
|
|
27
|
+
const CACHE_METRIC_NAMES = new Set([
|
|
28
|
+
CACHE_OPERATIONS_METRIC,
|
|
29
|
+
CACHE_ITEMS_METRIC,
|
|
30
|
+
CACHE_CAPACITY_METRIC,
|
|
31
|
+
CACHE_DISPOSED_METRIC,
|
|
32
|
+
]);
|
|
25
33
|
/**
|
|
26
34
|
* Converts an hrtime tuple [seconds, nanoseconds] to milliseconds.
|
|
27
35
|
*/
|
|
@@ -52,9 +60,43 @@ function limitCustomAttributes(customAttributes) {
|
|
|
52
60
|
return Object.fromEntries(entries.slice(0, MAX_CUSTOM_ATTRIBUTES));
|
|
53
61
|
}
|
|
54
62
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
|
|
63
|
+
* A cache with no `max` reports Infinity (lru-cache@5's default for a missing one), so
|
|
64
|
+
* the capacity gauge publishes the sentinel the catalog documents instead.
|
|
65
|
+
*/
|
|
66
|
+
function capacityOrSentinel(max) {
|
|
67
|
+
return Number.isFinite(max) ? max : Number.MAX_SAFE_INTEGER;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The cache's miss count. A cache that reports misses directly wins: deriving them from
|
|
71
|
+
* `total - hits` reads as a decrease while a read is in flight across a collection cycle,
|
|
72
|
+
* and the SDK turns a decrease on a monotonic counter into a reset.
|
|
73
|
+
*/
|
|
74
|
+
function missesOf(stats) {
|
|
75
|
+
var _a, _b;
|
|
76
|
+
if (typeof stats.misses === 'number') {
|
|
77
|
+
return stats.misses;
|
|
78
|
+
}
|
|
79
|
+
return Math.max(((_a = stats.total) !== null && _a !== void 0 ? _a : 0) - ((_b = stats.hits) !== null && _b !== void 0 ? _b : 0), 0);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* A disposer that only releases the entry it was created for. A later registration of
|
|
83
|
+
* the same name owns that entry, so an older disposer is a no-op instead of tearing
|
|
84
|
+
* down its successor.
|
|
85
|
+
*/
|
|
86
|
+
function releaseIfOwner(registry, name, entry, onRelease) {
|
|
87
|
+
return () => {
|
|
88
|
+
if (registry.get(name) !== entry) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
registry.delete(name);
|
|
92
|
+
if (onRelease) {
|
|
93
|
+
onRelease();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Applies the same cardinality limit the push methods use to whatever an observable
|
|
99
|
+
* callback reports. Observables run outside any request, so every attribute is custom.
|
|
58
100
|
*/
|
|
59
101
|
function limitObservableResult(result) {
|
|
60
102
|
return {
|
|
@@ -132,10 +174,11 @@ class DiagnosticsMetrics {
|
|
|
132
174
|
return this.clientInitPromise;
|
|
133
175
|
}
|
|
134
176
|
this.clientInitPromise = (async () => {
|
|
177
|
+
let timeoutId;
|
|
178
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
179
|
+
timeoutId = setTimeout(() => reject(new Error('Metric client initialization timeout')), constants_1.METRIC_CLIENT_INIT_TIMEOUT_MS);
|
|
180
|
+
});
|
|
135
181
|
try {
|
|
136
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
137
|
-
setTimeout(() => reject(new Error('Metric client initialization timeout')), constants_1.METRIC_CLIENT_INIT_TIMEOUT_MS);
|
|
138
|
-
});
|
|
139
182
|
this.metricsClient = await Promise.race([
|
|
140
183
|
(0, client_1.getMetricClient)(),
|
|
141
184
|
timeoutPromise
|
|
@@ -143,17 +186,39 @@ class DiagnosticsMetrics {
|
|
|
143
186
|
// Create the single latency histogram after client is ready
|
|
144
187
|
this.createLatencyHistogram();
|
|
145
188
|
// Attach any observable registrations made before the client was ready.
|
|
146
|
-
|
|
147
|
-
|
|
189
|
+
try {
|
|
190
|
+
this.flushPendingObservables();
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
// The client is up; only the pending registrations failed to attach.
|
|
194
|
+
console.error('Failed to attach the pending observable registrations:', error);
|
|
195
|
+
}
|
|
148
196
|
return this.metricsClient;
|
|
149
197
|
}
|
|
150
198
|
catch (error) {
|
|
151
199
|
console.error('Failed to initialize metric client:', error);
|
|
200
|
+
this.warnPendingRegistrations();
|
|
152
201
|
return undefined;
|
|
153
202
|
}
|
|
203
|
+
finally {
|
|
204
|
+
// Promise.race leaves the loser running, so the timeout timer needs clearing.
|
|
205
|
+
if (timeoutId) {
|
|
206
|
+
clearTimeout(timeoutId);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
154
209
|
})();
|
|
155
210
|
return this.clientInitPromise;
|
|
156
211
|
}
|
|
212
|
+
// Registrations that never attach should be reported once, not left for an app to
|
|
213
|
+
// discover through a dashboard with missing series.
|
|
214
|
+
warnPendingRegistrations() {
|
|
215
|
+
const pending = this.cacheRegistry.size +
|
|
216
|
+
this.observableRegistrations.gauge.size +
|
|
217
|
+
this.observableRegistrations.counter.size;
|
|
218
|
+
if (pending > 0) {
|
|
219
|
+
console.warn(`DiagnosticsMetrics: metric client unavailable; ${pending} pending registration(s) are not being published.`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
157
222
|
/**
|
|
158
223
|
* Create the single shared histogram for all latency measurements.
|
|
159
224
|
* Called after metric client is initialized.
|
|
@@ -246,20 +311,53 @@ class DiagnosticsMetrics {
|
|
|
246
311
|
// Merge: base attributes + non-conflicting custom attributes
|
|
247
312
|
return { ...baseAttributes, ...nonConflictingCustomAttributes };
|
|
248
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* The reason `name` cannot carry a new instrument, or undefined when it can. The SDK
|
|
316
|
+
* accepts two same-named streams silently and the collector then rejects them, so a name
|
|
317
|
+
* is owned by whichever instrument took it first: reusing it through the other API, or
|
|
318
|
+
* under the other kind, is a conflict. Reusing it in place — same API, same kind — is a
|
|
319
|
+
* replace, not a conflict.
|
|
320
|
+
*
|
|
321
|
+
* Disposing leaves a gap: remove() only detaches the callback, so the meter keeps the
|
|
322
|
+
* instrument and this check stops seeing the name. Reusing it under the other kind from
|
|
323
|
+
* then on would duplicate the stream, which only bites under cumulative temporality (a
|
|
324
|
+
* delta export drops the orphan). Re-registering the same kind after dispose is safe.
|
|
325
|
+
*/
|
|
326
|
+
nameConflict(name, kind, origin) {
|
|
327
|
+
if (CACHE_METRIC_NAMES.has(name)) {
|
|
328
|
+
return 'reserved for trackCache()';
|
|
329
|
+
}
|
|
330
|
+
const other = kind === 'gauge' ? 'counter' : 'gauge';
|
|
331
|
+
const foreign = origin === 'push'
|
|
332
|
+
? [this.observableRegistrations[kind], this.observableRegistrations[other], this.pushInstruments(other)]
|
|
333
|
+
: [this.pushInstruments(kind), this.pushInstruments(other), this.observableRegistrations[other]];
|
|
334
|
+
return foreign.some(instruments => instruments.has(name)) ? 'already used by another instrument' : undefined;
|
|
335
|
+
}
|
|
336
|
+
pushInstruments(kind) {
|
|
337
|
+
return kind === 'counter' ? this.counters : this.gauges;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Counter and gauge instruments are created once per name and reused.
|
|
341
|
+
*/
|
|
342
|
+
instrumentFor(map, name, create) {
|
|
343
|
+
const existing = map.get(name);
|
|
344
|
+
if (existing !== undefined) {
|
|
345
|
+
return existing;
|
|
346
|
+
}
|
|
347
|
+
const instrument = create();
|
|
348
|
+
map.set(name, instrument);
|
|
349
|
+
return instrument;
|
|
350
|
+
}
|
|
249
351
|
/**
|
|
250
352
|
* Record a latency measurement using the single shared histogram.
|
|
251
353
|
* Accepts either an hrtime tuple from process.hrtime() or milliseconds as a number.
|
|
252
354
|
* Use attributes to differentiate between different operations.
|
|
253
355
|
*
|
|
254
|
-
* Base attributes from
|
|
255
|
-
*
|
|
256
|
-
* precedence - if a custom attribute key conflicts with a base attribute key,
|
|
257
|
-
* the custom attribute is silently dropped.
|
|
258
|
-
*
|
|
259
|
-
* Custom attributes are limited to MAX_CUSTOM_ATTRIBUTES (5). Base attributes are not limited.
|
|
356
|
+
* Base attributes from `runWithBaseAttributes` are merged in automatically and take
|
|
357
|
+
* precedence; custom attributes are capped at MAX_CUSTOM_ATTRIBUTES (7). See the class doc.
|
|
260
358
|
*
|
|
261
359
|
* @param value Either [seconds, nanoseconds] from process.hrtime() or milliseconds
|
|
262
|
-
* @param attributes Custom attributes including 'operation' to identify the operation type (max
|
|
360
|
+
* @param attributes Custom attributes including 'operation' to identify the operation type (max 7 custom attributes)
|
|
263
361
|
*
|
|
264
362
|
* @example
|
|
265
363
|
* ```typescript
|
|
@@ -287,16 +385,12 @@ class DiagnosticsMetrics {
|
|
|
287
385
|
* Increment a counter by a specific value.
|
|
288
386
|
* Multiple counters are stored by name since counters represent different types of events.
|
|
289
387
|
*
|
|
290
|
-
* Base attributes from
|
|
291
|
-
*
|
|
292
|
-
* precedence - if a custom attribute key conflicts with a base attribute key,
|
|
293
|
-
* the custom attribute is silently dropped.
|
|
294
|
-
*
|
|
295
|
-
* Custom attributes are limited to MAX_CUSTOM_ATTRIBUTES (5). Base attributes are not limited.
|
|
388
|
+
* Base attributes from `runWithBaseAttributes` are merged in automatically and take
|
|
389
|
+
* precedence; custom attributes are capped at MAX_CUSTOM_ATTRIBUTES (7). See the class doc.
|
|
296
390
|
*
|
|
297
391
|
* @param name Counter name (e.g., 'http_requests_total', 'cache_hits_total')
|
|
298
392
|
* @param value Amount to increment by (typically 1)
|
|
299
|
-
* @param attributes Optional custom attributes for the counter (max
|
|
393
|
+
* @param attributes Optional custom attributes for the counter (max 7 custom attributes, e.g., { method: 'GET', status: '2xx' })
|
|
300
394
|
*
|
|
301
395
|
* @example
|
|
302
396
|
* ```typescript
|
|
@@ -308,33 +402,25 @@ class DiagnosticsMetrics {
|
|
|
308
402
|
console.warn('DiagnosticsMetrics not initialized. Call initialize() first.');
|
|
309
403
|
return;
|
|
310
404
|
}
|
|
311
|
-
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
unit: '1',
|
|
316
|
-
});
|
|
317
|
-
this.counters.set(name, counter);
|
|
405
|
+
const conflict = this.nameConflict(name, 'counter', 'push');
|
|
406
|
+
if (conflict) {
|
|
407
|
+
console.error(`DiagnosticsMetrics: '${name}' is ${conflict}; ignoring the counter. Pick a distinct metric name.`);
|
|
408
|
+
return;
|
|
318
409
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
this.counters.get(name).add(value, mergedAttributes);
|
|
410
|
+
const counter = this.instrumentFor(this.counters, name, () => this.metricsClient.createCounter(name, { description: `Counter for ${name}`, unit: '1' }));
|
|
411
|
+
// mergeAttributes() limits custom attributes internally
|
|
412
|
+
counter.add(value, this.mergeAttributes(attributes));
|
|
323
413
|
}
|
|
324
414
|
/**
|
|
325
415
|
* Set a gauge to a specific value (current state).
|
|
326
416
|
* Multiple gauges are stored by name since gauges represent different types of measurements.
|
|
327
417
|
*
|
|
328
|
-
* Base attributes from
|
|
329
|
-
*
|
|
330
|
-
* precedence - if a custom attribute key conflicts with a base attribute key,
|
|
331
|
-
* the custom attribute is silently dropped.
|
|
332
|
-
*
|
|
333
|
-
* Custom attributes are limited to MAX_CUSTOM_ATTRIBUTES (5). Base attributes are not limited.
|
|
418
|
+
* Base attributes from `runWithBaseAttributes` are merged in automatically and take
|
|
419
|
+
* precedence; custom attributes are capped at MAX_CUSTOM_ATTRIBUTES (7). See the class doc.
|
|
334
420
|
*
|
|
335
421
|
* @param name Gauge name (e.g., 'cache_items_current', 'memory_usage_bytes')
|
|
336
422
|
* @param value Current value
|
|
337
|
-
* @param attributes Optional custom attributes for the gauge (max
|
|
423
|
+
* @param attributes Optional custom attributes for the gauge (max 7 custom attributes, e.g., { cache: 'pages' })
|
|
338
424
|
*
|
|
339
425
|
* @example
|
|
340
426
|
* ```typescript
|
|
@@ -346,23 +432,18 @@ class DiagnosticsMetrics {
|
|
|
346
432
|
console.warn('DiagnosticsMetrics not initialized. Call initialize() first.');
|
|
347
433
|
return;
|
|
348
434
|
}
|
|
349
|
-
|
|
350
|
-
if (
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
unit: '1',
|
|
354
|
-
});
|
|
355
|
-
this.gauges.set(name, gauge);
|
|
435
|
+
const conflict = this.nameConflict(name, 'gauge', 'push');
|
|
436
|
+
if (conflict) {
|
|
437
|
+
console.error(`DiagnosticsMetrics: '${name}' is ${conflict}; ignoring the gauge. Pick a distinct metric name.`);
|
|
438
|
+
return;
|
|
356
439
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
this.gauges.get(name).set(value, mergedAttributes);
|
|
440
|
+
const gauge = this.instrumentFor(this.gauges, name, () => this.metricsClient.createGauge(name, { description: `Gauge for ${name}`, unit: '1' }));
|
|
441
|
+
// mergeAttributes() limits custom attributes internally
|
|
442
|
+
gauge.set(value, this.mergeAttributes(attributes));
|
|
361
443
|
}
|
|
362
|
-
// Registers (or replaces) a pull-based gauge: OTel calls `observe` on its own
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
// held to the same MAX_CUSTOM_ATTRIBUTES limit as the push-based methods.
|
|
444
|
+
// Registers (or replaces) a pull-based gauge: OTel calls `observe` on its own schedule
|
|
445
|
+
// with `result.observe(value, attributes?)`. No base-attribute merging (no request is
|
|
446
|
+
// in progress), but reported attributes keep the MAX_CUSTOM_ATTRIBUTES limit.
|
|
366
447
|
registerObservableGauge(name, observe, options) {
|
|
367
448
|
return this.registerObservable('gauge', name, observe, options);
|
|
368
449
|
}
|
|
@@ -372,38 +453,24 @@ class DiagnosticsMetrics {
|
|
|
372
453
|
return this.registerObservable('counter', name, observe, options);
|
|
373
454
|
}
|
|
374
455
|
registerObservable(kind, name, observe, options) {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
if (this.observableRegistrations[otherKind].has(name)) {
|
|
380
|
-
console.error(`DiagnosticsMetrics: '${name}' is already registered as an observable ${otherKind}; ` +
|
|
381
|
-
`ignoring the ${kind} registration. Pick a distinct metric name.`);
|
|
456
|
+
const conflict = this.nameConflict(name, kind, 'observable');
|
|
457
|
+
if (conflict) {
|
|
458
|
+
console.error(`DiagnosticsMetrics: '${name}' is ${conflict}; ignoring the ${kind} registration. ` +
|
|
459
|
+
`Pick a distinct metric name.`);
|
|
382
460
|
return () => undefined;
|
|
383
461
|
}
|
|
384
|
-
|
|
462
|
+
const registration = { observe, options };
|
|
463
|
+
this.observableRegistrations[kind].set(name, registration);
|
|
385
464
|
this.syncObservables(kind);
|
|
386
|
-
return () => {
|
|
387
|
-
var _a
|
|
388
|
-
|
|
389
|
-
// this one must not tear that down.
|
|
390
|
-
if (((_a = this.observableRegistrations[kind].get(name)) === null || _a === void 0 ? void 0 : _a.observe) !== observe) {
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
this.observableRegistrations[kind].delete(name);
|
|
394
|
-
(_b = this.observableInstruments[kind].get(name)) === null || _b === void 0 ? void 0 : _b.remove();
|
|
465
|
+
return releaseIfOwner(this.observableRegistrations[kind], name, registration, () => {
|
|
466
|
+
var _a;
|
|
467
|
+
(_a = this.observableInstruments[kind].get(name)) === null || _a === void 0 ? void 0 : _a.remove();
|
|
395
468
|
this.observableInstruments[kind].delete(name);
|
|
396
|
-
};
|
|
469
|
+
});
|
|
397
470
|
}
|
|
398
|
-
// Attaches every `kind` registration to its instrument
|
|
399
|
-
// registered or the client
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
// One instrument per name, carrying one callback for its whole life: the callback
|
|
403
|
-
// is a trampoline that reads whatever registration is current, so replacing the
|
|
404
|
-
// app's function is a map update and never touches the SDK. Re-creating the
|
|
405
|
-
// instrument instead would leave the meter holding a stream the collector sees as
|
|
406
|
-
// a duplicate type for the same name.
|
|
471
|
+
// Attaches every `kind` registration to its instrument; a no-op when there is nothing
|
|
472
|
+
// registered or the client is not ready. One instrument per name, with a callback that
|
|
473
|
+
// trampolines over the current registration — re-creating it would duplicate the stream.
|
|
407
474
|
syncObservables(kind) {
|
|
408
475
|
if (this.observableRegistrations[kind].size === 0) {
|
|
409
476
|
return; // skip the meter entirely if this kind is unused
|
|
@@ -414,7 +481,7 @@ class DiagnosticsMetrics {
|
|
|
414
481
|
}
|
|
415
482
|
for (const [name, { options }] of this.observableRegistrations[kind]) {
|
|
416
483
|
if (this.observableInstruments[kind].has(name)) {
|
|
417
|
-
continue;
|
|
484
|
+
continue; // already attached; options apply only on the first registration
|
|
418
485
|
}
|
|
419
486
|
const attached = (result) => {
|
|
420
487
|
var _a;
|
|
@@ -435,15 +502,19 @@ class DiagnosticsMetrics {
|
|
|
435
502
|
this.ensureCacheInstruments();
|
|
436
503
|
}
|
|
437
504
|
}
|
|
438
|
-
// Replacement for the legacy MetricsAccumulator.trackCache()
|
|
439
|
-
//
|
|
440
|
-
// has no side effects, so this can run alongside the legacy trackCache().
|
|
505
|
+
// Replacement for the legacy MetricsAccumulator.trackCache() over the same cache
|
|
506
|
+
// instances; reads getCumulativeStats(), which has no side effects. See METRICS_CATALOG.md.
|
|
441
507
|
trackCache(name, cacheInstance) {
|
|
442
|
-
|
|
508
|
+
// TrackedCache is a compile-time contract only: a JS caller, or a cache built by
|
|
509
|
+
// another copy of @vtex/api, would otherwise throw on every collection cycle.
|
|
510
|
+
if (typeof (cacheInstance === null || cacheInstance === void 0 ? void 0 : cacheInstance.getCumulativeStats) !== 'function') {
|
|
511
|
+
console.error(`DiagnosticsMetrics: '${name}' does not implement getCumulativeStats(); ignoring the cache registration.`);
|
|
512
|
+
return () => undefined;
|
|
513
|
+
}
|
|
514
|
+
const registration = { cache: cacheInstance };
|
|
515
|
+
this.cacheRegistry.set(name, registration);
|
|
443
516
|
this.ensureCacheInstruments();
|
|
444
|
-
return (
|
|
445
|
-
this.cacheRegistry.delete(name);
|
|
446
|
-
};
|
|
517
|
+
return releaseIfOwner(this.cacheRegistry, name, registration);
|
|
447
518
|
}
|
|
448
519
|
ensureCacheInstruments() {
|
|
449
520
|
if (this.cacheInstruments) {
|
|
@@ -473,10 +544,10 @@ class DiagnosticsMetrics {
|
|
|
473
544
|
// These live for the process, so only the handles are kept: the wrappers exist
|
|
474
545
|
// to attach the batch callback above.
|
|
475
546
|
this.cacheInstruments = {
|
|
476
|
-
operations: operations.instrument,
|
|
477
|
-
items: items.instrument,
|
|
478
547
|
capacity: capacity.instrument,
|
|
479
548
|
disposed: disposed.instrument,
|
|
549
|
+
items: items.instrument,
|
|
550
|
+
operations: operations.instrument,
|
|
480
551
|
};
|
|
481
552
|
}
|
|
482
553
|
// Reads each registered cache's cumulative counters straight into the instruments.
|
|
@@ -485,7 +556,7 @@ class DiagnosticsMetrics {
|
|
|
485
556
|
return;
|
|
486
557
|
}
|
|
487
558
|
const { operations, items, capacity, disposed } = this.cacheInstruments;
|
|
488
|
-
for (const [name, cache] of this.cacheRegistry) {
|
|
559
|
+
for (const [name, { cache }] of this.cacheRegistry) {
|
|
489
560
|
let stats;
|
|
490
561
|
try {
|
|
491
562
|
stats = cache.getCumulativeStats();
|
|
@@ -497,13 +568,13 @@ class DiagnosticsMetrics {
|
|
|
497
568
|
const attributes = { cache: name };
|
|
498
569
|
if (typeof stats.hits === 'number' && typeof stats.total === 'number') {
|
|
499
570
|
result.observe(operations, stats.hits, { ...attributes, cache_state: 'hit' });
|
|
500
|
-
result.observe(operations,
|
|
571
|
+
result.observe(operations, missesOf(stats), { ...attributes, cache_state: 'miss' });
|
|
501
572
|
}
|
|
502
573
|
if (typeof stats.itemCount === 'number') {
|
|
503
574
|
result.observe(items, stats.itemCount, attributes);
|
|
504
575
|
}
|
|
505
576
|
if (typeof stats.max === 'number') {
|
|
506
|
-
result.observe(capacity, stats.max, attributes);
|
|
577
|
+
result.observe(capacity, capacityOrSentinel(stats.max), attributes);
|
|
507
578
|
}
|
|
508
579
|
if (typeof stats.disposedItems === 'number') {
|
|
509
580
|
result.observe(disposed, stats.disposedItems, attributes);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vtex/api",
|
|
3
|
-
"version": "7.5.1-beta.
|
|
3
|
+
"version": "7.5.1-beta.2",
|
|
4
4
|
"description": "VTEX I/O API client",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"@opentelemetry/instrumentation-koa": "0.47.1",
|
|
54
54
|
"@types/koa": "^2.11.0",
|
|
55
55
|
"@types/koa-compose": "^3.2.3",
|
|
56
|
-
"@vtex/diagnostics-nodejs": "0.
|
|
57
|
-
"@vtex/diagnostics-semconv": "5.5.
|
|
56
|
+
"@vtex/diagnostics-nodejs": "0.2.0-io",
|
|
57
|
+
"@vtex/diagnostics-semconv": "5.5.3",
|
|
58
58
|
"@vtex/node-error-report": "^0.0.3",
|
|
59
59
|
"@wry/equality": "^0.1.9",
|
|
60
60
|
"agentkeepalive": "^4.0.2",
|