@vtex/api 7.5.1-beta.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 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
@@ -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 hits;
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;
@@ -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.hits = 0;
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 stats = {
24
- hits: this.hits - this.reported.hits,
25
- name,
26
- total: this.total - this.reported.total,
27
- };
28
- this.reported = { hits: this.hits, total: this.total };
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.total += 1;
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.hits += 1;
37
+ this.counters.countHit();
44
38
  resolve(fileData);
45
39
  }
46
- catch (e) {
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 (e) {
58
+ catch {
64
59
  release();
65
60
  resolve(true);
66
61
  }
@@ -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 hits;
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>;
@@ -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.hits += 1;
15
+ this.counters.countHit();
15
16
  }
16
- this.total += 1;
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 hits = this.hits - this.reported.hits;
24
- const total = this.total - this.reported.total;
25
- const stats = {
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
- disposedItems: this.disposed,
40
- hits: this.hits,
41
- itemCount: this.storage.itemCount,
42
- length: this.storage.length,
43
- max: this.storage.max,
44
- total: this.total,
45
- });
46
- this.hits = 0;
47
- this.total = 0;
48
- this.disposed = 0;
49
- this.reported = { disposed: 0, hits: 0, total: 0 };
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.disposed += 1,
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 disposed;
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 hits = this.hits - this.reported.hits;
22
- const total = this.total - this.reported.total;
23
- const stats = {
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
- disposedItems: this.disposed,
38
- hits: this.hits,
39
- itemCount: this.lruStorage.itemCount,
40
- length: this.lruStorage.length,
41
- max: this.lruStorage.max,
42
- total: this.total,
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.total += 1;
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.hits += 1;
60
+ this.counters.countHit();
61
61
  resolve(fileData);
62
62
  }
63
- catch (e) {
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 (e) {
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 (e) {
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.hits = 0;
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.disposed += 1;
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 hits;
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.hits = 0;
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 = this.hits - this.reported.hits;
45
- const total = this.total - this.reported.total;
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: this.hits,
57
- total: this.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.total += 1;
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.hits += 1;
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;
@@ -18,8 +18,9 @@ export type LRUStats = {
18
18
  total: number;
19
19
  };
20
20
  export type CumulativeStats = {
21
- hits: number;
22
- total: number;
21
+ hits?: number;
22
+ total?: number;
23
+ misses?: number;
23
24
  disposedItems?: number;
24
25
  itemCount?: number;
25
26
  length?: number;
@@ -126,20 +126,29 @@ export declare class DiagnosticsMetrics {
126
126
  * @returns Merged attributes (base + non-conflicting limited custom) or undefined if both are empty
127
127
  */
128
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;
129
142
  /**
130
143
  * Record a latency measurement using the single shared histogram.
131
144
  * Accepts either an hrtime tuple from process.hrtime() or milliseconds as a number.
132
145
  * Use attributes to differentiate between different operations.
133
146
  *
134
- * Base attributes from the current context (set via `runWithBaseAttributes`) are
135
- * automatically merged with the provided custom attributes. Base attributes take
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.
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.
140
149
  *
141
150
  * @param value Either [seconds, nanoseconds] from process.hrtime() or milliseconds
142
- * @param attributes Custom attributes including 'operation' to identify the operation type (max 5 custom attributes)
151
+ * @param attributes Custom attributes including 'operation' to identify the operation type (max 7 custom attributes)
143
152
  *
144
153
  * @example
145
154
  * ```typescript
@@ -156,16 +165,12 @@ export declare class DiagnosticsMetrics {
156
165
  * Increment a counter by a specific value.
157
166
  * Multiple counters are stored by name since counters represent different types of events.
158
167
  *
159
- * Base attributes from the current context (set via `runWithBaseAttributes`) are
160
- * automatically merged with the provided custom attributes. Base attributes take
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.
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.
165
170
  *
166
171
  * @param name Counter name (e.g., 'http_requests_total', 'cache_hits_total')
167
172
  * @param value Amount to increment by (typically 1)
168
- * @param attributes Optional custom attributes for the counter (max 5 custom attributes, e.g., { method: 'GET', status: '2xx' })
173
+ * @param attributes Optional custom attributes for the counter (max 7 custom attributes, e.g., { method: 'GET', status: '2xx' })
169
174
  *
170
175
  * @example
171
176
  * ```typescript
@@ -177,16 +182,12 @@ export declare class DiagnosticsMetrics {
177
182
  * Set a gauge to a specific value (current state).
178
183
  * Multiple gauges are stored by name since gauges represent different types of measurements.
179
184
  *
180
- * Base attributes from the current context (set via `runWithBaseAttributes`) are
181
- * automatically merged with the provided custom attributes. Base attributes take
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.
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.
186
187
  *
187
188
  * @param name Gauge name (e.g., 'cache_items_current', 'memory_usage_bytes')
188
189
  * @param value Current value
189
- * @param attributes Optional custom attributes for the gauge (max 5 custom attributes, e.g., { cache: 'pages' })
190
+ * @param attributes Optional custom attributes for the gauge (max 7 custom attributes, e.g., { cache: 'pages' })
190
191
  *
191
192
  * @example
192
193
  * ```typescript
@@ -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
- * Applies the same cardinality limit the push-based methods use to whatever an
56
- * observable callback reports. Observables run outside any request, so there are no
57
- * base attributes to merge — every attribute here is a custom one.
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 {
@@ -246,20 +288,48 @@ class DiagnosticsMetrics {
246
288
  // Merge: base attributes + non-conflicting custom attributes
247
289
  return { ...baseAttributes, ...nonConflictingCustomAttributes };
248
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
+ }
249
323
  /**
250
324
  * Record a latency measurement using the single shared histogram.
251
325
  * Accepts either an hrtime tuple from process.hrtime() or milliseconds as a number.
252
326
  * Use attributes to differentiate between different operations.
253
327
  *
254
- * Base attributes from the current context (set via `runWithBaseAttributes`) are
255
- * automatically merged with the provided custom attributes. Base attributes take
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.
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.
260
330
  *
261
331
  * @param value Either [seconds, nanoseconds] from process.hrtime() or milliseconds
262
- * @param attributes Custom attributes including 'operation' to identify the operation type (max 5 custom attributes)
332
+ * @param attributes Custom attributes including 'operation' to identify the operation type (max 7 custom attributes)
263
333
  *
264
334
  * @example
265
335
  * ```typescript
@@ -287,16 +357,12 @@ class DiagnosticsMetrics {
287
357
  * Increment a counter by a specific value.
288
358
  * Multiple counters are stored by name since counters represent different types of events.
289
359
  *
290
- * Base attributes from the current context (set via `runWithBaseAttributes`) are
291
- * automatically merged with the provided custom attributes. Base attributes take
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.
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.
296
362
  *
297
363
  * @param name Counter name (e.g., 'http_requests_total', 'cache_hits_total')
298
364
  * @param value Amount to increment by (typically 1)
299
- * @param attributes Optional custom attributes for the counter (max 5 custom attributes, e.g., { method: 'GET', status: '2xx' })
365
+ * @param attributes Optional custom attributes for the counter (max 7 custom attributes, e.g., { method: 'GET', status: '2xx' })
300
366
  *
301
367
  * @example
302
368
  * ```typescript
@@ -308,33 +374,25 @@ class DiagnosticsMetrics {
308
374
  console.warn('DiagnosticsMetrics not initialized. Call initialize() first.');
309
375
  return;
310
376
  }
311
- // Get or create counter instrument
312
- if (!this.counters.has(name)) {
313
- const counter = this.metricsClient.createCounter(name, {
314
- description: `Counter for ${name}`,
315
- unit: '1',
316
- });
317
- 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;
318
381
  }
319
- // Merge base attributes from context with custom attributes (custom attrs are limited internally)
320
- const mergedAttributes = this.mergeAttributes(attributes);
321
- // Increment the counter
322
- 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));
323
385
  }
324
386
  /**
325
387
  * Set a gauge to a specific value (current state).
326
388
  * Multiple gauges are stored by name since gauges represent different types of measurements.
327
389
  *
328
- * Base attributes from the current context (set via `runWithBaseAttributes`) are
329
- * automatically merged with the provided custom attributes. Base attributes take
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.
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.
334
392
  *
335
393
  * @param name Gauge name (e.g., 'cache_items_current', 'memory_usage_bytes')
336
394
  * @param value Current value
337
- * @param attributes Optional custom attributes for the gauge (max 5 custom attributes, e.g., { cache: 'pages' })
395
+ * @param attributes Optional custom attributes for the gauge (max 7 custom attributes, e.g., { cache: 'pages' })
338
396
  *
339
397
  * @example
340
398
  * ```typescript
@@ -346,23 +404,18 @@ class DiagnosticsMetrics {
346
404
  console.warn('DiagnosticsMetrics not initialized. Call initialize() first.');
347
405
  return;
348
406
  }
349
- // Get or create gauge instrument
350
- if (!this.gauges.has(name)) {
351
- const gauge = this.metricsClient.createGauge(name, {
352
- description: `Gauge for ${name}`,
353
- unit: '1',
354
- });
355
- this.gauges.set(name, gauge);
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;
356
411
  }
357
- // Merge base attributes from context with custom attributes (custom attrs are limited internally)
358
- const mergedAttributes = this.mergeAttributes(attributes);
359
- // Set the gauge value
360
- this.gauges.get(name).set(value, mergedAttributes);
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));
361
415
  }
362
- // Registers (or replaces) a pull-based gauge: OTel calls `observe` on its own
363
- // schedule, with `result.observe(value, attributes?)`. No base-attribute merging —
364
- // there's no request in progress when this runs — but the attributes reported are
365
- // held to the same MAX_CUSTOM_ATTRIBUTES limit as the push-based methods.
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.
366
419
  registerObservableGauge(name, observe, options) {
367
420
  return this.registerObservable('gauge', name, observe, options);
368
421
  }
@@ -372,38 +425,24 @@ class DiagnosticsMetrics {
372
425
  return this.registerObservable('counter', name, observe, options);
373
426
  }
374
427
  registerObservable(kind, name, observe, options) {
375
- // The same name registered as both kinds produces two same-named streams of
376
- // different types in one meter, which the SDK accepts silently and the collector
377
- // then rejects. Refuse the second one instead of publishing a broken metric.
378
- const otherKind = kind === 'gauge' ? 'counter' : 'gauge';
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.`);
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.`);
382
432
  return () => undefined;
383
433
  }
384
- this.observableRegistrations[kind].set(name, { observe, options });
434
+ const registration = { observe, options };
435
+ this.observableRegistrations[kind].set(name, registration);
385
436
  this.syncObservables(kind);
386
- return () => {
387
- var _a, _b;
388
- // A later registration for the same name owns the instrument now; disposing
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();
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();
395
440
  this.observableInstruments[kind].delete(name);
396
- };
441
+ });
397
442
  }
398
- // Attaches every `kind` registration to its instrument. A no-op if there's nothing
399
- // registered or the client isn't ready — safe to call anytime, including from
400
- // flushPendingObservables() once the client becomes ready.
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.
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.
407
446
  syncObservables(kind) {
408
447
  if (this.observableRegistrations[kind].size === 0) {
409
448
  return; // skip the meter entirely if this kind is unused
@@ -414,7 +453,7 @@ class DiagnosticsMetrics {
414
453
  }
415
454
  for (const [name, { options }] of this.observableRegistrations[kind]) {
416
455
  if (this.observableInstruments[kind].has(name)) {
417
- continue;
456
+ continue; // already attached; options apply only on the first registration
418
457
  }
419
458
  const attached = (result) => {
420
459
  var _a;
@@ -435,15 +474,13 @@ class DiagnosticsMetrics {
435
474
  this.ensureCacheInstruments();
436
475
  }
437
476
  }
438
- // Replacement for the legacy MetricsAccumulator.trackCache() — same cache instances,
439
- // see METRICS_CATALOG.md for the metrics emitted. Reads getCumulativeStats(), which
440
- // has no side effects, so this can run alongside the legacy trackCache().
477
+ // Replacement for the legacy MetricsAccumulator.trackCache() over the same cache
478
+ // instances; reads getCumulativeStats(), which has no side effects. See METRICS_CATALOG.md.
441
479
  trackCache(name, cacheInstance) {
442
- this.cacheRegistry.set(name, cacheInstance);
480
+ const registration = { cache: cacheInstance };
481
+ this.cacheRegistry.set(name, registration);
443
482
  this.ensureCacheInstruments();
444
- return () => {
445
- this.cacheRegistry.delete(name);
446
- };
483
+ return releaseIfOwner(this.cacheRegistry, name, registration);
447
484
  }
448
485
  ensureCacheInstruments() {
449
486
  if (this.cacheInstruments) {
@@ -473,10 +510,10 @@ class DiagnosticsMetrics {
473
510
  // These live for the process, so only the handles are kept: the wrappers exist
474
511
  // to attach the batch callback above.
475
512
  this.cacheInstruments = {
476
- operations: operations.instrument,
477
- items: items.instrument,
478
513
  capacity: capacity.instrument,
479
514
  disposed: disposed.instrument,
515
+ items: items.instrument,
516
+ operations: operations.instrument,
480
517
  };
481
518
  }
482
519
  // Reads each registered cache's cumulative counters straight into the instruments.
@@ -485,7 +522,7 @@ class DiagnosticsMetrics {
485
522
  return;
486
523
  }
487
524
  const { operations, items, capacity, disposed } = this.cacheInstruments;
488
- for (const [name, cache] of this.cacheRegistry) {
525
+ for (const [name, { cache }] of this.cacheRegistry) {
489
526
  let stats;
490
527
  try {
491
528
  stats = cache.getCumulativeStats();
@@ -497,13 +534,13 @@ class DiagnosticsMetrics {
497
534
  const attributes = { cache: name };
498
535
  if (typeof stats.hits === 'number' && typeof stats.total === 'number') {
499
536
  result.observe(operations, stats.hits, { ...attributes, cache_state: 'hit' });
500
- result.observe(operations, Math.max(stats.total - stats.hits, 0), { ...attributes, cache_state: 'miss' });
537
+ result.observe(operations, missesOf(stats), { ...attributes, cache_state: 'miss' });
501
538
  }
502
539
  if (typeof stats.itemCount === 'number') {
503
540
  result.observe(items, stats.itemCount, attributes);
504
541
  }
505
542
  if (typeof stats.max === 'number') {
506
- result.observe(capacity, stats.max, attributes);
543
+ result.observe(capacity, capacityOrSentinel(stats.max), attributes);
507
544
  }
508
545
  if (typeof stats.disposedItems === 'number') {
509
546
  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.0",
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",
@@ -54,7 +54,7 @@
54
54
  "@types/koa": "^2.11.0",
55
55
  "@types/koa-compose": "^3.2.3",
56
56
  "@vtex/diagnostics-nodejs": "0.1.8-io-beta.4",
57
- "@vtex/diagnostics-semconv": "5.5.2",
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",