@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.
@@ -3,11 +3,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.startMaster = void 0;
6
+ exports.startMaster = exports.onMessage = void 0;
7
7
  const cluster_1 = __importDefault(require("cluster"));
8
8
  const os_1 = require("os");
9
9
  const constants_1 = require("../constants");
10
10
  const logger_1 = require("./logger");
11
+ const clusterMetricsAggregator_1 = require("./metrics/clusterMetricsAggregator");
11
12
  const listeners_1 = require("./worker/listeners");
12
13
  const statusTrack_1 = require("./worker/runtime/statusTrack");
13
14
  let handledSignal;
@@ -19,7 +20,12 @@ const onMessage = (worker, message) => {
19
20
  (0, statusTrack_1.trackStatus)();
20
21
  (0, statusTrack_1.broadcastStatusTrack)();
21
22
  }
22
- else {
23
+ else if ((0, clusterMetricsAggregator_1.isAggMetricsRequest)(message)) {
24
+ (0, clusterMetricsAggregator_1.handleWorkerMetricsRequest)(worker, message);
25
+ }
26
+ else if (!(0, clusterMetricsAggregator_1.isPromClientMessage)(message)) {
27
+ // prom-client's own cluster messages are handled by its cluster listener;
28
+ // anything else that reaches here is genuinely unexpected.
23
29
  listeners_1.logger.warn({
24
30
  content: message,
25
31
  message: 'Worker sent message',
@@ -27,6 +33,7 @@ const onMessage = (worker, message) => {
27
33
  });
28
34
  }
29
35
  };
36
+ exports.onMessage = onMessage;
30
37
  const onExit = (worker, code, signal) => {
31
38
  if (!constants_1.LINKED && worker.exitedAfterDisconnect === false) {
32
39
  listeners_1.logger.error({
@@ -70,6 +77,11 @@ const startMaster = (service) => {
70
77
  if (service.deterministicVary) {
71
78
  process.env.DETERMINISTIC_VARY = 'true';
72
79
  }
80
+ // Set up the master-side Prometheus aggregator so workers can request a
81
+ // merged, monotonic /metrics view across the whole cluster over IPC.
82
+ if (numWorkers > 1) {
83
+ (0, clusterMetricsAggregator_1.initMasterAggregatorRegistry)();
84
+ }
73
85
  // Setup dubugger
74
86
  if (constants_1.LINKED) {
75
87
  cluster_1.default.setupMaster({ inspectPort: constants_1.INSPECT_DEBUGGER_PORT });
@@ -82,7 +94,7 @@ const startMaster = (service) => {
82
94
  }
83
95
  cluster_1.default.on('online', onOnline);
84
96
  cluster_1.default.on('exit', onExit);
85
- cluster_1.default.on('message', onMessage);
97
+ cluster_1.default.on('message', exports.onMessage);
86
98
  process.on('SIGINT', handleSignal(SIGINT_TIMEOUT_S));
87
99
  process.on('SIGTERM', handleSignal(shutdownTimeout));
88
100
  };
@@ -0,0 +1,2 @@
1
+ import { Types } from '@vtex/diagnostics-nodejs';
2
+ export declare const getMetricClient: () => Promise<Types.MetricClient>;
@@ -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,72 @@
1
+ import { Worker } from 'cluster';
2
+ import { AggregatorRegistry } from 'prom-client';
3
+ /**
4
+ * Cluster-wide Prometheus metrics aggregation.
5
+ *
6
+ * In multi-worker mode each cluster worker keeps its own prom-client default
7
+ * registry. Serving `/metrics` from a single (round-robin selected) worker
8
+ * therefore exposes only that worker's local counters, which makes Prometheus
9
+ * treat the per-scrape braid of independent counters as counter resets and
10
+ * inflates `rate()`/`increase()` by orders of magnitude.
11
+ *
12
+ * This module uses prom-client's `AggregatorRegistry` cluster IPC protocol so
13
+ * that the worker answering a scrape asks the master for a merged, monotonic
14
+ * view built from every worker's registry in one pass (no double-counting of
15
+ * the answering worker).
16
+ */
17
+ /** Tagged IPC message: worker -> master, "please build the cluster aggregate". */
18
+ export declare const AGG_METRICS_REQ = "vtex-api:aggMetricsReq";
19
+ /** Tagged IPC message: master -> worker, carries the aggregate (or an error). */
20
+ export declare const AGG_METRICS_RES = "vtex-api:aggMetricsRes";
21
+ export interface AggMetricsReqMessage {
22
+ type: typeof AGG_METRICS_REQ;
23
+ id: number;
24
+ }
25
+ export interface AggMetricsResMessage {
26
+ type: typeof AGG_METRICS_RES;
27
+ id: number;
28
+ body?: string;
29
+ error?: string;
30
+ }
31
+ export declare const isAggMetricsRequest: (message: any) => message is AggMetricsReqMessage;
32
+ export declare const isAggMetricsResponse: (message: any) => message is AggMetricsResMessage;
33
+ /**
34
+ * prom-client's cluster protocol emits its own tagged IPC messages
35
+ * (`prom-client:getMetricsReq` / `prom-client:getMetricsRes`). They are handled
36
+ * by prom-client's own cluster listeners, so our `onMessage` handlers must
37
+ * ignore them instead of warning about "unknown" messages.
38
+ */
39
+ export declare const isPromClientMessage: (message: any) => boolean;
40
+ /**
41
+ * Constructs the master-side `AggregatorRegistry` (idempotent). Its constructor
42
+ * installs prom-client's master `cluster.on('message')` collector, which is
43
+ * what makes `clusterMetrics()` work. Must be called in the master before any
44
+ * worker can answer a scrape.
45
+ */
46
+ export declare const initMasterAggregatorRegistry: () => AggregatorRegistry;
47
+ /**
48
+ * Handles an `AGG_METRICS_REQ` from a worker: builds the cluster aggregate and
49
+ * replies to that worker only. prom-client fans the request out to *all*
50
+ * connected workers (including the requester), so the requester is one of the N
51
+ * merged sources and is never double-counted.
52
+ */
53
+ export declare const handleWorkerMetricsRequest: (worker: Worker, message: AggMetricsReqMessage) => Promise<void>;
54
+ /**
55
+ * Constructs the worker-side `AggregatorRegistry` (idempotent). Its constructor
56
+ * installs prom-client's worker `process.on('message')` responder that answers
57
+ * the master's `getMetricsReq` with this worker's registry JSON.
58
+ */
59
+ export declare const ensureWorkerAggregatorRegistry: () => AggregatorRegistry;
60
+ /**
61
+ * Requests the cluster aggregate from the master over IPC and resolves with the
62
+ * merged exposition string. On timeout or any error it falls back to this
63
+ * worker's local `register.metrics()` so `/metrics` always answers promptly.
64
+ */
65
+ export declare const requestAggregatedMetrics: () => Promise<string>;
66
+ /**
67
+ * Resolves the pending `requestAggregatedMetrics` promise correlated by id when
68
+ * the master replies. On error it falls back to the local registry.
69
+ */
70
+ export declare const handleMasterMetricsResponse: (message: AggMetricsResMessage) => void;
71
+ /** Test-only helper to reset module state between test cases. */
72
+ export declare const __resetForTests: () => void;
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.__resetForTests = exports.handleMasterMetricsResponse = exports.requestAggregatedMetrics = exports.ensureWorkerAggregatorRegistry = exports.handleWorkerMetricsRequest = exports.initMasterAggregatorRegistry = exports.isPromClientMessage = exports.isAggMetricsResponse = exports.isAggMetricsRequest = exports.AGG_METRICS_RES = exports.AGG_METRICS_REQ = void 0;
4
+ const prom_client_1 = require("prom-client");
5
+ const listeners_1 = require("../worker/listeners");
6
+ /**
7
+ * Cluster-wide Prometheus metrics aggregation.
8
+ *
9
+ * In multi-worker mode each cluster worker keeps its own prom-client default
10
+ * registry. Serving `/metrics` from a single (round-robin selected) worker
11
+ * therefore exposes only that worker's local counters, which makes Prometheus
12
+ * treat the per-scrape braid of independent counters as counter resets and
13
+ * inflates `rate()`/`increase()` by orders of magnitude.
14
+ *
15
+ * This module uses prom-client's `AggregatorRegistry` cluster IPC protocol so
16
+ * that the worker answering a scrape asks the master for a merged, monotonic
17
+ * view built from every worker's registry in one pass (no double-counting of
18
+ * the answering worker).
19
+ */
20
+ /** Tagged IPC message: worker -> master, "please build the cluster aggregate". */
21
+ exports.AGG_METRICS_REQ = 'vtex-api:aggMetricsReq';
22
+ /** Tagged IPC message: master -> worker, carries the aggregate (or an error). */
23
+ exports.AGG_METRICS_RES = 'vtex-api:aggMetricsRes';
24
+ /**
25
+ * Upper bound for a worker waiting on the master aggregate. prom-client's own
26
+ * `clusterMetrics()` has a 5s timeout collecting worker registries; we guard
27
+ * slightly above that and fall back to the local registry on expiry so that
28
+ * `/metrics` never hangs or returns empty.
29
+ */
30
+ const AGG_METRICS_TIMEOUT_MS = 6000;
31
+ const isAggMetricsRequest = (message) => (message === null || message === void 0 ? void 0 : message.type) === exports.AGG_METRICS_REQ && typeof (message === null || message === void 0 ? void 0 : message.id) === 'number';
32
+ exports.isAggMetricsRequest = isAggMetricsRequest;
33
+ const isAggMetricsResponse = (message) => (message === null || message === void 0 ? void 0 : message.type) === exports.AGG_METRICS_RES && typeof (message === null || message === void 0 ? void 0 : message.id) === 'number';
34
+ exports.isAggMetricsResponse = isAggMetricsResponse;
35
+ /**
36
+ * prom-client's cluster protocol emits its own tagged IPC messages
37
+ * (`prom-client:getMetricsReq` / `prom-client:getMetricsRes`). They are handled
38
+ * by prom-client's own cluster listeners, so our `onMessage` handlers must
39
+ * ignore them instead of warning about "unknown" messages.
40
+ */
41
+ const isPromClientMessage = (message) => typeof (message === null || message === void 0 ? void 0 : message.type) === 'string' && message.type.startsWith('prom-client:');
42
+ exports.isPromClientMessage = isPromClientMessage;
43
+ // ---------------------------------------------------------------------------
44
+ // Master side
45
+ // ---------------------------------------------------------------------------
46
+ let masterAggregatorRegistry;
47
+ /**
48
+ * Constructs the master-side `AggregatorRegistry` (idempotent). Its constructor
49
+ * installs prom-client's master `cluster.on('message')` collector, which is
50
+ * what makes `clusterMetrics()` work. Must be called in the master before any
51
+ * worker can answer a scrape.
52
+ */
53
+ const initMasterAggregatorRegistry = () => {
54
+ if (!masterAggregatorRegistry) {
55
+ masterAggregatorRegistry = new prom_client_1.AggregatorRegistry();
56
+ }
57
+ return masterAggregatorRegistry;
58
+ };
59
+ exports.initMasterAggregatorRegistry = initMasterAggregatorRegistry;
60
+ /**
61
+ * Handles an `AGG_METRICS_REQ` from a worker: builds the cluster aggregate and
62
+ * replies to that worker only. prom-client fans the request out to *all*
63
+ * connected workers (including the requester), so the requester is one of the N
64
+ * merged sources and is never double-counted.
65
+ */
66
+ const handleWorkerMetricsRequest = async (worker, message) => {
67
+ var _a, _b;
68
+ const aggregator = (0, exports.initMasterAggregatorRegistry)();
69
+ let response;
70
+ try {
71
+ const body = await aggregator.clusterMetrics();
72
+ response = { type: exports.AGG_METRICS_RES, id: message.id, body };
73
+ }
74
+ catch (err) {
75
+ response = { type: exports.AGG_METRICS_RES, id: message.id, error: (_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err) };
76
+ }
77
+ // `worker.send` can throw (e.g. the worker disconnected between the request
78
+ // and the reply). This handler is invoked without `await` from the master's
79
+ // message listener, so a thrown send would surface as an unhandled rejection
80
+ // in the master process. Swallow and log it instead.
81
+ try {
82
+ worker.send(response);
83
+ }
84
+ catch (err) {
85
+ listeners_1.logger.warn({
86
+ content: (_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : String(err),
87
+ message: 'Failed to send aggregated cluster metrics to worker',
88
+ pid: process.pid,
89
+ workerId: worker.id,
90
+ });
91
+ }
92
+ };
93
+ exports.handleWorkerMetricsRequest = handleWorkerMetricsRequest;
94
+ // ---------------------------------------------------------------------------
95
+ // Worker side
96
+ // ---------------------------------------------------------------------------
97
+ let workerAggregatorRegistry;
98
+ const pendingRequests = new Map();
99
+ let requestCounter = 0;
100
+ /**
101
+ * Constructs the worker-side `AggregatorRegistry` (idempotent). Its constructor
102
+ * installs prom-client's worker `process.on('message')` responder that answers
103
+ * the master's `getMetricsReq` with this worker's registry JSON.
104
+ */
105
+ const ensureWorkerAggregatorRegistry = () => {
106
+ if (!workerAggregatorRegistry) {
107
+ workerAggregatorRegistry = new prom_client_1.AggregatorRegistry();
108
+ }
109
+ return workerAggregatorRegistry;
110
+ };
111
+ exports.ensureWorkerAggregatorRegistry = ensureWorkerAggregatorRegistry;
112
+ /**
113
+ * Requests the cluster aggregate from the master over IPC and resolves with the
114
+ * merged exposition string. On timeout or any error it falls back to this
115
+ * worker's local `register.metrics()` so `/metrics` always answers promptly.
116
+ */
117
+ const requestAggregatedMetrics = async () => {
118
+ if (typeof process.send !== 'function') {
119
+ return prom_client_1.register.metrics();
120
+ }
121
+ const id = requestCounter++;
122
+ return new Promise((resolve) => {
123
+ var _a;
124
+ const timer = setTimeout(() => {
125
+ pendingRequests.delete(id);
126
+ listeners_1.logger.warn({
127
+ message: 'Timed out waiting for aggregated cluster metrics; falling back to local registry',
128
+ pid: process.pid,
129
+ });
130
+ prom_client_1.register.metrics().then(resolve).catch(() => resolve(''));
131
+ }, AGG_METRICS_TIMEOUT_MS);
132
+ pendingRequests.set(id, { resolve, timer });
133
+ try {
134
+ process.send({ type: exports.AGG_METRICS_REQ, id });
135
+ }
136
+ catch (err) {
137
+ clearTimeout(timer);
138
+ pendingRequests.delete(id);
139
+ listeners_1.logger.warn({
140
+ content: (_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err),
141
+ message: 'Failed to request aggregated cluster metrics; falling back to local registry',
142
+ pid: process.pid,
143
+ });
144
+ prom_client_1.register.metrics().then(resolve).catch(() => resolve(''));
145
+ }
146
+ });
147
+ };
148
+ exports.requestAggregatedMetrics = requestAggregatedMetrics;
149
+ /**
150
+ * Resolves the pending `requestAggregatedMetrics` promise correlated by id when
151
+ * the master replies. On error it falls back to the local registry.
152
+ */
153
+ const handleMasterMetricsResponse = (message) => {
154
+ const pending = pendingRequests.get(message.id);
155
+ if (!pending) {
156
+ return;
157
+ }
158
+ pendingRequests.delete(message.id);
159
+ clearTimeout(pending.timer);
160
+ if (message.error != null || message.body == null) {
161
+ listeners_1.logger.warn({
162
+ content: message.error,
163
+ message: 'Master failed to aggregate cluster metrics; falling back to local registry',
164
+ pid: process.pid,
165
+ });
166
+ prom_client_1.register.metrics().then(pending.resolve).catch(() => pending.resolve(''));
167
+ return;
168
+ }
169
+ pending.resolve(message.body);
170
+ };
171
+ exports.handleMasterMetricsResponse = handleMasterMetricsResponse;
172
+ /** Test-only helper to reset module state between test cases. */
173
+ // tslint:disable-next-line:variable-name
174
+ const __resetForTests = () => {
175
+ masterAggregatorRegistry = undefined;
176
+ workerAggregatorRegistry = undefined;
177
+ pendingRequests.forEach((p) => clearTimeout(p.timer));
178
+ pendingRequests.clear();
179
+ requestCounter = 0;
180
+ };
181
+ exports.__resetForTests = __resetForTests;
@@ -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;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Label value reported for requests that never reached a named handler: unmatched
3
+ * paths (Koa answers its default 404), requests rejected before the route pipeline
4
+ * (replica-level rate limiter, errors in compress/recorder), and handlers that
5
+ * don't set `ctx.requestHandlerName`.
6
+ *
7
+ * The value must be a non-empty string. `ctx.requestHandlerName` is `undefined`
8
+ * for those requests, and Node's cluster IPC serializes each worker's registry as
9
+ * JSON, which drops properties whose value is `undefined`. The sample then reaches
10
+ * the aggregated `/metrics` with the `handler` label missing altogether, which
11
+ * Prometheus reads as `handler=""` — a distinct, unnamed series that shows up as
12
+ * an extra "Value" line in dashboards.
13
+ *
14
+ * `'undefined'` is deliberate rather than a nicer word: prom-client's local
15
+ * exposition already rendered `handler: undefined` as `handler="undefined"` before
16
+ * the cluster aggregation was introduced, so keeping that value makes the
17
+ * aggregated output match the historical series identity and keeps existing
18
+ * dashboards, filters and alerts (e.g. `handler!~"builtin:.*|undefined"`) working.
19
+ */
20
+ export declare const UNNAMED_REQUEST_HANDLER = "undefined";
21
+ /**
22
+ * Resolves the `handler` label value for a request, falling back to
23
+ * {@link UNNAMED_REQUEST_HANDLER} when the pipeline never named the handler.
24
+ * Empty strings fall back too, so the label is never emitted empty.
25
+ */
26
+ export declare const requestHandlerLabel: (requestHandlerName?: string) => string;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requestHandlerLabel = exports.UNNAMED_REQUEST_HANDLER = void 0;
4
+ /**
5
+ * Label value reported for requests that never reached a named handler: unmatched
6
+ * paths (Koa answers its default 404), requests rejected before the route pipeline
7
+ * (replica-level rate limiter, errors in compress/recorder), and handlers that
8
+ * don't set `ctx.requestHandlerName`.
9
+ *
10
+ * The value must be a non-empty string. `ctx.requestHandlerName` is `undefined`
11
+ * for those requests, and Node's cluster IPC serializes each worker's registry as
12
+ * JSON, which drops properties whose value is `undefined`. The sample then reaches
13
+ * the aggregated `/metrics` with the `handler` label missing altogether, which
14
+ * Prometheus reads as `handler=""` — a distinct, unnamed series that shows up as
15
+ * an extra "Value" line in dashboards.
16
+ *
17
+ * `'undefined'` is deliberate rather than a nicer word: prom-client's local
18
+ * exposition already rendered `handler: undefined` as `handler="undefined"` before
19
+ * the cluster aggregation was introduced, so keeping that value makes the
20
+ * aggregated output match the historical series identity and keeps existing
21
+ * dashboards, filters and alerts (e.g. `handler!~"builtin:.*|undefined"`) working.
22
+ */
23
+ exports.UNNAMED_REQUEST_HANDLER = 'undefined';
24
+ /**
25
+ * Resolves the `handler` label value for a request, falling back to
26
+ * {@link UNNAMED_REQUEST_HANDLER} when the pipeline never named the handler.
27
+ * Empty strings fall back too, so the label is never emitted empty.
28
+ */
29
+ const requestHandlerLabel = (requestHandlerName) => requestHandlerName || exports.UNNAMED_REQUEST_HANDLER;
30
+ exports.requestHandlerLabel = requestHandlerLabel;
@@ -4,6 +4,7 @@ exports.addRequestMetricsMiddleware = void 0;
4
4
  const stream_1 = require("stream");
5
5
  const utils_1 = require("../../utils");
6
6
  const instruments_1 = require("../tracing/metrics/instruments");
7
+ const requestHandlerLabel_1 = require("./requestHandlerLabel");
7
8
  const addRequestMetricsMiddleware = () => {
8
9
  const concurrentRequests = (0, instruments_1.createConcurrentRequestsInstrument)();
9
10
  const requestTimings = (0, instruments_1.createRequestsTimingsInstrument)();
@@ -13,7 +14,7 @@ const addRequestMetricsMiddleware = () => {
13
14
  return async function addRequestMetrics(ctx, next) {
14
15
  const start = process.hrtime();
15
16
  concurrentRequests.inc(1);
16
- ctx.req.once('aborted', () => abortedRequests.inc({ ["handler" /* RequestsMetricLabels.REQUEST_HANDLER */]: ctx.requestHandlerName }, 1));
17
+ ctx.req.once('aborted', () => abortedRequests.inc({ ["handler" /* RequestsMetricLabels.REQUEST_HANDLER */]: (0, requestHandlerLabel_1.requestHandlerLabel)(ctx.requestHandlerName) }, 1));
17
18
  let responseClosed = false;
18
19
  ctx.res.once('close', () => (responseClosed = true));
19
20
  try {
@@ -22,15 +23,15 @@ const addRequestMetricsMiddleware = () => {
22
23
  finally {
23
24
  const responseLength = ctx.response.length;
24
25
  if (responseLength) {
25
- responseSizes.observe({ ["handler" /* RequestsMetricLabels.REQUEST_HANDLER */]: ctx.requestHandlerName }, responseLength);
26
+ responseSizes.observe({ ["handler" /* RequestsMetricLabels.REQUEST_HANDLER */]: (0, requestHandlerLabel_1.requestHandlerLabel)(ctx.requestHandlerName) }, responseLength);
26
27
  }
27
28
  totalRequests.inc({
28
- ["handler" /* RequestsMetricLabels.REQUEST_HANDLER */]: ctx.requestHandlerName,
29
+ ["handler" /* RequestsMetricLabels.REQUEST_HANDLER */]: (0, requestHandlerLabel_1.requestHandlerLabel)(ctx.requestHandlerName),
29
30
  ["status_code" /* RequestsMetricLabels.STATUS_CODE */]: ctx.response.status,
30
31
  }, 1);
31
32
  const onResFinished = () => {
32
33
  requestTimings.observe({
33
- ["handler" /* RequestsMetricLabels.REQUEST_HANDLER */]: ctx.requestHandlerName,
34
+ ["handler" /* RequestsMetricLabels.REQUEST_HANDLER */]: (0, requestHandlerLabel_1.requestHandlerLabel)(ctx.requestHandlerName),
34
35
  }, (0, utils_1.hrToMillisFloat)(process.hrtime(start)));
35
36
  concurrentRequests.dec(1);
36
37
  };
@@ -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 resetTelemetryClient(): void;
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.resetTelemetryClient = exports.getTelemetryClient = void 0;
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
- async initTelemetryClient() {
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', constants_1.APP.ID || 'vtex-app', {
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
- 'environment': process.env.VTEX_WORKSPACE || 'development',
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.telemetryClient = telemetryClient;
25
- return telemetryClient;
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 client:', error);
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().getClient();
117
+ return TelemetryClientSingleton.getInstance().getRawTelemetryClient();
52
118
  }
53
119
  exports.getTelemetryClient = getTelemetryClient;
54
- function resetTelemetryClient() {
120
+ function resetTelemetry() {
55
121
  TelemetryClientSingleton.getInstance().reset();
56
122
  }
57
- exports.resetTelemetryClient = resetTelemetryClient;
123
+ exports.resetTelemetry = resetTelemetry;
@@ -0,0 +1 @@
1
+ export declare const getClusterResourceAttributes: (clusterId?: string, clusterRole?: string) => Record<string, string>;