@vtex/api 6.52.0 → 6.53.0-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 +11 -1
- package/lib/HttpClient/middlewares/metrics.js +52 -5
- package/lib/HttpClient/middlewares/request/HttpAgentSingleton.d.ts +5 -0
- package/lib/HttpClient/middlewares/request/HttpAgentSingleton.js +15 -0
- package/lib/constants.d.ts +15 -0
- package/lib/constants.js +18 -1
- package/lib/metrics/DiagnosticsMetrics.d.ts +189 -0
- package/lib/metrics/DiagnosticsMetrics.js +342 -0
- package/lib/service/index.d.ts +9 -1
- package/lib/service/index.js +5 -1
- package/lib/service/logger/client.js +0 -5
- package/lib/service/metrics/client.d.ts +2 -0
- package/lib/service/metrics/client.js +38 -0
- package/lib/service/metrics/instruments/hostMetrics.d.ts +14 -0
- package/lib/service/metrics/instruments/hostMetrics.js +33 -0
- package/lib/service/telemetry/client.d.ts +9 -1
- package/lib/service/telemetry/client.js +93 -27
- package/lib/service/telemetry/resourceAttributes.d.ts +1 -0
- package/lib/service/telemetry/resourceAttributes.js +21 -0
- package/lib/service/worker/runtime/graphql/schema/schemaDirectives/Metric.js +20 -2
- package/lib/service/worker/runtime/http/middlewares/requestStats.js +42 -2
- package/lib/service/worker/runtime/http/middlewares/timings.js +49 -9
- package/lib/service/worker/runtime/statusTrack.js +3 -0
- package/package.json +7 -2
|
@@ -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;
|
package/lib/service/index.d.ts
CHANGED
|
@@ -1,2 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import { DiagnosticsMetrics } from '../metrics/DiagnosticsMetrics';
|
|
2
|
+
export declare const startApp: () => Promise<void>;
|
|
2
3
|
export { appPath } from './loaders';
|
|
4
|
+
declare global {
|
|
5
|
+
namespace NodeJS {
|
|
6
|
+
interface Global {
|
|
7
|
+
diagnosticsMetrics: DiagnosticsMetrics;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
package/lib/service/index.js
CHANGED
|
@@ -6,11 +6,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.appPath = exports.startApp = void 0;
|
|
7
7
|
const cluster_1 = __importDefault(require("cluster"));
|
|
8
8
|
const constants_1 = require("../constants");
|
|
9
|
+
const DiagnosticsMetrics_1 = require("../metrics/DiagnosticsMetrics");
|
|
9
10
|
const loaders_1 = require("./loaders");
|
|
10
11
|
const logger_1 = require("./logger");
|
|
11
12
|
const master_1 = require("./master");
|
|
13
|
+
const telemetry_1 = require("./telemetry");
|
|
12
14
|
const worker_1 = require("./worker");
|
|
13
|
-
const startApp = () => {
|
|
15
|
+
const startApp = async () => {
|
|
16
|
+
await (0, telemetry_1.initializeTelemetry)();
|
|
17
|
+
global.diagnosticsMetrics = new DiagnosticsMetrics_1.DiagnosticsMetrics();
|
|
14
18
|
const serviceJSON = (0, loaders_1.getServiceJSON)();
|
|
15
19
|
try {
|
|
16
20
|
// if it is a master process then call setting up worker process
|
|
@@ -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();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getMetricClient = void 0;
|
|
4
|
+
const telemetry_1 = require("../telemetry");
|
|
5
|
+
class MetricClientSingleton {
|
|
6
|
+
constructor() { }
|
|
7
|
+
static getInstance() {
|
|
8
|
+
if (!MetricClientSingleton.instance) {
|
|
9
|
+
MetricClientSingleton.instance = new MetricClientSingleton();
|
|
10
|
+
}
|
|
11
|
+
return MetricClientSingleton.instance;
|
|
12
|
+
}
|
|
13
|
+
async getClient() {
|
|
14
|
+
if (this.client) {
|
|
15
|
+
return this.client;
|
|
16
|
+
}
|
|
17
|
+
if (this.initPromise) {
|
|
18
|
+
return this.initPromise;
|
|
19
|
+
}
|
|
20
|
+
this.initPromise = this.initializeClient();
|
|
21
|
+
return this.initPromise;
|
|
22
|
+
}
|
|
23
|
+
async initializeClient() {
|
|
24
|
+
try {
|
|
25
|
+
const { metricsClient } = await (0, telemetry_1.initializeTelemetry)();
|
|
26
|
+
this.client = metricsClient;
|
|
27
|
+
this.initPromise = undefined;
|
|
28
|
+
return metricsClient;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.error('Failed to initialize metrics client:', error);
|
|
32
|
+
this.initPromise = undefined;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const getMetricClient = () => MetricClientSingleton.getInstance().getClient();
|
|
38
|
+
exports.getMetricClient = getMetricClient;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { MeterProvider } from '@opentelemetry/api';
|
|
2
|
+
import { InstrumentationBase, InstrumentationConfig } from '@opentelemetry/instrumentation';
|
|
3
|
+
interface HostMetricsInstrumentationConfig extends InstrumentationConfig {
|
|
4
|
+
name?: string;
|
|
5
|
+
meterProvider?: MeterProvider;
|
|
6
|
+
}
|
|
7
|
+
export declare class HostMetricsInstrumentation extends InstrumentationBase<HostMetricsInstrumentationConfig> {
|
|
8
|
+
private hostMetrics?;
|
|
9
|
+
constructor(config?: HostMetricsInstrumentationConfig);
|
|
10
|
+
init(): void;
|
|
11
|
+
enable(): void;
|
|
12
|
+
disable(): void;
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HostMetricsInstrumentation = void 0;
|
|
4
|
+
const host_metrics_1 = require("@opentelemetry/host-metrics");
|
|
5
|
+
const instrumentation_1 = require("@opentelemetry/instrumentation");
|
|
6
|
+
class HostMetricsInstrumentation extends instrumentation_1.InstrumentationBase {
|
|
7
|
+
constructor(config = {}) {
|
|
8
|
+
const instrumentationName = config.name || 'host-metrics-instrumentation';
|
|
9
|
+
const instrumentationVersion = '1.0.0';
|
|
10
|
+
super(instrumentationName, instrumentationVersion, config);
|
|
11
|
+
}
|
|
12
|
+
init() {
|
|
13
|
+
// No-op: instrumentation is started explicitly in enable().
|
|
14
|
+
}
|
|
15
|
+
enable() {
|
|
16
|
+
if (!this._config.meterProvider) {
|
|
17
|
+
throw new Error('MeterProvider is required for HostMetricsInstrumentation');
|
|
18
|
+
}
|
|
19
|
+
this.hostMetrics = new host_metrics_1.HostMetrics({
|
|
20
|
+
meterProvider: this._config.meterProvider,
|
|
21
|
+
name: this._config.name || 'host-metrics',
|
|
22
|
+
});
|
|
23
|
+
this.hostMetrics.start();
|
|
24
|
+
console.debug('HostMetricsInstrumentation enabled');
|
|
25
|
+
}
|
|
26
|
+
disable() {
|
|
27
|
+
if (this.hostMetrics) {
|
|
28
|
+
this.hostMetrics = undefined;
|
|
29
|
+
console.debug('HostMetricsInstrumentation disabled');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.HostMetricsInstrumentation = HostMetricsInstrumentation;
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import { Logs, Metrics, Traces } from '@vtex/diagnostics-nodejs';
|
|
1
2
|
import { TelemetryClient } from '@vtex/diagnostics-nodejs/dist/telemetry';
|
|
3
|
+
interface TelemetryClients {
|
|
4
|
+
logsClient: Logs.LogClient;
|
|
5
|
+
metricsClient: Metrics.MetricsClient;
|
|
6
|
+
tracesClient: Traces.TraceClient;
|
|
7
|
+
}
|
|
8
|
+
export declare function initializeTelemetry(): Promise<TelemetryClients>;
|
|
2
9
|
export declare function getTelemetryClient(): Promise<TelemetryClient>;
|
|
3
|
-
export declare function
|
|
10
|
+
export declare function resetTelemetry(): void;
|
|
11
|
+
export {};
|
|
@@ -1,57 +1,123 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.resetTelemetry = exports.getTelemetryClient = exports.initializeTelemetry = void 0;
|
|
4
|
+
const instrumentation_koa_1 = require("@opentelemetry/instrumentation-koa");
|
|
4
5
|
const diagnostics_nodejs_1 = require("@vtex/diagnostics-nodejs");
|
|
5
6
|
const constants_1 = require("../../constants");
|
|
7
|
+
const hostMetrics_1 = require("../metrics/instruments/hostMetrics");
|
|
8
|
+
const resourceAttributes_1 = require("./resourceAttributes");
|
|
9
|
+
const APPLICATION_ID = constants_1.APP.ID || 'vtex-io-app';
|
|
6
10
|
class TelemetryClientSingleton {
|
|
7
|
-
constructor() {
|
|
8
|
-
this.initializationPromise = undefined;
|
|
9
|
-
}
|
|
10
11
|
static getInstance() {
|
|
11
12
|
if (!TelemetryClientSingleton.instance) {
|
|
12
13
|
TelemetryClientSingleton.instance = new TelemetryClientSingleton();
|
|
13
14
|
}
|
|
14
15
|
return TelemetryClientSingleton.instance;
|
|
15
16
|
}
|
|
16
|
-
|
|
17
|
+
constructor() {
|
|
18
|
+
this.initializationPromise = undefined;
|
|
19
|
+
this.initializeTracesClient = async (telemetryClient) => await telemetryClient.newTracesClient({
|
|
20
|
+
exporter: diagnostics_nodejs_1.Exporters.CreateExporter(diagnostics_nodejs_1.Exporters.CreateTracesExporterConfig({
|
|
21
|
+
endpoint: constants_1.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
22
|
+
}), 'otlp'),
|
|
23
|
+
});
|
|
24
|
+
this.initializeMetricsClient = async (telemetryClient) => await telemetryClient.newMetricsClient({
|
|
25
|
+
exporter: diagnostics_nodejs_1.Exporters.CreateExporter(diagnostics_nodejs_1.Exporters.CreateMetricsExporterConfig({
|
|
26
|
+
endpoint: constants_1.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
27
|
+
interval: 60,
|
|
28
|
+
temporality: 'delta',
|
|
29
|
+
timeoutSeconds: 60,
|
|
30
|
+
}), 'otlp'),
|
|
31
|
+
});
|
|
32
|
+
this.initializeLogsClient = async (telemetryClient) => await telemetryClient.newLogsClient({
|
|
33
|
+
exporter: diagnostics_nodejs_1.Exporters.CreateExporter(diagnostics_nodejs_1.Exporters.CreateLogsExporterConfig({
|
|
34
|
+
endpoint: constants_1.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
35
|
+
}), 'otlp'),
|
|
36
|
+
loggerName: `node-vtex-api-${APPLICATION_ID}`,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async getTelemetryClients() {
|
|
40
|
+
if (this.telemetryClients) {
|
|
41
|
+
return this.telemetryClients;
|
|
42
|
+
}
|
|
43
|
+
if (this.initializationPromise) {
|
|
44
|
+
return this.initializationPromise;
|
|
45
|
+
}
|
|
46
|
+
this.initializationPromise = this.initializeTelemetryClients();
|
|
47
|
+
return this.initializationPromise;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Exposes the raw @vtex/diagnostics-nodejs TelemetryClient (rather than one of the three
|
|
51
|
+
* pre-built traces/metrics/logs clients) for consumers that need to build their own client
|
|
52
|
+
* with per-call configuration — e.g. the structured logger's per-request loggerName.
|
|
53
|
+
*/
|
|
54
|
+
async getRawTelemetryClient() {
|
|
55
|
+
await this.getTelemetryClients();
|
|
56
|
+
return this.rawTelemetryClient;
|
|
57
|
+
}
|
|
58
|
+
reset() {
|
|
59
|
+
this.telemetryClients = undefined;
|
|
60
|
+
this.rawTelemetryClient = undefined;
|
|
61
|
+
this.initializationPromise = undefined;
|
|
62
|
+
}
|
|
63
|
+
async initializeTelemetryClients() {
|
|
17
64
|
try {
|
|
18
|
-
const telemetryClient = await (0, diagnostics_nodejs_1.NewTelemetryClient)('node-vtex-api',
|
|
65
|
+
const telemetryClient = await (0, diagnostics_nodejs_1.NewTelemetryClient)(constants_1.DK_APP_ID, 'node-vtex-api', APPLICATION_ID, {
|
|
19
66
|
additionalAttrs: {
|
|
67
|
+
[constants_1.AttributeKeys.VTEX_IO_APP_ID]: APPLICATION_ID,
|
|
68
|
+
'vendor': constants_1.APP.VENDOR,
|
|
20
69
|
'version': constants_1.APP.VERSION || '',
|
|
21
|
-
|
|
70
|
+
[constants_1.AttributeKeys.VTEX_IO_WORKSPACE_NAME]: constants_1.WORKSPACE,
|
|
71
|
+
[constants_1.AttributeKeys.VTEX_IO_WORKSPACE_TYPE]: constants_1.PRODUCTION ? 'production' : 'development',
|
|
72
|
+
...(0, resourceAttributes_1.getClusterResourceAttributes)(constants_1.CLUSTER_ID, constants_1.CLUSTER_ROLE),
|
|
22
73
|
},
|
|
74
|
+
// Use built-in no-op functionality when telemetry is disabled
|
|
75
|
+
noop: !constants_1.DIAGNOSTICS_TELEMETRY_ENABLED,
|
|
23
76
|
});
|
|
24
|
-
this.
|
|
25
|
-
|
|
77
|
+
this.rawTelemetryClient = telemetryClient;
|
|
78
|
+
const [tracesClient, metricsClient, logsClient] = await Promise.all([
|
|
79
|
+
this.initializeTracesClient(telemetryClient),
|
|
80
|
+
this.initializeMetricsClient(telemetryClient),
|
|
81
|
+
this.initializeLogsClient(telemetryClient),
|
|
82
|
+
]);
|
|
83
|
+
if (constants_1.DIAGNOSTICS_TELEMETRY_ENABLED) {
|
|
84
|
+
console.log(`Telemetry enabled for app: ${constants_1.APP.ID} (vendor: ${constants_1.APP.VENDOR})`);
|
|
85
|
+
const instrumentations = [
|
|
86
|
+
...diagnostics_nodejs_1.Instrumentation.CommonInstrumentations.minimal(),
|
|
87
|
+
new instrumentation_koa_1.KoaInstrumentation(),
|
|
88
|
+
new hostMetrics_1.HostMetricsInstrumentation({
|
|
89
|
+
meterProvider: metricsClient.provider(),
|
|
90
|
+
name: 'host-metrics-instrumentation',
|
|
91
|
+
}),
|
|
92
|
+
];
|
|
93
|
+
telemetryClient.registerInstrumentations(instrumentations);
|
|
94
|
+
}
|
|
95
|
+
const clients = {
|
|
96
|
+
logsClient,
|
|
97
|
+
metricsClient,
|
|
98
|
+
tracesClient,
|
|
99
|
+
};
|
|
100
|
+
this.telemetryClients = clients;
|
|
101
|
+
return clients;
|
|
26
102
|
}
|
|
27
103
|
catch (error) {
|
|
28
|
-
console.error('Failed to initialize telemetry
|
|
104
|
+
console.error('Failed to initialize telemetry clients:', error);
|
|
29
105
|
throw error;
|
|
30
106
|
}
|
|
31
107
|
finally {
|
|
32
108
|
this.initializationPromise = undefined;
|
|
33
109
|
}
|
|
34
110
|
}
|
|
35
|
-
async getClient() {
|
|
36
|
-
if (this.telemetryClient) {
|
|
37
|
-
return this.telemetryClient;
|
|
38
|
-
}
|
|
39
|
-
if (this.initializationPromise) {
|
|
40
|
-
return this.initializationPromise;
|
|
41
|
-
}
|
|
42
|
-
this.initializationPromise = this.initTelemetryClient();
|
|
43
|
-
return this.initializationPromise;
|
|
44
|
-
}
|
|
45
|
-
reset() {
|
|
46
|
-
this.telemetryClient = undefined;
|
|
47
|
-
this.initializationPromise = undefined;
|
|
48
|
-
}
|
|
49
111
|
}
|
|
112
|
+
async function initializeTelemetry() {
|
|
113
|
+
return TelemetryClientSingleton.getInstance().getTelemetryClients();
|
|
114
|
+
}
|
|
115
|
+
exports.initializeTelemetry = initializeTelemetry;
|
|
50
116
|
async function getTelemetryClient() {
|
|
51
|
-
return TelemetryClientSingleton.getInstance().
|
|
117
|
+
return TelemetryClientSingleton.getInstance().getRawTelemetryClient();
|
|
52
118
|
}
|
|
53
119
|
exports.getTelemetryClient = getTelemetryClient;
|
|
54
|
-
function
|
|
120
|
+
function resetTelemetry() {
|
|
55
121
|
TelemetryClientSingleton.getInstance().reset();
|
|
56
122
|
}
|
|
57
|
-
exports.
|
|
123
|
+
exports.resetTelemetry = resetTelemetry;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const getClusterResourceAttributes: (clusterId?: string, clusterRole?: string) => Record<string, string>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getClusterResourceAttributes = void 0;
|
|
4
|
+
const constants_1 = require("../../constants");
|
|
5
|
+
const normalizeAttribute = (value) => {
|
|
6
|
+
const normalized = value === null || value === void 0 ? void 0 : value.trim();
|
|
7
|
+
return normalized || undefined;
|
|
8
|
+
};
|
|
9
|
+
const getClusterResourceAttributes = (clusterId, clusterRole) => {
|
|
10
|
+
const attributes = {};
|
|
11
|
+
const normalizedClusterId = normalizeAttribute(clusterId);
|
|
12
|
+
const normalizedClusterRole = normalizeAttribute(clusterRole);
|
|
13
|
+
if (normalizedClusterId) {
|
|
14
|
+
attributes[constants_1.AttributeKeys.VTEX_IO_CLUSTER_ID] = normalizedClusterId;
|
|
15
|
+
}
|
|
16
|
+
if (normalizedClusterRole) {
|
|
17
|
+
attributes[constants_1.AttributeKeys.VTEX_IO_CLUSTER_ROLE] = normalizedClusterRole;
|
|
18
|
+
}
|
|
19
|
+
return attributes;
|
|
20
|
+
};
|
|
21
|
+
exports.getClusterResourceAttributes = getClusterResourceAttributes;
|