@vtex/api 7.5.0 → 7.5.1-beta.1
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 +13 -0
- package/lib/caches/DiskCache.d.ts +3 -3
- package/lib/caches/DiskCache.js +13 -14
- package/lib/caches/LRUCache.d.ts +3 -4
- package/lib/caches/LRUCache.js +26 -15
- package/lib/caches/LRUDiskCache.d.ts +3 -4
- package/lib/caches/LRUDiskCache.js +28 -20
- package/lib/caches/MultilayeredCache.d.ts +3 -4
- package/lib/caches/MultilayeredCache.js +14 -14
- package/lib/caches/WindowedCounters.d.ts +35 -0
- package/lib/caches/WindowedCounters.js +45 -0
- package/lib/caches/typings.d.ts +9 -0
- package/lib/metrics/DiagnosticsMetrics.d.ts +41 -24
- package/lib/metrics/DiagnosticsMetrics.js +253 -44
- package/package.json +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ 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.1]
|
|
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
|
+
|
|
8
21
|
## [7.5.0]
|
|
9
22
|
|
|
10
23
|
### Added
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { CacheLayer } from './CacheLayer';
|
|
2
|
-
import { DiskStats } from './typings';
|
|
2
|
+
import { CumulativeStats, DiskStats } from './typings';
|
|
3
3
|
import { outputJSON, readJSON } from 'fs-extra';
|
|
4
4
|
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
10
|
constructor(cachePath: string, readFile?: typeof readJSON, writeFile?: typeof outputJSON);
|
|
12
11
|
has: (key: string) => boolean;
|
|
13
12
|
getStats: (name?: string) => DiskStats;
|
|
13
|
+
getCumulativeStats: () => CumulativeStats;
|
|
14
14
|
get: (key: string) => Promise<V | void>;
|
|
15
15
|
set: (key: string, value: V) => Promise<boolean>;
|
|
16
16
|
private getPathKey;
|
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,35 +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;
|
|
16
|
+
this.counters = new WindowedCounters_1.WindowedCounters();
|
|
17
17
|
this.has = (key) => {
|
|
18
18
|
const pathKey = this.getPathKey(key);
|
|
19
19
|
return (0, fs_extra_1.pathExistsSync)(pathKey);
|
|
20
20
|
};
|
|
21
21
|
this.getStats = (name = 'disk-cache') => {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
this.total = 0;
|
|
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
29
|
this.get = async (key) => {
|
|
32
30
|
const pathKey = this.getPathKey(key);
|
|
33
|
-
this.
|
|
31
|
+
this.counters.countRead();
|
|
34
32
|
const data = await new Promise(resolve => {
|
|
35
33
|
this.lock.readLock(key, async (release) => {
|
|
36
34
|
try {
|
|
37
35
|
const fileData = await this.readFile(pathKey);
|
|
38
36
|
release();
|
|
39
|
-
this.
|
|
37
|
+
this.counters.countHit();
|
|
40
38
|
resolve(fileData);
|
|
41
39
|
}
|
|
42
|
-
catch
|
|
40
|
+
catch {
|
|
43
41
|
release();
|
|
42
|
+
this.counters.countMiss(); // a missing or unreadable file is a miss
|
|
44
43
|
resolve(null);
|
|
45
44
|
}
|
|
46
45
|
});
|
|
@@ -56,7 +55,7 @@ class DiskCache {
|
|
|
56
55
|
release();
|
|
57
56
|
resolve(writePromise);
|
|
58
57
|
}
|
|
59
|
-
catch
|
|
58
|
+
catch {
|
|
60
59
|
release();
|
|
61
60
|
resolve(true);
|
|
62
61
|
}
|
package/lib/caches/LRUCache.d.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import LRU from 'lru-cache';
|
|
2
2
|
import { CacheLayer } from './CacheLayer';
|
|
3
|
-
import { FetchResult, LRUStats } from './typings';
|
|
3
|
+
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;
|
|
7
|
+
private readonly counters;
|
|
10
8
|
constructor(options: LRU.Options<K, V>);
|
|
11
9
|
get: (key: K) => V | void;
|
|
12
10
|
getOrSet: (key: K, fetcher?: () => Promise<FetchResult<V>>) => Promise<V | void>;
|
|
13
11
|
set: (key: K, value: V, maxAge?: number) => boolean;
|
|
14
12
|
has: (key: K) => boolean;
|
|
15
13
|
getStats: (name?: string) => LRUStats;
|
|
14
|
+
getCumulativeStats: () => CumulativeStats;
|
|
16
15
|
}
|
package/lib/caches/LRUCache.js
CHANGED
|
@@ -6,41 +6,52 @@ 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
|
-
hits:
|
|
27
|
+
const { disposed, hits, total } = this.counters.windowed();
|
|
28
|
+
return {
|
|
29
|
+
disposedItems: disposed,
|
|
30
|
+
hitRate: total > 0 ? hits / total : undefined,
|
|
31
|
+
hits,
|
|
27
32
|
itemCount: this.storage.itemCount,
|
|
28
33
|
length: this.storage.length,
|
|
29
34
|
max: this.storage.max,
|
|
30
35
|
name,
|
|
31
|
-
total
|
|
36
|
+
total,
|
|
37
|
+
};
|
|
38
|
+
};
|
|
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,
|
|
32
49
|
};
|
|
33
|
-
this.hits = 0;
|
|
34
|
-
this.total = 0;
|
|
35
|
-
this.disposed = 0;
|
|
36
|
-
return stats;
|
|
37
50
|
};
|
|
38
|
-
this.
|
|
39
|
-
this.total = 0;
|
|
40
|
-
this.disposed = 0;
|
|
51
|
+
this.counters = new WindowedCounters_1.WindowedCounters();
|
|
41
52
|
this.storage = new lru_cache_1.default({
|
|
42
53
|
...options,
|
|
43
|
-
dispose: () => this.
|
|
54
|
+
dispose: () => this.counters.countDisposed(),
|
|
44
55
|
noDisposeOnSet: true,
|
|
45
56
|
});
|
|
46
57
|
this.multilayer = new MultilayeredCache_1.MultilayeredCache([this]);
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { CacheLayer } from './CacheLayer';
|
|
2
|
-
import { LRUDiskCacheOptions, LRUStats } from './typings';
|
|
2
|
+
import { CumulativeStats, LRUDiskCacheOptions, LRUStats } from './typings';
|
|
3
3
|
import { outputJSON, readJSON } from 'fs-extra';
|
|
4
4
|
export declare class LRUDiskCache<V> implements CacheLayer<string, V> {
|
|
5
5
|
private cachePath;
|
|
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
12
|
constructor(cachePath: string, options: LRUDiskCacheOptions, readFile?: typeof readJSON, writeFile?: typeof outputJSON);
|
|
15
13
|
has: (key: string) => boolean;
|
|
16
14
|
getStats: (name?: string) => LRUStats;
|
|
15
|
+
getCumulativeStats: () => CumulativeStats;
|
|
17
16
|
get: (key: string) => Promise<V | void>;
|
|
18
17
|
set: (key: string, value: V, maxAge?: number) => Promise<boolean>;
|
|
19
18
|
private getPathKey;
|
|
@@ -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,29 +14,37 @@ 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
17
|
this.has = (key) => this.lruStorage.has(key);
|
|
19
18
|
this.getStats = (name = 'disk-lru-cache') => {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
hits:
|
|
19
|
+
const { disposed, hits, total } = this.counters.windowed();
|
|
20
|
+
return {
|
|
21
|
+
disposedItems: disposed,
|
|
22
|
+
hitRate: total > 0 ? hits / total : undefined,
|
|
23
|
+
hits,
|
|
24
24
|
itemCount: this.lruStorage.itemCount,
|
|
25
25
|
length: this.lruStorage.length,
|
|
26
26
|
max: this.lruStorage.max,
|
|
27
27
|
name,
|
|
28
|
-
total
|
|
28
|
+
total,
|
|
29
|
+
};
|
|
30
|
+
};
|
|
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,
|
|
29
41
|
};
|
|
30
|
-
this.hits = 0;
|
|
31
|
-
this.total = 0;
|
|
32
|
-
this.disposed = 0;
|
|
33
|
-
return stats;
|
|
34
42
|
};
|
|
35
43
|
this.get = async (key) => {
|
|
36
44
|
const timeOfDeath = this.lruStorage.get(key);
|
|
37
|
-
this.
|
|
45
|
+
this.counters.countRead();
|
|
38
46
|
if (timeOfDeath === undefined) {
|
|
47
|
+
this.counters.countMiss();
|
|
39
48
|
// if it is an outdated file when stale=false
|
|
40
49
|
if (this.keyToBeDeleted) {
|
|
41
50
|
await this.deleteFile(key);
|
|
@@ -48,11 +57,12 @@ class LRUDiskCache {
|
|
|
48
57
|
try {
|
|
49
58
|
const fileData = await this.readFile(pathKey);
|
|
50
59
|
release();
|
|
51
|
-
this.
|
|
60
|
+
this.counters.countHit();
|
|
52
61
|
resolve(fileData);
|
|
53
62
|
}
|
|
54
|
-
catch
|
|
63
|
+
catch {
|
|
55
64
|
release();
|
|
65
|
+
this.counters.countMiss(); // a missing or unreadable file is a miss
|
|
56
66
|
resolve(null);
|
|
57
67
|
}
|
|
58
68
|
});
|
|
@@ -84,7 +94,7 @@ class LRUDiskCache {
|
|
|
84
94
|
release();
|
|
85
95
|
resolve(writePromise);
|
|
86
96
|
}
|
|
87
|
-
catch
|
|
97
|
+
catch {
|
|
88
98
|
release();
|
|
89
99
|
resolve(true);
|
|
90
100
|
}
|
|
@@ -105,7 +115,7 @@ class LRUDiskCache {
|
|
|
105
115
|
release();
|
|
106
116
|
resolve(removePromise);
|
|
107
117
|
}
|
|
108
|
-
catch
|
|
118
|
+
catch {
|
|
109
119
|
release();
|
|
110
120
|
resolve(true);
|
|
111
121
|
}
|
|
@@ -113,14 +123,12 @@ class LRUDiskCache {
|
|
|
113
123
|
});
|
|
114
124
|
return !failure;
|
|
115
125
|
};
|
|
116
|
-
this.
|
|
117
|
-
this.total = 0;
|
|
118
|
-
this.disposed = 0;
|
|
126
|
+
this.counters = new WindowedCounters_1.WindowedCounters();
|
|
119
127
|
this.keyToBeDeleted = '';
|
|
120
128
|
this.lock = new rwlock_1.default();
|
|
121
129
|
const dispose = (key) => {
|
|
122
130
|
this.keyToBeDeleted = key;
|
|
123
|
-
this.
|
|
131
|
+
this.counters.countDisposed();
|
|
124
132
|
};
|
|
125
133
|
const lruOptions = {
|
|
126
134
|
...options,
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { CacheLayer } from './CacheLayer';
|
|
2
|
-
import { FetchResult, MultilayerStats } from './typings';
|
|
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;
|
|
5
|
+
private readonly counters;
|
|
7
6
|
constructor(caches: Array<CacheLayer<K, V>>);
|
|
8
7
|
get: (key: K, fetcher?: () => Promise<FetchResult<V>>) => Promise<V | void>;
|
|
9
8
|
set: (key: K, value: V, maxAge?: number) => Promise<boolean>;
|
|
10
9
|
has: (key: K) => Promise<boolean>;
|
|
11
10
|
getStats: (name?: string) => MultilayerStats;
|
|
11
|
+
getCumulativeStats: () => CumulativeStats;
|
|
12
12
|
private findIndex;
|
|
13
|
-
private resetCounters;
|
|
14
13
|
}
|
|
@@ -2,11 +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;
|
|
9
|
+
this.counters = new WindowedCounters_1.WindowedCounters();
|
|
10
10
|
this.get = async (key, fetcher) => {
|
|
11
11
|
let value = undefined;
|
|
12
12
|
let maxAge;
|
|
@@ -40,30 +40,30 @@ class MultilayeredCache {
|
|
|
40
40
|
return (0, ramda_1.any)(item => item, hasList);
|
|
41
41
|
};
|
|
42
42
|
this.getStats = (name = 'multilayred-cache') => {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
hits:
|
|
43
|
+
const { hits, total } = this.counters.windowed();
|
|
44
|
+
return {
|
|
45
|
+
hitRate: total > 0 ? hits / total : undefined,
|
|
46
|
+
hits,
|
|
46
47
|
name,
|
|
47
|
-
total
|
|
48
|
+
total,
|
|
48
49
|
};
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
};
|
|
51
|
+
this.getCumulativeStats = () => {
|
|
52
|
+
const { hits, misses, total } = this.counters.cumulative();
|
|
53
|
+
return { hits, misses, total };
|
|
51
54
|
};
|
|
52
55
|
this.findIndex = async (func, array) => {
|
|
53
|
-
this.
|
|
56
|
+
this.counters.countRead();
|
|
54
57
|
for (let index = 0; index < array.length; index++) {
|
|
55
58
|
const hasKey = await func(array[index]);
|
|
56
59
|
if (hasKey) {
|
|
57
|
-
this.
|
|
60
|
+
this.counters.countHit();
|
|
58
61
|
return index;
|
|
59
62
|
}
|
|
60
63
|
}
|
|
64
|
+
this.counters.countMiss();
|
|
61
65
|
return -1;
|
|
62
66
|
};
|
|
63
67
|
}
|
|
64
|
-
resetCounters() {
|
|
65
|
-
this.hits = 0;
|
|
66
|
-
this.total = 0;
|
|
67
|
-
}
|
|
68
68
|
}
|
|
69
69
|
exports.MultilayeredCache = MultilayeredCache;
|
|
@@ -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
|
@@ -17,6 +17,15 @@ export type LRUStats = {
|
|
|
17
17
|
name: string;
|
|
18
18
|
total: number;
|
|
19
19
|
};
|
|
20
|
+
export type CumulativeStats = {
|
|
21
|
+
hits?: number;
|
|
22
|
+
total?: number;
|
|
23
|
+
misses?: number;
|
|
24
|
+
disposedItems?: number;
|
|
25
|
+
itemCount?: number;
|
|
26
|
+
length?: number;
|
|
27
|
+
max?: number;
|
|
28
|
+
};
|
|
20
29
|
export type MultilayerStats = {
|
|
21
30
|
hitRate: number | undefined;
|
|
22
31
|
hits: number;
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import { Attributes } from '@opentelemetry/api';
|
|
1
|
+
import { Attributes, MetricOptions, ObservableCallback } from '@opentelemetry/api';
|
|
2
|
+
import { CumulativeStats } from '../caches/typings';
|
|
3
|
+
export interface TrackedCache {
|
|
4
|
+
getCumulativeStats(): CumulativeStats;
|
|
5
|
+
}
|
|
2
6
|
/**
|
|
3
7
|
* DiagnosticsMetrics provides a high-level API for recording metrics using
|
|
4
8
|
* the @vtex/diagnostics-nodejs library. It completely abstracts instrument
|
|
@@ -56,8 +60,12 @@ export declare class DiagnosticsMetrics {
|
|
|
56
60
|
private metricsClient;
|
|
57
61
|
private clientInitPromise;
|
|
58
62
|
private latencyHistogram;
|
|
59
|
-
private counters;
|
|
60
|
-
private gauges;
|
|
63
|
+
private readonly counters;
|
|
64
|
+
private readonly gauges;
|
|
65
|
+
private readonly observableRegistrations;
|
|
66
|
+
private readonly observableInstruments;
|
|
67
|
+
private readonly cacheRegistry;
|
|
68
|
+
private cacheInstruments;
|
|
61
69
|
constructor();
|
|
62
70
|
/**
|
|
63
71
|
* Initialize the metrics client with timeout handling.
|
|
@@ -118,20 +126,29 @@ export declare class DiagnosticsMetrics {
|
|
|
118
126
|
* @returns Merged attributes (base + non-conflicting limited custom) or undefined if both are empty
|
|
119
127
|
*/
|
|
120
128
|
private mergeAttributes;
|
|
129
|
+
/**
|
|
130
|
+
* The reason `name` cannot carry a new instrument, or undefined when it can. The SDK
|
|
131
|
+
* accepts two same-named streams silently and the collector then rejects them, so a name
|
|
132
|
+
* is owned by whichever instrument took it first: reusing it through the other API, or
|
|
133
|
+
* under the other kind, is a conflict. Reusing it in place — same API, same kind — is a
|
|
134
|
+
* replace, not a conflict.
|
|
135
|
+
*/
|
|
136
|
+
private nameConflict;
|
|
137
|
+
private pushInstruments;
|
|
138
|
+
/**
|
|
139
|
+
* Counter and gauge instruments are created once per name and reused.
|
|
140
|
+
*/
|
|
141
|
+
private instrumentFor;
|
|
121
142
|
/**
|
|
122
143
|
* Record a latency measurement using the single shared histogram.
|
|
123
144
|
* Accepts either an hrtime tuple from process.hrtime() or milliseconds as a number.
|
|
124
145
|
* Use attributes to differentiate between different operations.
|
|
125
146
|
*
|
|
126
|
-
* Base attributes from
|
|
127
|
-
*
|
|
128
|
-
* precedence - if a custom attribute key conflicts with a base attribute key,
|
|
129
|
-
* the custom attribute is silently dropped.
|
|
130
|
-
*
|
|
131
|
-
* Custom attributes are limited to MAX_CUSTOM_ATTRIBUTES (5). Base attributes are not limited.
|
|
147
|
+
* Base attributes from `runWithBaseAttributes` are merged in automatically and take
|
|
148
|
+
* precedence; custom attributes are capped at MAX_CUSTOM_ATTRIBUTES (7). See the class doc.
|
|
132
149
|
*
|
|
133
150
|
* @param value Either [seconds, nanoseconds] from process.hrtime() or milliseconds
|
|
134
|
-
* @param attributes Custom attributes including 'operation' to identify the operation type (max
|
|
151
|
+
* @param attributes Custom attributes including 'operation' to identify the operation type (max 7 custom attributes)
|
|
135
152
|
*
|
|
136
153
|
* @example
|
|
137
154
|
* ```typescript
|
|
@@ -148,16 +165,12 @@ export declare class DiagnosticsMetrics {
|
|
|
148
165
|
* Increment a counter by a specific value.
|
|
149
166
|
* Multiple counters are stored by name since counters represent different types of events.
|
|
150
167
|
*
|
|
151
|
-
* Base attributes from
|
|
152
|
-
*
|
|
153
|
-
* precedence - if a custom attribute key conflicts with a base attribute key,
|
|
154
|
-
* the custom attribute is silently dropped.
|
|
155
|
-
*
|
|
156
|
-
* Custom attributes are limited to MAX_CUSTOM_ATTRIBUTES (5). Base attributes are not limited.
|
|
168
|
+
* Base attributes from `runWithBaseAttributes` are merged in automatically and take
|
|
169
|
+
* precedence; custom attributes are capped at MAX_CUSTOM_ATTRIBUTES (7). See the class doc.
|
|
157
170
|
*
|
|
158
171
|
* @param name Counter name (e.g., 'http_requests_total', 'cache_hits_total')
|
|
159
172
|
* @param value Amount to increment by (typically 1)
|
|
160
|
-
* @param attributes Optional custom attributes for the counter (max
|
|
173
|
+
* @param attributes Optional custom attributes for the counter (max 7 custom attributes, e.g., { method: 'GET', status: '2xx' })
|
|
161
174
|
*
|
|
162
175
|
* @example
|
|
163
176
|
* ```typescript
|
|
@@ -169,16 +182,12 @@ export declare class DiagnosticsMetrics {
|
|
|
169
182
|
* Set a gauge to a specific value (current state).
|
|
170
183
|
* Multiple gauges are stored by name since gauges represent different types of measurements.
|
|
171
184
|
*
|
|
172
|
-
* Base attributes from
|
|
173
|
-
*
|
|
174
|
-
* precedence - if a custom attribute key conflicts with a base attribute key,
|
|
175
|
-
* the custom attribute is silently dropped.
|
|
176
|
-
*
|
|
177
|
-
* Custom attributes are limited to MAX_CUSTOM_ATTRIBUTES (5). Base attributes are not limited.
|
|
185
|
+
* Base attributes from `runWithBaseAttributes` are merged in automatically and take
|
|
186
|
+
* precedence; custom attributes are capped at MAX_CUSTOM_ATTRIBUTES (7). See the class doc.
|
|
178
187
|
*
|
|
179
188
|
* @param name Gauge name (e.g., 'cache_items_current', 'memory_usage_bytes')
|
|
180
189
|
* @param value Current value
|
|
181
|
-
* @param attributes Optional custom attributes for the gauge (max
|
|
190
|
+
* @param attributes Optional custom attributes for the gauge (max 7 custom attributes, e.g., { cache: 'pages' })
|
|
182
191
|
*
|
|
183
192
|
* @example
|
|
184
193
|
* ```typescript
|
|
@@ -186,4 +195,12 @@ export declare class DiagnosticsMetrics {
|
|
|
186
195
|
* ```
|
|
187
196
|
*/
|
|
188
197
|
setGauge(name: string, value: number, attributes?: Attributes): void;
|
|
198
|
+
registerObservableGauge(name: string, observe: ObservableCallback, options?: MetricOptions): () => void;
|
|
199
|
+
registerObservableCounter(name: string, observe: ObservableCallback, options?: MetricOptions): () => void;
|
|
200
|
+
private registerObservable;
|
|
201
|
+
private syncObservables;
|
|
202
|
+
private flushPendingObservables;
|
|
203
|
+
trackCache(name: string, cacheInstance: TrackedCache): () => void;
|
|
204
|
+
private ensureCacheInstruments;
|
|
205
|
+
private observeCaches;
|
|
189
206
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DiagnosticsMetrics = void 0;
|
|
4
4
|
const api_1 = require("@opentelemetry/api");
|
|
5
|
-
const client_1 = require("../service/metrics/client");
|
|
6
5
|
const constants_1 = require("../constants");
|
|
6
|
+
const client_1 = require("../service/metrics/client");
|
|
7
7
|
/**
|
|
8
8
|
* Maximum number of custom attributes allowed per metric call to control cardinality.
|
|
9
9
|
* This limit applies only to custom attributes provided by callers (VTEX IO Apps).
|
|
@@ -17,6 +17,19 @@ const MAX_CUSTOM_ATTRIBUTES = 7;
|
|
|
17
17
|
* These attributes are automatically merged with custom attributes in all metric methods.
|
|
18
18
|
*/
|
|
19
19
|
const BASE_ATTRIBUTES_KEY = (0, api_1.createContextKey)('vtex.metrics.baseAttributes');
|
|
20
|
+
// trackCache() metric names — one shared instrument set, differentiated by `cache`.
|
|
21
|
+
const CACHE_OPERATIONS_METRIC = 'io_app_cache_operations_total';
|
|
22
|
+
const CACHE_ITEMS_METRIC = 'io_app_cache_items_current';
|
|
23
|
+
const CACHE_CAPACITY_METRIC = 'io_app_cache_capacity';
|
|
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
|
+
]);
|
|
20
33
|
/**
|
|
21
34
|
* Converts an hrtime tuple [seconds, nanoseconds] to milliseconds.
|
|
22
35
|
*/
|
|
@@ -46,6 +59,50 @@ function limitCustomAttributes(customAttributes) {
|
|
|
46
59
|
}
|
|
47
60
|
return Object.fromEntries(entries.slice(0, MAX_CUSTOM_ATTRIBUTES));
|
|
48
61
|
}
|
|
62
|
+
/**
|
|
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.
|
|
100
|
+
*/
|
|
101
|
+
function limitObservableResult(result) {
|
|
102
|
+
return {
|
|
103
|
+
observe: (value, attributes) => result.observe(value, limitCustomAttributes(attributes)),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
49
106
|
/**
|
|
50
107
|
* DiagnosticsMetrics provides a high-level API for recording metrics using
|
|
51
108
|
* the @vtex/diagnostics-nodejs library. It completely abstracts instrument
|
|
@@ -103,6 +160,9 @@ class DiagnosticsMetrics {
|
|
|
103
160
|
constructor() {
|
|
104
161
|
this.counters = new Map();
|
|
105
162
|
this.gauges = new Map();
|
|
163
|
+
this.observableRegistrations = { gauge: new Map(), counter: new Map() };
|
|
164
|
+
this.observableInstruments = { gauge: new Map(), counter: new Map() };
|
|
165
|
+
this.cacheRegistry = new Map();
|
|
106
166
|
this.initMetricClient();
|
|
107
167
|
}
|
|
108
168
|
/**
|
|
@@ -124,6 +184,9 @@ class DiagnosticsMetrics {
|
|
|
124
184
|
]);
|
|
125
185
|
// Create the single latency histogram after client is ready
|
|
126
186
|
this.createLatencyHistogram();
|
|
187
|
+
// Attach any observable registrations made before the client was ready.
|
|
188
|
+
// No-op if there are none.
|
|
189
|
+
this.flushPendingObservables();
|
|
127
190
|
return this.metricsClient;
|
|
128
191
|
}
|
|
129
192
|
catch (error) {
|
|
@@ -225,20 +288,48 @@ class DiagnosticsMetrics {
|
|
|
225
288
|
// Merge: base attributes + non-conflicting custom attributes
|
|
226
289
|
return { ...baseAttributes, ...nonConflictingCustomAttributes };
|
|
227
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* The reason `name` cannot carry a new instrument, or undefined when it can. The SDK
|
|
293
|
+
* accepts two same-named streams silently and the collector then rejects them, so a name
|
|
294
|
+
* is owned by whichever instrument took it first: reusing it through the other API, or
|
|
295
|
+
* under the other kind, is a conflict. Reusing it in place — same API, same kind — is a
|
|
296
|
+
* replace, not a conflict.
|
|
297
|
+
*/
|
|
298
|
+
nameConflict(name, kind, origin) {
|
|
299
|
+
if (CACHE_METRIC_NAMES.has(name)) {
|
|
300
|
+
return 'reserved for trackCache()';
|
|
301
|
+
}
|
|
302
|
+
const other = kind === 'gauge' ? 'counter' : 'gauge';
|
|
303
|
+
const foreign = origin === 'push'
|
|
304
|
+
? [this.observableRegistrations[kind], this.observableRegistrations[other], this.pushInstruments(other)]
|
|
305
|
+
: [this.pushInstruments(kind), this.pushInstruments(other), this.observableRegistrations[other]];
|
|
306
|
+
return foreign.some(instruments => instruments.has(name)) ? 'already used by another instrument' : undefined;
|
|
307
|
+
}
|
|
308
|
+
pushInstruments(kind) {
|
|
309
|
+
return kind === 'counter' ? this.counters : this.gauges;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Counter and gauge instruments are created once per name and reused.
|
|
313
|
+
*/
|
|
314
|
+
instrumentFor(map, name, create) {
|
|
315
|
+
const existing = map.get(name);
|
|
316
|
+
if (existing !== undefined) {
|
|
317
|
+
return existing;
|
|
318
|
+
}
|
|
319
|
+
const instrument = create();
|
|
320
|
+
map.set(name, instrument);
|
|
321
|
+
return instrument;
|
|
322
|
+
}
|
|
228
323
|
/**
|
|
229
324
|
* Record a latency measurement using the single shared histogram.
|
|
230
325
|
* Accepts either an hrtime tuple from process.hrtime() or milliseconds as a number.
|
|
231
326
|
* Use attributes to differentiate between different operations.
|
|
232
327
|
*
|
|
233
|
-
* Base attributes from
|
|
234
|
-
*
|
|
235
|
-
* precedence - if a custom attribute key conflicts with a base attribute key,
|
|
236
|
-
* the custom attribute is silently dropped.
|
|
237
|
-
*
|
|
238
|
-
* Custom attributes are limited to MAX_CUSTOM_ATTRIBUTES (5). Base attributes are not limited.
|
|
328
|
+
* Base attributes from `runWithBaseAttributes` are merged in automatically and take
|
|
329
|
+
* precedence; custom attributes are capped at MAX_CUSTOM_ATTRIBUTES (7). See the class doc.
|
|
239
330
|
*
|
|
240
331
|
* @param value Either [seconds, nanoseconds] from process.hrtime() or milliseconds
|
|
241
|
-
* @param attributes Custom attributes including 'operation' to identify the operation type (max
|
|
332
|
+
* @param attributes Custom attributes including 'operation' to identify the operation type (max 7 custom attributes)
|
|
242
333
|
*
|
|
243
334
|
* @example
|
|
244
335
|
* ```typescript
|
|
@@ -266,16 +357,12 @@ class DiagnosticsMetrics {
|
|
|
266
357
|
* Increment a counter by a specific value.
|
|
267
358
|
* Multiple counters are stored by name since counters represent different types of events.
|
|
268
359
|
*
|
|
269
|
-
* Base attributes from
|
|
270
|
-
*
|
|
271
|
-
* precedence - if a custom attribute key conflicts with a base attribute key,
|
|
272
|
-
* the custom attribute is silently dropped.
|
|
273
|
-
*
|
|
274
|
-
* Custom attributes are limited to MAX_CUSTOM_ATTRIBUTES (5). Base attributes are not limited.
|
|
360
|
+
* Base attributes from `runWithBaseAttributes` are merged in automatically and take
|
|
361
|
+
* precedence; custom attributes are capped at MAX_CUSTOM_ATTRIBUTES (7). See the class doc.
|
|
275
362
|
*
|
|
276
363
|
* @param name Counter name (e.g., 'http_requests_total', 'cache_hits_total')
|
|
277
364
|
* @param value Amount to increment by (typically 1)
|
|
278
|
-
* @param attributes Optional custom attributes for the counter (max
|
|
365
|
+
* @param attributes Optional custom attributes for the counter (max 7 custom attributes, e.g., { method: 'GET', status: '2xx' })
|
|
279
366
|
*
|
|
280
367
|
* @example
|
|
281
368
|
* ```typescript
|
|
@@ -287,33 +374,25 @@ class DiagnosticsMetrics {
|
|
|
287
374
|
console.warn('DiagnosticsMetrics not initialized. Call initialize() first.');
|
|
288
375
|
return;
|
|
289
376
|
}
|
|
290
|
-
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
unit: '1',
|
|
295
|
-
});
|
|
296
|
-
this.counters.set(name, counter);
|
|
377
|
+
const conflict = this.nameConflict(name, 'counter', 'push');
|
|
378
|
+
if (conflict) {
|
|
379
|
+
console.error(`DiagnosticsMetrics: '${name}' is ${conflict}; ignoring the counter. Pick a distinct metric name.`);
|
|
380
|
+
return;
|
|
297
381
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
this.counters.get(name).add(value, mergedAttributes);
|
|
382
|
+
const counter = this.instrumentFor(this.counters, name, () => this.metricsClient.createCounter(name, { description: `Counter for ${name}`, unit: '1' }));
|
|
383
|
+
// mergeAttributes() limits custom attributes internally
|
|
384
|
+
counter.add(value, this.mergeAttributes(attributes));
|
|
302
385
|
}
|
|
303
386
|
/**
|
|
304
387
|
* Set a gauge to a specific value (current state).
|
|
305
388
|
* Multiple gauges are stored by name since gauges represent different types of measurements.
|
|
306
389
|
*
|
|
307
|
-
* Base attributes from
|
|
308
|
-
*
|
|
309
|
-
* precedence - if a custom attribute key conflicts with a base attribute key,
|
|
310
|
-
* the custom attribute is silently dropped.
|
|
311
|
-
*
|
|
312
|
-
* Custom attributes are limited to MAX_CUSTOM_ATTRIBUTES (5). Base attributes are not limited.
|
|
390
|
+
* Base attributes from `runWithBaseAttributes` are merged in automatically and take
|
|
391
|
+
* precedence; custom attributes are capped at MAX_CUSTOM_ATTRIBUTES (7). See the class doc.
|
|
313
392
|
*
|
|
314
393
|
* @param name Gauge name (e.g., 'cache_items_current', 'memory_usage_bytes')
|
|
315
394
|
* @param value Current value
|
|
316
|
-
* @param attributes Optional custom attributes for the gauge (max
|
|
395
|
+
* @param attributes Optional custom attributes for the gauge (max 7 custom attributes, e.g., { cache: 'pages' })
|
|
317
396
|
*
|
|
318
397
|
* @example
|
|
319
398
|
* ```typescript
|
|
@@ -325,18 +404,148 @@ class DiagnosticsMetrics {
|
|
|
325
404
|
console.warn('DiagnosticsMetrics not initialized. Call initialize() first.');
|
|
326
405
|
return;
|
|
327
406
|
}
|
|
328
|
-
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
407
|
+
const conflict = this.nameConflict(name, 'gauge', 'push');
|
|
408
|
+
if (conflict) {
|
|
409
|
+
console.error(`DiagnosticsMetrics: '${name}' is ${conflict}; ignoring the gauge. Pick a distinct metric name.`);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const gauge = this.instrumentFor(this.gauges, name, () => this.metricsClient.createGauge(name, { description: `Gauge for ${name}`, unit: '1' }));
|
|
413
|
+
// mergeAttributes() limits custom attributes internally
|
|
414
|
+
gauge.set(value, this.mergeAttributes(attributes));
|
|
415
|
+
}
|
|
416
|
+
// Registers (or replaces) a pull-based gauge: OTel calls `observe` on its own schedule
|
|
417
|
+
// with `result.observe(value, attributes?)`. No base-attribute merging (no request is
|
|
418
|
+
// in progress), but reported attributes keep the MAX_CUSTOM_ATTRIBUTES limit.
|
|
419
|
+
registerObservableGauge(name, observe, options) {
|
|
420
|
+
return this.registerObservable('gauge', name, observe, options);
|
|
421
|
+
}
|
|
422
|
+
// Same as registerObservableGauge, but `observe` reports the cumulative total —
|
|
423
|
+
// the SDK derives the delta itself.
|
|
424
|
+
registerObservableCounter(name, observe, options) {
|
|
425
|
+
return this.registerObservable('counter', name, observe, options);
|
|
426
|
+
}
|
|
427
|
+
registerObservable(kind, name, observe, options) {
|
|
428
|
+
const conflict = this.nameConflict(name, kind, 'observable');
|
|
429
|
+
if (conflict) {
|
|
430
|
+
console.error(`DiagnosticsMetrics: '${name}' is ${conflict}; ignoring the ${kind} registration. ` +
|
|
431
|
+
`Pick a distinct metric name.`);
|
|
432
|
+
return () => undefined;
|
|
433
|
+
}
|
|
434
|
+
const registration = { observe, options };
|
|
435
|
+
this.observableRegistrations[kind].set(name, registration);
|
|
436
|
+
this.syncObservables(kind);
|
|
437
|
+
return releaseIfOwner(this.observableRegistrations[kind], name, registration, () => {
|
|
438
|
+
var _a;
|
|
439
|
+
(_a = this.observableInstruments[kind].get(name)) === null || _a === void 0 ? void 0 : _a.remove();
|
|
440
|
+
this.observableInstruments[kind].delete(name);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
// Attaches every `kind` registration to its instrument; a no-op when there is nothing
|
|
444
|
+
// registered or the client is not ready. One instrument per name, with a callback that
|
|
445
|
+
// trampolines over the current registration — re-creating it would duplicate the stream.
|
|
446
|
+
syncObservables(kind) {
|
|
447
|
+
if (this.observableRegistrations[kind].size === 0) {
|
|
448
|
+
return; // skip the meter entirely if this kind is unused
|
|
449
|
+
}
|
|
450
|
+
const client = this.metricsClient;
|
|
451
|
+
if (!client) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
for (const [name, { options }] of this.observableRegistrations[kind]) {
|
|
455
|
+
if (this.observableInstruments[kind].has(name)) {
|
|
456
|
+
continue; // already attached; options apply only on the first registration
|
|
457
|
+
}
|
|
458
|
+
const attached = (result) => {
|
|
459
|
+
var _a;
|
|
460
|
+
(_a = this.observableRegistrations[kind].get(name)) === null || _a === void 0 ? void 0 : _a.observe(limitObservableResult(result));
|
|
461
|
+
};
|
|
462
|
+
const instrument = kind === 'gauge'
|
|
463
|
+
? client.createObservableGauge(name, attached, options)
|
|
464
|
+
: client.createObservableCounter(name, attached, options);
|
|
465
|
+
this.observableInstruments[kind].set(name, instrument);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// Runs once the client is ready. No-op for an app that never calls
|
|
469
|
+
// registerObservableGauge/Counter/trackCache.
|
|
470
|
+
flushPendingObservables() {
|
|
471
|
+
this.syncObservables('gauge');
|
|
472
|
+
this.syncObservables('counter');
|
|
473
|
+
if (this.cacheRegistry.size > 0) {
|
|
474
|
+
this.ensureCacheInstruments();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// Replacement for the legacy MetricsAccumulator.trackCache() over the same cache
|
|
478
|
+
// instances; reads getCumulativeStats(), which has no side effects. See METRICS_CATALOG.md.
|
|
479
|
+
trackCache(name, cacheInstance) {
|
|
480
|
+
const registration = { cache: cacheInstance };
|
|
481
|
+
this.cacheRegistry.set(name, registration);
|
|
482
|
+
this.ensureCacheInstruments();
|
|
483
|
+
return releaseIfOwner(this.cacheRegistry, name, registration);
|
|
484
|
+
}
|
|
485
|
+
ensureCacheInstruments() {
|
|
486
|
+
if (this.cacheInstruments) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
const client = this.metricsClient;
|
|
490
|
+
if (!client) {
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const operations = client.createObservableCounter(CACHE_OPERATIONS_METRIC, undefined, {
|
|
494
|
+
description: 'Hit/miss operations for a VTEX IO app in-memory cache',
|
|
495
|
+
unit: '1',
|
|
496
|
+
});
|
|
497
|
+
const items = client.createObservableGauge(CACHE_ITEMS_METRIC, undefined, {
|
|
498
|
+
description: 'Current number of items held by a VTEX IO app cache',
|
|
499
|
+
unit: '1',
|
|
500
|
+
});
|
|
501
|
+
const capacity = client.createObservableGauge(CACHE_CAPACITY_METRIC, undefined, {
|
|
502
|
+
description: 'Capacity of a VTEX IO app cache, in the units that cache uses (item count, unless the LRU was built with a length function)',
|
|
503
|
+
unit: '1',
|
|
504
|
+
});
|
|
505
|
+
const disposed = client.createObservableCounter(CACHE_DISPOSED_METRIC, undefined, {
|
|
506
|
+
description: 'Items disposed (evicted) from a VTEX IO app cache',
|
|
507
|
+
unit: '1',
|
|
508
|
+
});
|
|
509
|
+
client.addBatchObservableCallback((result) => this.observeCaches(result), [operations.instrument, items.instrument, capacity.instrument, disposed.instrument]);
|
|
510
|
+
// These live for the process, so only the handles are kept: the wrappers exist
|
|
511
|
+
// to attach the batch callback above.
|
|
512
|
+
this.cacheInstruments = {
|
|
513
|
+
capacity: capacity.instrument,
|
|
514
|
+
disposed: disposed.instrument,
|
|
515
|
+
items: items.instrument,
|
|
516
|
+
operations: operations.instrument,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
// Reads each registered cache's cumulative counters straight into the instruments.
|
|
520
|
+
observeCaches(result) {
|
|
521
|
+
if (!this.cacheInstruments) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const { operations, items, capacity, disposed } = this.cacheInstruments;
|
|
525
|
+
for (const [name, { cache }] of this.cacheRegistry) {
|
|
526
|
+
let stats;
|
|
527
|
+
try {
|
|
528
|
+
stats = cache.getCumulativeStats();
|
|
529
|
+
}
|
|
530
|
+
catch (error) {
|
|
531
|
+
console.error('DiagnosticsMetrics: failed to read stats for cache', name, error);
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
const attributes = { cache: name };
|
|
535
|
+
if (typeof stats.hits === 'number' && typeof stats.total === 'number') {
|
|
536
|
+
result.observe(operations, stats.hits, { ...attributes, cache_state: 'hit' });
|
|
537
|
+
result.observe(operations, missesOf(stats), { ...attributes, cache_state: 'miss' });
|
|
538
|
+
}
|
|
539
|
+
if (typeof stats.itemCount === 'number') {
|
|
540
|
+
result.observe(items, stats.itemCount, attributes);
|
|
541
|
+
}
|
|
542
|
+
if (typeof stats.max === 'number') {
|
|
543
|
+
result.observe(capacity, capacityOrSentinel(stats.max), attributes);
|
|
544
|
+
}
|
|
545
|
+
if (typeof stats.disposedItems === 'number') {
|
|
546
|
+
result.observe(disposed, stats.disposedItems, attributes);
|
|
547
|
+
}
|
|
335
548
|
}
|
|
336
|
-
// Merge base attributes from context with custom attributes (custom attrs are limited internally)
|
|
337
|
-
const mergedAttributes = this.mergeAttributes(attributes);
|
|
338
|
-
// Set the gauge value
|
|
339
|
-
this.gauges.get(name).set(value, mergedAttributes);
|
|
340
549
|
}
|
|
341
550
|
}
|
|
342
551
|
exports.DiagnosticsMetrics = DiagnosticsMetrics;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vtex/api",
|
|
3
|
-
"version": "7.5.
|
|
3
|
+
"version": "7.5.1-beta.1",
|
|
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.1.8-io",
|
|
57
|
-
"@vtex/diagnostics-semconv": "5.5.
|
|
56
|
+
"@vtex/diagnostics-nodejs": "0.1.8-io-beta.4",
|
|
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",
|
|
@@ -95,6 +95,8 @@
|
|
|
95
95
|
"xss": "^1.0.6"
|
|
96
96
|
},
|
|
97
97
|
"devDependencies": {
|
|
98
|
+
"@opentelemetry/context-async-hooks": "^1.30.1",
|
|
99
|
+
"@opentelemetry/sdk-metrics": "^1.30.1",
|
|
98
100
|
"@tiagonapoli/opentracing-alternate-mock": "^0.0.3",
|
|
99
101
|
"@types/archiver": "^2.0.1",
|
|
100
102
|
"@types/bluebird": "^3.5.27",
|