@vtex/api 6.51.3 → 6.53.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,7 +5,39 @@ 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
- ## [Unreleased]
8
+ ## [6.53.0-beta.0]
9
+ ### Added
10
+ - Diagnostics metrics observability, backported from the `master` (7.x) line: `DiagnosticsMetrics`
11
+ (`recordLatency`, `incrementCounter`, `setGauge`, `runWithBaseAttributes`), a split
12
+ traces/metrics/logs telemetry client (`@vtex/diagnostics-nodejs@0.1.8-io`,
13
+ `@vtex/diagnostics-semconv`), cluster resource attributes, and automatic Koa + host-metrics
14
+ instrumentation. Disabled by default; opt in per app with
15
+ `VTEX_DIAGNOSTICS_TELEMETRY_ENABLED=true`.
16
+
17
+ ## [6.52.0]
18
+ ### Added
19
+ - Cluster-wide Prometheus `/metrics` aggregation. Each cluster worker keeps its own
20
+ prom-client registry, so serving `/metrics` from a single round-robin selected
21
+ worker exposed only that worker's counters; Prometheus read the per-scrape braid
22
+ of independent monotonic counters as counter resets and inflated `rate()` /
23
+ `increase()` on `runtime_http_*` counters by orders of magnitude. In multi-worker
24
+ mode the worker answering a scrape now asks the master for a merged, monotonic
25
+ view built from every worker's registry over the existing cluster IPC (via
26
+ prom-client's `AggregatorRegistry`), with a bounded timeout and a local-registry
27
+ fallback. Single-worker mode (`workers === 1`, includes `LINKED`) is unchanged.
28
+ Backport of #667 to the 6.x line.
29
+ ### Fixed
30
+ - `runtime_http_*` metrics no longer emit samples without a `handler` label. Requests
31
+ that never reach a named handler (unmatched paths, replica-level rate limit
32
+ rejections, errors before the route pipeline) were counted with
33
+ `handler: undefined`; Node's cluster IPC serializes worker registries as JSON, which
34
+ drops `undefined` values, so the aggregated `/metrics` exposed a second, unnamed
35
+ series that Prometheus reads as `handler=""`. Those requests are now labelled
36
+ `handler="undefined"` — the same value prom-client rendered locally before cluster
37
+ aggregation — keeping dashboards and alerts that filter on it working. Backport of
38
+ #673 to the 6.x line.
39
+ - `/_status` requests are now reported as `handler="builtin:status-track"`, matching
40
+ the other builtin handlers, instead of falling into the unnamed bucket.
9
41
 
10
42
  ## [6.51.0] - 2026-06-23
11
43
  ### Added
@@ -2,6 +2,8 @@ export declare const NODE_VTEX_API_VERSION: any;
2
2
  export declare const DEFAULT_WORKSPACE = "master";
3
3
  export declare const IS_IO: string | undefined;
4
4
  export declare const PID: number;
5
+ export declare const CLUSTER_ID: string;
6
+ export declare const CLUSTER_ROLE: string;
5
7
  export declare const CACHE_CONTROL_HEADER = "cache-control";
6
8
  export declare const SEGMENT_HEADER = "x-vtex-segment";
7
9
  export declare const SESSION_HEADER = "x-vtex-session";
@@ -40,6 +42,15 @@ export declare const MAX_AGE: {
40
42
  };
41
43
  export declare const HTTP_SERVER_PORT = 5050;
42
44
  export declare const MAX_WORKERS = 4;
45
+ export declare const AttributeKeys: {
46
+ VTEX_ACCOUNT_NAME: "vtex.account.name";
47
+ VTEX_IO_APP_AUTHOR_TYPE: "vtex_io.app.author_type";
48
+ VTEX_IO_APP_ID: "vtex_io.app.id";
49
+ VTEX_IO_CLUSTER_ID: "vtex_io.cluster.id";
50
+ VTEX_IO_CLUSTER_ROLE: "vtex_io.cluster.role";
51
+ VTEX_IO_WORKSPACE_NAME: "vtex_io.workspace.name";
52
+ VTEX_IO_WORKSPACE_TYPE: "vtex_io.workspace.type";
53
+ };
43
54
  export declare const LINKED: boolean;
44
55
  export declare const REGION: string;
45
56
  export declare const PUBLIC_ENDPOINT: string;
@@ -58,3 +69,7 @@ export declare const PRODUCTION: boolean;
58
69
  export declare const INSPECT_DEBUGGER_PORT = 5858;
59
70
  export declare const cancellableMethods: Set<string>;
60
71
  export declare const LOG_CLIENT_INIT_TIMEOUT_MS = 5000;
72
+ export declare const METRIC_CLIENT_INIT_TIMEOUT_MS = 5000;
73
+ export declare const OTEL_EXPORTER_OTLP_ENDPOINT: string;
74
+ export declare const DK_APP_ID: string;
75
+ export declare const DIAGNOSTICS_TELEMETRY_ENABLED: boolean;
package/lib/constants.js CHANGED
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LOG_CLIENT_INIT_TIMEOUT_MS = exports.cancellableMethods = exports.INSPECT_DEBUGGER_PORT = exports.PRODUCTION = exports.WORKSPACE = exports.ACCOUNT = exports.NODE_ENV = exports.APP = exports.PUBLIC_ENDPOINT = exports.REGION = exports.LINKED = exports.MAX_WORKERS = exports.HTTP_SERVER_PORT = exports.MAX_AGE = exports.UP_SIGNAL = exports.BODY_HASH = exports.PROVIDER_HEADER = exports.TRACE_ID_HEADER = exports.COLOSSUS_PARAMS_HEADER = exports.COLOSSUS_ROUTE_ID_HEADER = exports.COLOSSUS_ROUTE_DECLARER_HEADER = exports.EVENT_HANDLER_ID_HEADER = exports.EVENT_SUBJECT_HEADER = exports.EVENT_SENDER_HEADER = exports.EVENT_KEY_HEADER = exports.WORKSPACE_HEADER = exports.WORKSPACE_IS_PRODUCTION_HEADER = exports.PLATFORM_HEADER = exports.OPERATION_ID_HEADER = exports.ROUTER_CACHE_HEADER = exports.REQUEST_ID_HEADER = exports.CREDENTIAL_HEADER = exports.ACCOUNT_HEADER = exports.ETAG_HEADER = exports.META_HEADER_BUCKET = exports.META_HEADER = exports.BINDING_HEADER = exports.TENANT_HEADER = exports.FORWARDED_HOST_HEADER = exports.LOCALE_HEADER = exports.PRODUCT_HEADER = exports.SESSION_HEADER = exports.SEGMENT_HEADER = exports.CACHE_CONTROL_HEADER = exports.PID = exports.IS_IO = exports.DEFAULT_WORKSPACE = exports.NODE_VTEX_API_VERSION = void 0;
3
+ exports.cancellableMethods = exports.INSPECT_DEBUGGER_PORT = exports.PRODUCTION = exports.WORKSPACE = exports.ACCOUNT = exports.NODE_ENV = exports.APP = exports.PUBLIC_ENDPOINT = exports.REGION = exports.LINKED = exports.AttributeKeys = exports.MAX_WORKERS = exports.HTTP_SERVER_PORT = exports.MAX_AGE = exports.UP_SIGNAL = exports.BODY_HASH = exports.PROVIDER_HEADER = exports.TRACE_ID_HEADER = exports.COLOSSUS_PARAMS_HEADER = exports.COLOSSUS_ROUTE_ID_HEADER = exports.COLOSSUS_ROUTE_DECLARER_HEADER = exports.EVENT_HANDLER_ID_HEADER = exports.EVENT_SUBJECT_HEADER = exports.EVENT_SENDER_HEADER = exports.EVENT_KEY_HEADER = exports.WORKSPACE_HEADER = exports.WORKSPACE_IS_PRODUCTION_HEADER = exports.PLATFORM_HEADER = exports.OPERATION_ID_HEADER = exports.ROUTER_CACHE_HEADER = exports.REQUEST_ID_HEADER = exports.CREDENTIAL_HEADER = exports.ACCOUNT_HEADER = exports.ETAG_HEADER = exports.META_HEADER_BUCKET = exports.META_HEADER = exports.BINDING_HEADER = exports.TENANT_HEADER = exports.FORWARDED_HOST_HEADER = exports.LOCALE_HEADER = exports.PRODUCT_HEADER = exports.SESSION_HEADER = exports.SEGMENT_HEADER = exports.CACHE_CONTROL_HEADER = exports.CLUSTER_ROLE = exports.CLUSTER_ID = exports.PID = exports.IS_IO = exports.DEFAULT_WORKSPACE = exports.NODE_VTEX_API_VERSION = void 0;
4
+ exports.DIAGNOSTICS_TELEMETRY_ENABLED = exports.DK_APP_ID = exports.OTEL_EXPORTER_OTLP_ENDPOINT = exports.METRIC_CLIENT_INIT_TIMEOUT_MS = exports.LOG_CLIENT_INIT_TIMEOUT_MS = void 0;
5
+ const diagnostics_semconv_1 = require("@vtex/diagnostics-semconv");
4
6
  const app_1 = require("./utils/app");
5
7
  // tslint:disable-next-line
6
8
  const pkg = require('../package.json');
@@ -8,6 +10,8 @@ exports.NODE_VTEX_API_VERSION = pkg.version;
8
10
  exports.DEFAULT_WORKSPACE = 'master';
9
11
  exports.IS_IO = process.env.VTEX_IO;
10
12
  exports.PID = process.pid;
13
+ exports.CLUSTER_ID = process.env.VTEX_CLUSTER_ID;
14
+ exports.CLUSTER_ROLE = process.env.VTEX_CLUSTER_ROLE;
11
15
  exports.CACHE_CONTROL_HEADER = 'cache-control';
12
16
  exports.SEGMENT_HEADER = 'x-vtex-segment';
13
17
  exports.SESSION_HEADER = 'x-vtex-session';
@@ -45,6 +49,15 @@ exports.MAX_AGE = {
45
49
  };
46
50
  exports.HTTP_SERVER_PORT = 5050;
47
51
  exports.MAX_WORKERS = 4;
52
+ exports.AttributeKeys = {
53
+ VTEX_ACCOUNT_NAME: diagnostics_semconv_1.ATTR_VTEX_ACCOUNT_NAME,
54
+ VTEX_IO_APP_AUTHOR_TYPE: diagnostics_semconv_1.ATTR_VTEX_IO_APP_AUTHOR_TYPE,
55
+ VTEX_IO_APP_ID: diagnostics_semconv_1.ATTR_VTEX_IO_APP_ID,
56
+ VTEX_IO_CLUSTER_ID: diagnostics_semconv_1.ATTR_VTEX_IO_CLUSTER_ID,
57
+ VTEX_IO_CLUSTER_ROLE: diagnostics_semconv_1.ATTR_VTEX_IO_CLUSTER_ROLE,
58
+ VTEX_IO_WORKSPACE_NAME: diagnostics_semconv_1.ATTR_VTEX_IO_WORKSPACE_NAME,
59
+ VTEX_IO_WORKSPACE_TYPE: diagnostics_semconv_1.ATTR_VTEX_IO_WORKSPACE_TYPE,
60
+ };
48
61
  exports.LINKED = !!process.env.VTEX_APP_LINK;
49
62
  exports.REGION = process.env.VTEX_REGION;
50
63
  exports.PUBLIC_ENDPOINT = process.env.VTEX_PUBLIC_ENDPOINT || 'myvtex.com';
@@ -65,3 +78,7 @@ exports.PRODUCTION = process.env.VTEX_PRODUCTION === 'true';
65
78
  exports.INSPECT_DEBUGGER_PORT = 5858;
66
79
  exports.cancellableMethods = new Set(['GET', 'OPTIONS', 'HEAD']);
67
80
  exports.LOG_CLIENT_INIT_TIMEOUT_MS = 5000;
81
+ exports.METRIC_CLIENT_INIT_TIMEOUT_MS = 5000;
82
+ exports.OTEL_EXPORTER_OTLP_ENDPOINT = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
83
+ exports.DK_APP_ID = process.env.NODE_VTEX_API_DK_APP_ID || 'apps-team';
84
+ exports.DIAGNOSTICS_TELEMETRY_ENABLED = process.env.VTEX_DIAGNOSTICS_TELEMETRY_ENABLED === 'true';
@@ -0,0 +1,189 @@
1
+ import { Attributes } from '@opentelemetry/api';
2
+ /**
3
+ * DiagnosticsMetrics provides a high-level API for recording metrics using
4
+ * the @vtex/diagnostics-nodejs library. It completely abstracts instrument
5
+ * management, bucket configuration, and lifecycle.
6
+ *
7
+ * Uses a single histogram for all latency measurements with attributes to differentiate.
8
+ * This follows OpenTelemetry best practices and reduces metric cardinality.
9
+ *
10
+ * ## Base Attributes (Request Context)
11
+ *
12
+ * DiagnosticsMetrics supports automatic merging of request-scoped "base attributes"
13
+ * with custom attributes provided in each metric call. This is useful for automatically
14
+ * including request context (account, status_code, route_id, etc.) in all metrics
15
+ * recorded during a request lifecycle.
16
+ *
17
+ * Use `runWithBaseAttributes()` to set base attributes for a scope. All metric calls
18
+ * within that scope will automatically include these base attributes, merged with
19
+ * any custom attributes provided.
20
+ *
21
+ * **Important:** Base attributes take precedence over custom attributes. If a custom
22
+ * attribute has the same key as a base attribute, the custom attribute is silently
23
+ * dropped and the base attribute value is used.
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const diagnosticsMetrics = new DiagnosticsMetrics()
28
+ * diagnosticsMetrics.initMetricClient()
29
+ *
30
+ * // Record latency with operation type in attributes
31
+ * const start = process.hrtime()
32
+ * // ... do work ...
33
+ * diagnosticsMetrics.recordLatency(process.hrtime(start), { operation: 'api-call', status: '2xx' })
34
+ *
35
+ * // Or from milliseconds
36
+ * diagnosticsMetrics.recordLatency(42.5, { operation: 'db-query', status: 'success' })
37
+ *
38
+ * // Increment a counter
39
+ * diagnosticsMetrics.incrementCounter('http_requests_total', 1, { method: 'GET', status: '2xx' })
40
+ *
41
+ * // Set a gauge value
42
+ * diagnosticsMetrics.setGauge('cache_items_current', 1024, { cache: 'pages' })
43
+ *
44
+ * // Using base attributes for request context
45
+ * await diagnosticsMetrics.runWithBaseAttributes(
46
+ * { 'vtex.account.name': 'mystore', status_code: 200 },
47
+ * async () => {
48
+ * // All metrics recorded here will include the base attributes
49
+ * diagnosticsMetrics.recordLatency(100, { operation: 'custom-op' })
50
+ * // Result: { 'vtex.account.name': 'mystore', status_code: 200, operation: 'custom-op' }
51
+ * }
52
+ * )
53
+ * ```
54
+ */
55
+ export declare class DiagnosticsMetrics {
56
+ private metricsClient;
57
+ private clientInitPromise;
58
+ private latencyHistogram;
59
+ private counters;
60
+ private gauges;
61
+ constructor();
62
+ /**
63
+ * Initialize the metrics client with timeout handling.
64
+ * Called automatically in constructor.
65
+ */
66
+ private initMetricClient;
67
+ /**
68
+ * Create the single shared histogram for all latency measurements.
69
+ * Called after metric client is initialized.
70
+ */
71
+ private createLatencyHistogram;
72
+ /**
73
+ * Execute a function with base attributes set in the OpenTelemetry context.
74
+ * All metric calls within the function will automatically include these base attributes,
75
+ * merged with any custom attributes provided in each call.
76
+ *
77
+ * Base attributes take precedence over custom attributes when there are key conflicts.
78
+ * Conflicting custom attributes are silently dropped.
79
+ *
80
+ * @param baseAttributes Base attributes to include in all metrics within the scope
81
+ * @param fn Function to execute with the base attributes context
82
+ * @returns The return value of the function
83
+ *
84
+ * @example
85
+ * ```typescript
86
+ * // In a request middleware
87
+ * await diagnosticsMetrics.runWithBaseAttributes(
88
+ * {
89
+ * 'vtex.account.name': ctx.vtex.account,
90
+ * status_code: ctx.status,
91
+ * route_id: ctx.vtex.route.id,
92
+ * },
93
+ * async () => {
94
+ * await next()
95
+ * }
96
+ * )
97
+ *
98
+ * // In app code (custom attributes are merged with base)
99
+ * diagnosticsMetrics.recordLatency(elapsed, { operation: 'my-operation', status: 'success' })
100
+ * // Result includes both base attributes AND custom attributes
101
+ * ```
102
+ */
103
+ runWithBaseAttributes<T>(baseAttributes: Attributes, fn: () => T): T;
104
+ /**
105
+ * Get the base attributes from the current OpenTelemetry context.
106
+ * Returns undefined if no base attributes are set.
107
+ */
108
+ private getBaseAttributes;
109
+ /**
110
+ * Merge base attributes from context with provided custom attributes.
111
+ * Base attributes take precedence over custom attributes when there are key conflicts.
112
+ *
113
+ * Custom attributes are limited to MAX_CUSTOM_ATTRIBUTES before merging.
114
+ * Custom attributes with keys that conflict with base attributes are silently dropped.
115
+ * Base attributes are not limited.
116
+ *
117
+ * @param customAttributes Custom attributes provided by the caller
118
+ * @returns Merged attributes (base + non-conflicting limited custom) or undefined if both are empty
119
+ */
120
+ private mergeAttributes;
121
+ /**
122
+ * Record a latency measurement using the single shared histogram.
123
+ * Accepts either an hrtime tuple from process.hrtime() or milliseconds as a number.
124
+ * Use attributes to differentiate between different operations.
125
+ *
126
+ * Base attributes from the current context (set via `runWithBaseAttributes`) are
127
+ * automatically merged with the provided custom attributes. Base attributes take
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.
132
+ *
133
+ * @param value Either [seconds, nanoseconds] from process.hrtime() or milliseconds
134
+ * @param attributes Custom attributes including 'operation' to identify the operation type (max 5 custom attributes)
135
+ *
136
+ * @example
137
+ * ```typescript
138
+ * const start = process.hrtime()
139
+ * // ... do work ...
140
+ * diagnosticsMetrics.recordLatency(process.hrtime(start), { operation: 'api-call', status: '2xx' })
141
+ *
142
+ * // Or with milliseconds
143
+ * diagnosticsMetrics.recordLatency(42.5, { operation: 'db-query', status: 'success' })
144
+ * ```
145
+ */
146
+ recordLatency(value: [number, number] | number, attributes?: Attributes): void;
147
+ /**
148
+ * Increment a counter by a specific value.
149
+ * Multiple counters are stored by name since counters represent different types of events.
150
+ *
151
+ * Base attributes from the current context (set via `runWithBaseAttributes`) are
152
+ * automatically merged with the provided custom attributes. Base attributes take
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.
157
+ *
158
+ * @param name Counter name (e.g., 'http_requests_total', 'cache_hits_total')
159
+ * @param value Amount to increment by (typically 1)
160
+ * @param attributes Optional custom attributes for the counter (max 5 custom attributes, e.g., { method: 'GET', status: '2xx' })
161
+ *
162
+ * @example
163
+ * ```typescript
164
+ * diagnosticsMetrics.incrementCounter('http_requests_total', 1, { method: 'GET', status: '2xx' })
165
+ * ```
166
+ */
167
+ incrementCounter(name: string, value: number, attributes?: Attributes): void;
168
+ /**
169
+ * Set a gauge to a specific value (current state).
170
+ * Multiple gauges are stored by name since gauges represent different types of measurements.
171
+ *
172
+ * Base attributes from the current context (set via `runWithBaseAttributes`) are
173
+ * automatically merged with the provided custom attributes. Base attributes take
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.
178
+ *
179
+ * @param name Gauge name (e.g., 'cache_items_current', 'memory_usage_bytes')
180
+ * @param value Current value
181
+ * @param attributes Optional custom attributes for the gauge (max 5 custom attributes, e.g., { cache: 'pages' })
182
+ *
183
+ * @example
184
+ * ```typescript
185
+ * diagnosticsMetrics.setGauge('cache_items_current', 1024, { cache: 'pages' })
186
+ * ```
187
+ */
188
+ setGauge(name: string, value: number, attributes?: Attributes): void;
189
+ }
@@ -0,0 +1,342 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DiagnosticsMetrics = void 0;
4
+ const api_1 = require("@opentelemetry/api");
5
+ const constants_1 = require("../constants");
6
+ const client_1 = require("../service/metrics/client");
7
+ /**
8
+ * Maximum number of custom attributes allowed per metric call to control cardinality.
9
+ * This limit applies only to custom attributes provided by callers (VTEX IO Apps).
10
+ * Base attributes (set via runWithBaseAttributes) are not counted toward this limit.
11
+ *
12
+ * Total attributes sent = base attributes + custom attributes (up to MAX_CUSTOM_ATTRIBUTES)
13
+ */
14
+ const MAX_CUSTOM_ATTRIBUTES = 7;
15
+ /**
16
+ * Context key for storing base attributes in OpenTelemetry context.
17
+ * These attributes are automatically merged with custom attributes in all metric methods.
18
+ */
19
+ const BASE_ATTRIBUTES_KEY = (0, api_1.createContextKey)('vtex.metrics.baseAttributes');
20
+ /**
21
+ * Converts an hrtime tuple [seconds, nanoseconds] to milliseconds.
22
+ */
23
+ function hrtimeToMillis(hrtime) {
24
+ return (hrtime[0] * 1e3) + (hrtime[1] / 1e6);
25
+ }
26
+ /**
27
+ * Limits the number of custom attributes to prevent high cardinality.
28
+ * Takes the first MAX_CUSTOM_ATTRIBUTES entries if the limit is exceeded.
29
+ *
30
+ * Note: This limit applies only to custom attributes. Base attributes are not limited.
31
+ *
32
+ * @param customAttributes Optional custom attributes object
33
+ * @returns Limited custom attributes object or undefined
34
+ */
35
+ function limitCustomAttributes(customAttributes) {
36
+ if (!customAttributes) {
37
+ return undefined;
38
+ }
39
+ const entries = Object.entries(customAttributes);
40
+ if (entries.length <= MAX_CUSTOM_ATTRIBUTES) {
41
+ return customAttributes;
42
+ }
43
+ if (constants_1.LINKED) {
44
+ console.warn(`Custom attribute limit exceeded: ${entries.length} custom attributes provided, using only the first ${MAX_CUSTOM_ATTRIBUTES}. ` +
45
+ `Consider reducing the number of custom attributes to avoid high cardinality. `);
46
+ }
47
+ return Object.fromEntries(entries.slice(0, MAX_CUSTOM_ATTRIBUTES));
48
+ }
49
+ /**
50
+ * DiagnosticsMetrics provides a high-level API for recording metrics using
51
+ * the @vtex/diagnostics-nodejs library. It completely abstracts instrument
52
+ * management, bucket configuration, and lifecycle.
53
+ *
54
+ * Uses a single histogram for all latency measurements with attributes to differentiate.
55
+ * This follows OpenTelemetry best practices and reduces metric cardinality.
56
+ *
57
+ * ## Base Attributes (Request Context)
58
+ *
59
+ * DiagnosticsMetrics supports automatic merging of request-scoped "base attributes"
60
+ * with custom attributes provided in each metric call. This is useful for automatically
61
+ * including request context (account, status_code, route_id, etc.) in all metrics
62
+ * recorded during a request lifecycle.
63
+ *
64
+ * Use `runWithBaseAttributes()` to set base attributes for a scope. All metric calls
65
+ * within that scope will automatically include these base attributes, merged with
66
+ * any custom attributes provided.
67
+ *
68
+ * **Important:** Base attributes take precedence over custom attributes. If a custom
69
+ * attribute has the same key as a base attribute, the custom attribute is silently
70
+ * dropped and the base attribute value is used.
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * const diagnosticsMetrics = new DiagnosticsMetrics()
75
+ * diagnosticsMetrics.initMetricClient()
76
+ *
77
+ * // Record latency with operation type in attributes
78
+ * const start = process.hrtime()
79
+ * // ... do work ...
80
+ * diagnosticsMetrics.recordLatency(process.hrtime(start), { operation: 'api-call', status: '2xx' })
81
+ *
82
+ * // Or from milliseconds
83
+ * diagnosticsMetrics.recordLatency(42.5, { operation: 'db-query', status: 'success' })
84
+ *
85
+ * // Increment a counter
86
+ * diagnosticsMetrics.incrementCounter('http_requests_total', 1, { method: 'GET', status: '2xx' })
87
+ *
88
+ * // Set a gauge value
89
+ * diagnosticsMetrics.setGauge('cache_items_current', 1024, { cache: 'pages' })
90
+ *
91
+ * // Using base attributes for request context
92
+ * await diagnosticsMetrics.runWithBaseAttributes(
93
+ * { 'vtex.account.name': 'mystore', status_code: 200 },
94
+ * async () => {
95
+ * // All metrics recorded here will include the base attributes
96
+ * diagnosticsMetrics.recordLatency(100, { operation: 'custom-op' })
97
+ * // Result: { 'vtex.account.name': 'mystore', status_code: 200, operation: 'custom-op' }
98
+ * }
99
+ * )
100
+ * ```
101
+ */
102
+ class DiagnosticsMetrics {
103
+ constructor() {
104
+ this.counters = new Map();
105
+ this.gauges = new Map();
106
+ this.initMetricClient();
107
+ }
108
+ /**
109
+ * Initialize the metrics client with timeout handling.
110
+ * Called automatically in constructor.
111
+ */
112
+ initMetricClient() {
113
+ if (this.clientInitPromise) {
114
+ return this.clientInitPromise;
115
+ }
116
+ this.clientInitPromise = (async () => {
117
+ try {
118
+ const timeoutPromise = new Promise((_, reject) => {
119
+ setTimeout(() => reject(new Error('Metric client initialization timeout')), constants_1.METRIC_CLIENT_INIT_TIMEOUT_MS);
120
+ });
121
+ this.metricsClient = await Promise.race([
122
+ (0, client_1.getMetricClient)(),
123
+ timeoutPromise,
124
+ ]);
125
+ // Create the single latency histogram after client is ready
126
+ this.createLatencyHistogram();
127
+ return this.metricsClient;
128
+ }
129
+ catch (error) {
130
+ console.error('Failed to initialize metric client:', error);
131
+ return undefined;
132
+ }
133
+ })();
134
+ return this.clientInitPromise;
135
+ }
136
+ /**
137
+ * Create the single shared histogram for all latency measurements.
138
+ * Called after metric client is initialized.
139
+ */
140
+ createLatencyHistogram() {
141
+ if (!this.metricsClient) {
142
+ return;
143
+ }
144
+ this.latencyHistogram = this.metricsClient.createHistogram('io_app_operation_duration_milliseconds', {
145
+ description: 'Duration of VTEX IO app operations in milliseconds',
146
+ unit: 'ms',
147
+ });
148
+ }
149
+ /**
150
+ * Execute a function with base attributes set in the OpenTelemetry context.
151
+ * All metric calls within the function will automatically include these base attributes,
152
+ * merged with any custom attributes provided in each call.
153
+ *
154
+ * Base attributes take precedence over custom attributes when there are key conflicts.
155
+ * Conflicting custom attributes are silently dropped.
156
+ *
157
+ * @param baseAttributes Base attributes to include in all metrics within the scope
158
+ * @param fn Function to execute with the base attributes context
159
+ * @returns The return value of the function
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * // In a request middleware
164
+ * await diagnosticsMetrics.runWithBaseAttributes(
165
+ * {
166
+ * 'vtex.account.name': ctx.vtex.account,
167
+ * status_code: ctx.status,
168
+ * route_id: ctx.vtex.route.id,
169
+ * },
170
+ * async () => {
171
+ * await next()
172
+ * }
173
+ * )
174
+ *
175
+ * // In app code (custom attributes are merged with base)
176
+ * diagnosticsMetrics.recordLatency(elapsed, { operation: 'my-operation', status: 'success' })
177
+ * // Result includes both base attributes AND custom attributes
178
+ * ```
179
+ */
180
+ runWithBaseAttributes(baseAttributes, fn) {
181
+ const currentContext = api_1.context.active();
182
+ const newContext = currentContext.setValue(BASE_ATTRIBUTES_KEY, baseAttributes);
183
+ return api_1.context.with(newContext, fn);
184
+ }
185
+ /**
186
+ * Get the base attributes from the current OpenTelemetry context.
187
+ * Returns undefined if no base attributes are set.
188
+ */
189
+ getBaseAttributes() {
190
+ return api_1.context.active().getValue(BASE_ATTRIBUTES_KEY);
191
+ }
192
+ /**
193
+ * Merge base attributes from context with provided custom attributes.
194
+ * Base attributes take precedence over custom attributes when there are key conflicts.
195
+ *
196
+ * Custom attributes are limited to MAX_CUSTOM_ATTRIBUTES before merging.
197
+ * Custom attributes with keys that conflict with base attributes are silently dropped.
198
+ * Base attributes are not limited.
199
+ *
200
+ * @param customAttributes Custom attributes provided by the caller
201
+ * @returns Merged attributes (base + non-conflicting limited custom) or undefined if both are empty
202
+ */
203
+ mergeAttributes(customAttributes) {
204
+ const baseAttributes = this.getBaseAttributes();
205
+ // Limit custom attributes before merging
206
+ const limitedCustomAttributes = limitCustomAttributes(customAttributes);
207
+ if (!baseAttributes && !limitedCustomAttributes) {
208
+ return undefined;
209
+ }
210
+ if (!baseAttributes) {
211
+ return limitedCustomAttributes;
212
+ }
213
+ if (!limitedCustomAttributes) {
214
+ return baseAttributes;
215
+ }
216
+ // Filter out custom attributes that conflict with base attributes (base takes precedence)
217
+ const baseKeys = new Set(Object.keys(baseAttributes));
218
+ const nonConflictingCustomAttributes = {};
219
+ for (const [key, value] of Object.entries(limitedCustomAttributes)) {
220
+ if (!baseKeys.has(key)) {
221
+ nonConflictingCustomAttributes[key] = value;
222
+ }
223
+ // Silently drop conflicting custom attributes - base attributes take precedence
224
+ }
225
+ // Merge: base attributes + non-conflicting custom attributes
226
+ return { ...baseAttributes, ...nonConflictingCustomAttributes };
227
+ }
228
+ /**
229
+ * Record a latency measurement using the single shared histogram.
230
+ * Accepts either an hrtime tuple from process.hrtime() or milliseconds as a number.
231
+ * Use attributes to differentiate between different operations.
232
+ *
233
+ * Base attributes from the current context (set via `runWithBaseAttributes`) are
234
+ * automatically merged with the provided custom attributes. Base attributes take
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.
239
+ *
240
+ * @param value Either [seconds, nanoseconds] from process.hrtime() or milliseconds
241
+ * @param attributes Custom attributes including 'operation' to identify the operation type (max 5 custom attributes)
242
+ *
243
+ * @example
244
+ * ```typescript
245
+ * const start = process.hrtime()
246
+ * // ... do work ...
247
+ * diagnosticsMetrics.recordLatency(process.hrtime(start), { operation: 'api-call', status: '2xx' })
248
+ *
249
+ * // Or with milliseconds
250
+ * diagnosticsMetrics.recordLatency(42.5, { operation: 'db-query', status: 'success' })
251
+ * ```
252
+ */
253
+ recordLatency(value, attributes) {
254
+ if (!this.latencyHistogram) {
255
+ console.warn('DiagnosticsMetrics not initialized. Call initialize() first.');
256
+ return;
257
+ }
258
+ // Convert hrtime to milliseconds if needed
259
+ const milliseconds = Array.isArray(value) ? hrtimeToMillis(value) : value;
260
+ // Merge base attributes from context with custom attributes (custom attrs are limited internally)
261
+ const mergedAttributes = this.mergeAttributes(attributes);
262
+ // Record to the single shared histogram with merged attributes
263
+ this.latencyHistogram.record(milliseconds, mergedAttributes);
264
+ }
265
+ /**
266
+ * Increment a counter by a specific value.
267
+ * Multiple counters are stored by name since counters represent different types of events.
268
+ *
269
+ * Base attributes from the current context (set via `runWithBaseAttributes`) are
270
+ * automatically merged with the provided custom attributes. Base attributes take
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.
275
+ *
276
+ * @param name Counter name (e.g., 'http_requests_total', 'cache_hits_total')
277
+ * @param value Amount to increment by (typically 1)
278
+ * @param attributes Optional custom attributes for the counter (max 5 custom attributes, e.g., { method: 'GET', status: '2xx' })
279
+ *
280
+ * @example
281
+ * ```typescript
282
+ * diagnosticsMetrics.incrementCounter('http_requests_total', 1, { method: 'GET', status: '2xx' })
283
+ * ```
284
+ */
285
+ incrementCounter(name, value, attributes) {
286
+ if (!this.metricsClient) {
287
+ console.warn('DiagnosticsMetrics not initialized. Call initialize() first.');
288
+ return;
289
+ }
290
+ // Get or create counter instrument
291
+ if (!this.counters.has(name)) {
292
+ const counter = this.metricsClient.createCounter(name, {
293
+ description: `Counter for ${name}`,
294
+ unit: '1',
295
+ });
296
+ this.counters.set(name, counter);
297
+ }
298
+ // Merge base attributes from context with custom attributes (custom attrs are limited internally)
299
+ const mergedAttributes = this.mergeAttributes(attributes);
300
+ // Increment the counter
301
+ this.counters.get(name).add(value, mergedAttributes);
302
+ }
303
+ /**
304
+ * Set a gauge to a specific value (current state).
305
+ * Multiple gauges are stored by name since gauges represent different types of measurements.
306
+ *
307
+ * Base attributes from the current context (set via `runWithBaseAttributes`) are
308
+ * automatically merged with the provided custom attributes. Base attributes take
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.
313
+ *
314
+ * @param name Gauge name (e.g., 'cache_items_current', 'memory_usage_bytes')
315
+ * @param value Current value
316
+ * @param attributes Optional custom attributes for the gauge (max 5 custom attributes, e.g., { cache: 'pages' })
317
+ *
318
+ * @example
319
+ * ```typescript
320
+ * diagnosticsMetrics.setGauge('cache_items_current', 1024, { cache: 'pages' })
321
+ * ```
322
+ */
323
+ setGauge(name, value, attributes) {
324
+ if (!this.metricsClient) {
325
+ console.warn('DiagnosticsMetrics not initialized. Call initialize() first.');
326
+ return;
327
+ }
328
+ // Get or create gauge instrument
329
+ if (!this.gauges.has(name)) {
330
+ const gauge = this.metricsClient.createGauge(name, {
331
+ description: `Gauge for ${name}`,
332
+ unit: '1',
333
+ });
334
+ this.gauges.set(name, gauge);
335
+ }
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
+ }
341
+ }
342
+ exports.DiagnosticsMetrics = DiagnosticsMetrics;
@@ -23,11 +23,6 @@ async function initializeClient(account, workspace, appName) {
23
23
  const telemetryClient = await (0, telemetry_1.getTelemetryClient)();
24
24
  const logsConfig = diagnostics_nodejs_1.Exporters.CreateLogsExporterConfig({
25
25
  endpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
26
- path: process.env.OTEL_EXPORTER_OTLP_PATH || '/v1/logs',
27
- protocol: 'http',
28
- interval: 5,
29
- timeoutSeconds: 5,
30
- headers: { 'Content-Type': 'application/json' },
31
26
  });
32
27
  const logsExporter = diagnostics_nodejs_1.Exporters.CreateExporter(logsConfig, 'otlp');
33
28
  await logsExporter.initialize();
@@ -1,2 +1,4 @@
1
+ import { Worker } from 'cluster';
1
2
  import { ServiceJSON } from './worker/runtime/typings';
3
+ export declare const onMessage: (worker: Worker, message: any) => void;
2
4
  export declare const startMaster: (service: ServiceJSON) => void;