@vtex/api 7.3.1 → 7.4.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +3 -0
  3. package/lib/clients/IOClients.d.ts +2 -1
  4. package/lib/clients/IOClients.js +3 -0
  5. package/lib/clients/janus/Catalog/index.d.ts +7 -0
  6. package/lib/clients/janus/Catalog/index.js +37 -0
  7. package/lib/clients/janus/Catalog/types.d.ts +5 -0
  8. package/lib/clients/janus/Catalog/types.js +2 -0
  9. package/lib/clients/janus/{LicenseManager.d.ts → LicenseManager/index.d.ts} +6 -2
  10. package/lib/clients/janus/LicenseManager/index.js +91 -0
  11. package/lib/clients/janus/LicenseManager/types.d.ts +16 -0
  12. package/lib/clients/janus/LicenseManager/types.js +2 -0
  13. package/lib/clients/janus/LicenseManager/utils.d.ts +6 -0
  14. package/lib/clients/janus/LicenseManager/utils.js +40 -0
  15. package/lib/clients/janus/index.d.ts +1 -0
  16. package/lib/clients/janus/index.js +1 -0
  17. package/lib/service/master.d.ts +2 -0
  18. package/lib/service/master.js +15 -2
  19. package/lib/service/metrics/clusterMetricsAggregator.d.ts +72 -0
  20. package/lib/service/metrics/clusterMetricsAggregator.js +164 -0
  21. package/lib/service/metrics/metrics.d.ts +2 -2
  22. package/lib/service/metrics/metrics.js +13 -13
  23. package/lib/service/metrics/otelRequestMetricsMiddleware.js +1 -1
  24. package/lib/service/telemetry/client.js +23 -23
  25. package/lib/service/worker/index.d.ts +1 -0
  26. package/lib/service/worker/index.js +18 -5
  27. package/lib/service/worker/runtime/builtIn/middlewares.d.ts +1 -1
  28. package/lib/service/worker/runtime/builtIn/middlewares.js +7 -2
  29. package/lib/service/worker/runtime/graphql/schema/schemaDirectives/Auth.js +2 -2
  30. package/lib/service/worker/runtime/http/middlewares/timings.js +1 -1
  31. package/lib/utils/buildFullPath.js +3 -3
  32. package/package.json +2 -2
  33. package/lib/clients/janus/LicenseManager.js +0 -59
package/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [7.4.0] - 2026-06-22
11
+ ### Changed
12
+ - Renamed the base `IOClients` Janus Catalog getter from `catalogSystem` to
13
+ `janusCatalogSystem` to avoid a name collision with apps that already define
14
+ their own `catalogSystem` client (e.g. vtex-apps/promotion-cloner).
15
+ ### Added
16
+ - Base `IOClients` getter `janusCatalogSystem` (Janus Catalog) and
17
+ `licenseManager.listBindings`, plus binding-mapping helpers
18
+ (getCanonicalAndAlternateAddresses, inferTargetProduct) used by the
19
+ Tenant API migration.
20
+
10
21
  ## [7.3.1] - 2026-01-06
11
22
 
12
23
  ### Added
package/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  # VTEX IO API Client for Node
2
2
 
3
+
3
4
  This library enables developers to quickly integrate with the VTEX IO APIs and create full fledged node services using VTEX IO.
4
5
 
5
6
  [![Build Status](https://travis-ci.org/vtex/node-vtex-api.svg?branch=master)](https://travis-ci.org/vtex/node-vtex-api)
@@ -63,3 +64,5 @@ export const example = async (ctx: Context, next: () => Promise<void>) => {
63
64
  Now, when you get a workspace up and running for your app with `vtex link`, you'll have this package linked as well.
64
65
 
65
66
  > When done developing, don't forget to unlink it from `<your-app>/node`: `yarn unlink @vtex/api`
67
+
68
+ <!-- Test PR to validate SonarQube configuration -->
@@ -5,7 +5,7 @@ import { CatalogGraphQL } from './apps/catalogGraphQL/index';
5
5
  import { ID, MasterData, PaymentProvider } from './external';
6
6
  import { Apps, Assets, BillingMetrics, Events, Registry, Router, VBase, Workspaces } from './infra';
7
7
  import { IOClientConstructor } from './IOClient';
8
- import { LicenseManager, Segment, Session, TenantClient } from './janus';
8
+ import { LicenseManager, Segment, Session, TenantClient, Catalog } from './janus';
9
9
  export type ClientsImplementation<T extends IOClients> = new (clientOptions: Record<string, InstanceOptions>, ctx: IOContext) => T;
10
10
  export declare class IOClients {
11
11
  private clientOptions;
@@ -31,6 +31,7 @@ export declare class IOClients {
31
31
  get vbase(): VBase;
32
32
  get workspaces(): Workspaces;
33
33
  get catalogGraphQL(): CatalogGraphQL;
34
+ get janusCatalogSystem(): Catalog;
34
35
  get paymentProvider(): PaymentProvider;
35
36
  protected getOrSet<TClient extends IOClientConstructor>(key: string, Implementation: TClient): InstanceType<TClient>;
36
37
  }
@@ -69,6 +69,9 @@ class IOClients {
69
69
  get catalogGraphQL() {
70
70
  return this.getOrSet('catalogGraphQL', index_1.CatalogGraphQL);
71
71
  }
72
+ get janusCatalogSystem() {
73
+ return this.getOrSet('janusCatalogSystem', janus_1.Catalog);
74
+ }
72
75
  get paymentProvider() {
73
76
  return this.getOrSet('paymentProvider', external_1.PaymentProvider);
74
77
  }
@@ -0,0 +1,7 @@
1
+ import { RequestConfig } from '../../../HttpClient';
2
+ import { JanusClient } from '../JanusClient';
3
+ import { SalesChannel } from './types';
4
+ export declare class Catalog extends JanusClient {
5
+ getSalesChannel(id: number, config?: RequestConfig): Promise<SalesChannel>;
6
+ getDefaultSalesChannel(config?: RequestConfig): Promise<SalesChannel>;
7
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Catalog = void 0;
4
+ const HttpClient_1 = require("../../../HttpClient");
5
+ const JanusClient_1 = require("../JanusClient");
6
+ const BASE_URL = '/api/catalog_system';
7
+ const routes = {
8
+ salesChannel: (salesChannelId) => `${BASE_URL}/pub/saleschannel/${salesChannelId}`,
9
+ defaultSalesChannel: () => `${BASE_URL}/pub/saleschannel/default`,
10
+ };
11
+ class Catalog extends JanusClient_1.JanusClient {
12
+ getSalesChannel(id, config) {
13
+ const metric = 'catalog-saleschannel';
14
+ return this.http.get(routes.salesChannel(id), {
15
+ inflightKey: HttpClient_1.inflightUrlWithQuery,
16
+ metric,
17
+ ...config,
18
+ tracing: {
19
+ requestSpanNameSuffix: metric,
20
+ ...config === null || config === void 0 ? void 0 : config.tracing,
21
+ },
22
+ });
23
+ }
24
+ getDefaultSalesChannel(config) {
25
+ const metric = 'catalog-saleschannel-default';
26
+ return this.http.get(routes.defaultSalesChannel(), {
27
+ inflightKey: HttpClient_1.inflightUrlWithQuery,
28
+ metric,
29
+ ...config,
30
+ tracing: {
31
+ requestSpanNameSuffix: metric,
32
+ ...config === null || config === void 0 ? void 0 : config.tracing,
33
+ },
34
+ });
35
+ }
36
+ }
37
+ exports.Catalog = Catalog;
@@ -0,0 +1,5 @@
1
+ export interface SalesChannel {
2
+ Id: number;
3
+ CultureInfo: string;
4
+ CurrencyCode: string;
5
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,7 +1,11 @@
1
- import { RequestTracingConfig } from '../../HttpClient';
2
- import { JanusClient } from './JanusClient';
1
+ import { RequestTracingConfig, RequestConfig } from '../../../HttpClient';
2
+ import { JanusClient } from '../JanusClient';
3
+ import { APIBindingRes } from './types';
4
+ export * from './types';
5
+ export * from './utils';
3
6
  export declare class LicenseManager extends JanusClient {
4
7
  getAccountData(VtexIdclientAutCookie: string, tracingConfig?: RequestTracingConfig): Promise<any>;
5
8
  getTopbarData(VtexIdclientAutCookie: string, tracingConfig?: RequestTracingConfig): Promise<any>;
6
9
  canAccessResource(VtexIdclientAutCookie: string, resourceKey: string, tracingConfig?: RequestTracingConfig): Promise<boolean>;
10
+ listBindings(tenant: string, config?: RequestConfig): Promise<APIBindingRes[]>;
7
11
  }
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.LicenseManager = void 0;
18
+ const HttpClient_1 = require("../../../HttpClient");
19
+ const JanusClient_1 = require("../JanusClient");
20
+ __exportStar(require("./types"), exports);
21
+ __exportStar(require("./utils"), exports);
22
+ const TWO_MINUTES_S = 2 * 60;
23
+ const BASE_URL = '/api/license-manager';
24
+ const routes = {
25
+ accountData: () => `${BASE_URL}/account`,
26
+ resourceAccess: (resourceKey) => `${BASE_URL}/resources/${encodeURIComponent(resourceKey)}/access`,
27
+ topbarData: () => `${BASE_URL}/site/pvt/newtopbar`,
28
+ listBindings: (tenant) => `${BASE_URL}/binding/site/${encodeURIComponent(tenant)}`,
29
+ };
30
+ class LicenseManager extends JanusClient_1.JanusClient {
31
+ getAccountData(VtexIdclientAutCookie, tracingConfig) {
32
+ const metric = 'lm-account-data';
33
+ return this.http.get(routes.accountData(), {
34
+ forceMaxAge: TWO_MINUTES_S,
35
+ headers: {
36
+ VtexIdclientAutCookie,
37
+ },
38
+ inflightKey: HttpClient_1.inflightUrlWithQuery,
39
+ metric,
40
+ tracing: {
41
+ requestSpanNameSuffix: metric,
42
+ ...tracingConfig === null || tracingConfig === void 0 ? void 0 : tracingConfig.tracing,
43
+ },
44
+ });
45
+ }
46
+ getTopbarData(VtexIdclientAutCookie, tracingConfig) {
47
+ const metric = 'lm-topbar-data';
48
+ return this.http.get(routes.topbarData(), {
49
+ headers: {
50
+ VtexIdclientAutCookie,
51
+ },
52
+ metric,
53
+ tracing: {
54
+ requestSpanNameSuffix: metric,
55
+ ...tracingConfig === null || tracingConfig === void 0 ? void 0 : tracingConfig.tracing,
56
+ },
57
+ });
58
+ }
59
+ canAccessResource(VtexIdclientAutCookie, resourceKey, tracingConfig) {
60
+ const metric = 'lm-resource-access';
61
+ return this.http
62
+ .get(routes.resourceAccess(resourceKey), {
63
+ headers: {
64
+ VtexIdclientAutCookie,
65
+ },
66
+ metric,
67
+ tracing: {
68
+ requestSpanNameSuffix: metric,
69
+ ...tracingConfig === null || tracingConfig === void 0 ? void 0 : tracingConfig.tracing,
70
+ },
71
+ })
72
+ .then(() => true, () => false);
73
+ }
74
+ listBindings(tenant, config) {
75
+ const metric = 'lm-list-bindings';
76
+ return this.http.get(routes.listBindings(tenant), {
77
+ inflightKey: HttpClient_1.inflightUrlWithQuery,
78
+ memoizeable: true,
79
+ metric,
80
+ headers: {
81
+ VtexIdclientAutCookie: this.context.authToken,
82
+ },
83
+ ...config,
84
+ tracing: {
85
+ requestSpanNameSuffix: metric,
86
+ ...config === null || config === void 0 ? void 0 : config.tracing,
87
+ },
88
+ });
89
+ }
90
+ }
91
+ exports.LicenseManager = LicenseManager;
@@ -0,0 +1,16 @@
1
+ export interface APIAddress {
2
+ Host: string;
3
+ IsCanonical: boolean;
4
+ BasePath: string;
5
+ Localization: {
6
+ [k: string]: string;
7
+ };
8
+ }
9
+ export interface APIBindingRes {
10
+ Id: string;
11
+ Addresses: APIAddress[];
12
+ SiteName: string;
13
+ DefaultSalesChannelId: number | null;
14
+ DefaultLocale: string;
15
+ SupportedLocales: string[];
16
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,6 @@
1
+ import { APIBindingRes } from './types';
2
+ export declare const getCanonicalAndAlternateAddresses: (lmBinding: APIBindingRes) => {
3
+ canonicalBaseAddress: string;
4
+ alternateBaseAddresses: string[];
5
+ };
6
+ export declare const inferTargetProduct: (canonicalBaseAddress: string, alternateBaseAddresses: string[]) => 'vtex-admin' | 'vtex-storefront';
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.inferTargetProduct = exports.getCanonicalAndAlternateAddresses = void 0;
4
+ const buildAddressPath = (basePath, localizationKey) => {
5
+ if (basePath === '') {
6
+ return `/${localizationKey}`;
7
+ }
8
+ if (localizationKey === '') {
9
+ return `/${basePath}`;
10
+ }
11
+ return `/${basePath}/${localizationKey}`;
12
+ };
13
+ const getCanonicalAndAlternateAddresses = (lmBinding) => {
14
+ let canonicalBaseAddress = '';
15
+ const alternateBaseAddresses = [];
16
+ for (const addressEntry of lmBinding.Addresses) {
17
+ const localizationKeys = Object.keys(addressEntry.Localization);
18
+ for (const localizationKey of localizationKeys) {
19
+ alternateBaseAddresses.push(addressEntry.Host + buildAddressPath(addressEntry.BasePath, localizationKey));
20
+ }
21
+ if (addressEntry.IsCanonical) {
22
+ // The canonical address is built from the shortest localization key.
23
+ const [shortestLocalizationKey = ''] = [...localizationKeys].sort((a, b) => a.length - b.length);
24
+ canonicalBaseAddress = addressEntry.Host + buildAddressPath(addressEntry.BasePath, shortestLocalizationKey);
25
+ }
26
+ }
27
+ // The canonical address must not also appear in the alternates list.
28
+ const canonicalIndexInAlternates = alternateBaseAddresses.indexOf(canonicalBaseAddress);
29
+ if (canonicalIndexInAlternates !== -1) {
30
+ alternateBaseAddresses.splice(canonicalIndexInAlternates, 1);
31
+ }
32
+ return { canonicalBaseAddress, alternateBaseAddresses };
33
+ };
34
+ exports.getCanonicalAndAlternateAddresses = getCanonicalAndAlternateAddresses;
35
+ const inferTargetProduct = (canonicalBaseAddress, alternateBaseAddresses) => {
36
+ const isAdmin = canonicalBaseAddress.endsWith('myvtex.com/admin') ||
37
+ alternateBaseAddresses.some(address => address.endsWith('myvtex.com/admin'));
38
+ return isAdmin ? 'vtex-admin' : 'vtex-storefront';
39
+ };
40
+ exports.inferTargetProduct = inferTargetProduct;
@@ -3,3 +3,4 @@ export * from './LicenseManager';
3
3
  export * from './Segment';
4
4
  export * from './Session';
5
5
  export * from './Tenant';
6
+ export * from './Catalog';
@@ -19,3 +19,4 @@ __exportStar(require("./LicenseManager"), exports);
19
19
  __exportStar(require("./Segment"), exports);
20
20
  __exportStar(require("./Session"), exports);
21
21
  __exportStar(require("./Tenant"), exports);
22
+ __exportStar(require("./Catalog"), exports);
@@ -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;
@@ -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,6 +20,12 @@ const onMessage = (worker, message) => {
19
20
  (0, statusTrack_1.trackStatus)();
20
21
  (0, statusTrack_1.broadcastStatusTrack)();
21
22
  }
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
+ // Handled by prom-client's own cluster listener; ignore here.
28
+ }
22
29
  else {
23
30
  listeners_1.logger.warn({
24
31
  content: message,
@@ -27,6 +34,7 @@ const onMessage = (worker, message) => {
27
34
  });
28
35
  }
29
36
  };
37
+ exports.onMessage = onMessage;
30
38
  const onExit = (worker, code, signal) => {
31
39
  if (!constants_1.LINKED && worker.exitedAfterDisconnect === false) {
32
40
  listeners_1.logger.error({
@@ -70,6 +78,11 @@ const startMaster = (service) => {
70
78
  if (service.deterministicVary) {
71
79
  process.env.DETERMINISTIC_VARY = 'true';
72
80
  }
81
+ // Set up the master-side Prometheus aggregator so workers can request a
82
+ // merged, monotonic /metrics view across the whole cluster over IPC.
83
+ if (numWorkers > 1) {
84
+ (0, clusterMetricsAggregator_1.initMasterAggregatorRegistry)();
85
+ }
73
86
  // Setup dubugger
74
87
  if (constants_1.LINKED) {
75
88
  cluster_1.default.setupMaster({ inspectPort: constants_1.INSPECT_DEBUGGER_PORT });
@@ -82,7 +95,7 @@ const startMaster = (service) => {
82
95
  }
83
96
  cluster_1.default.on('online', onOnline);
84
97
  cluster_1.default.on('exit', onExit);
85
- cluster_1.default.on('message', onMessage);
98
+ cluster_1.default.on('message', exports.onMessage);
86
99
  process.on('SIGINT', handleSignal(SIGINT_TIMEOUT_S));
87
100
  process.on('SIGTERM', handleSignal(shutdownTimeout));
88
101
  };
@@ -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,164 @@
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;
68
+ const aggregator = (0, exports.initMasterAggregatorRegistry)();
69
+ try {
70
+ const body = await aggregator.clusterMetrics();
71
+ worker.send({ type: exports.AGG_METRICS_RES, id: message.id, body });
72
+ }
73
+ catch (err) {
74
+ worker.send({ 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) });
75
+ }
76
+ };
77
+ exports.handleWorkerMetricsRequest = handleWorkerMetricsRequest;
78
+ // ---------------------------------------------------------------------------
79
+ // Worker side
80
+ // ---------------------------------------------------------------------------
81
+ let workerAggregatorRegistry;
82
+ const pendingRequests = new Map();
83
+ let requestCounter = 0;
84
+ /**
85
+ * Constructs the worker-side `AggregatorRegistry` (idempotent). Its constructor
86
+ * installs prom-client's worker `process.on('message')` responder that answers
87
+ * the master's `getMetricsReq` with this worker's registry JSON.
88
+ */
89
+ const ensureWorkerAggregatorRegistry = () => {
90
+ if (!workerAggregatorRegistry) {
91
+ workerAggregatorRegistry = new prom_client_1.AggregatorRegistry();
92
+ }
93
+ return workerAggregatorRegistry;
94
+ };
95
+ exports.ensureWorkerAggregatorRegistry = ensureWorkerAggregatorRegistry;
96
+ /**
97
+ * Requests the cluster aggregate from the master over IPC and resolves with the
98
+ * merged exposition string. On timeout or any error it falls back to this
99
+ * worker's local `register.metrics()` so `/metrics` always answers promptly.
100
+ */
101
+ const requestAggregatedMetrics = async () => {
102
+ if (typeof process.send !== 'function') {
103
+ return prom_client_1.register.metrics();
104
+ }
105
+ const id = requestCounter++;
106
+ return new Promise((resolve) => {
107
+ var _a;
108
+ const timer = setTimeout(() => {
109
+ pendingRequests.delete(id);
110
+ listeners_1.logger.warn({
111
+ message: 'Timed out waiting for aggregated cluster metrics; falling back to local registry',
112
+ pid: process.pid,
113
+ });
114
+ prom_client_1.register.metrics().then(resolve).catch(() => resolve(''));
115
+ }, AGG_METRICS_TIMEOUT_MS);
116
+ pendingRequests.set(id, { resolve, timer });
117
+ try {
118
+ process.send({ type: exports.AGG_METRICS_REQ, id });
119
+ }
120
+ catch (err) {
121
+ clearTimeout(timer);
122
+ pendingRequests.delete(id);
123
+ listeners_1.logger.warn({
124
+ content: (_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err),
125
+ message: 'Failed to request aggregated cluster metrics; falling back to local registry',
126
+ pid: process.pid,
127
+ });
128
+ prom_client_1.register.metrics().then(resolve).catch(() => resolve(''));
129
+ }
130
+ });
131
+ };
132
+ exports.requestAggregatedMetrics = requestAggregatedMetrics;
133
+ /**
134
+ * Resolves the pending `requestAggregatedMetrics` promise correlated by id when
135
+ * the master replies. On error it falls back to the local registry.
136
+ */
137
+ const handleMasterMetricsResponse = (message) => {
138
+ const pending = pendingRequests.get(message.id);
139
+ if (!pending) {
140
+ return;
141
+ }
142
+ pendingRequests.delete(message.id);
143
+ clearTimeout(pending.timer);
144
+ if (message.error != null || message.body == null) {
145
+ listeners_1.logger.warn({
146
+ content: message.error,
147
+ message: 'Master failed to aggregate cluster metrics; falling back to local registry',
148
+ pid: process.pid,
149
+ });
150
+ prom_client_1.register.metrics().then(pending.resolve).catch(() => pending.resolve(''));
151
+ return;
152
+ }
153
+ pending.resolve(message.body);
154
+ };
155
+ exports.handleMasterMetricsResponse = handleMasterMetricsResponse;
156
+ /** Test-only helper to reset module state between test cases. */
157
+ const __resetForTests = () => {
158
+ masterAggregatorRegistry = undefined;
159
+ workerAggregatorRegistry = undefined;
160
+ pendingRequests.forEach((p) => clearTimeout(p.timer));
161
+ pendingRequests.clear();
162
+ requestCounter = 0;
163
+ };
164
+ exports.__resetForTests = __resetForTests;
@@ -1,8 +1,8 @@
1
1
  import { Types } from '@vtex/diagnostics-nodejs';
2
2
  export declare const RequestsMetricLabels: {
3
- readonly STATUS_CODE: "status_code";
4
- readonly REQUEST_HANDLER: "handler";
5
3
  readonly ACCOUNT_NAME: "vtex.account.name";
4
+ readonly REQUEST_HANDLER: "handler";
5
+ readonly STATUS_CODE: "status_code";
6
6
  };
7
7
  export interface OtelRequestInstruments {
8
8
  concurrentRequests: Types.Gauge;
@@ -1,56 +1,56 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getOtelInstruments = exports.RequestsMetricLabels = void 0;
4
- const client_1 = require("./client");
5
4
  const constants_1 = require("../../constants");
5
+ const client_1 = require("./client");
6
6
  exports.RequestsMetricLabels = {
7
- STATUS_CODE: 'status_code',
8
- REQUEST_HANDLER: 'handler',
9
7
  ACCOUNT_NAME: constants_1.AttributeKeys.VTEX_ACCOUNT_NAME,
8
+ REQUEST_HANDLER: 'handler',
9
+ STATUS_CODE: 'status_code',
10
10
  };
11
11
  const createOtelConcurrentRequestsInstrument = async () => {
12
12
  const metricsClient = await (0, client_1.getMetricClient)();
13
13
  return metricsClient.createGauge('io_http_requests_current', {
14
14
  description: 'The current number of requests in course.',
15
- unit: '1'
15
+ unit: '1',
16
16
  });
17
17
  };
18
18
  const createOtelRequestsTimingsInstrument = async () => {
19
19
  const metricsClient = await (0, client_1.getMetricClient)();
20
20
  return metricsClient.createHistogram('runtime_http_requests_duration_milliseconds', {
21
21
  description: 'The incoming http requests total duration.',
22
- unit: 'ms'
22
+ unit: 'ms',
23
23
  });
24
24
  };
25
25
  const createOtelTotalRequestsInstrument = async () => {
26
26
  const metricsClient = await (0, client_1.getMetricClient)();
27
27
  return metricsClient.createCounter('runtime_http_requests_total', {
28
28
  description: 'The total number of HTTP requests.',
29
- unit: '1'
29
+ unit: '1',
30
30
  });
31
31
  };
32
32
  const createOtelRequestsResponseSizesInstrument = async () => {
33
33
  const metricsClient = await (0, client_1.getMetricClient)();
34
34
  return metricsClient.createHistogram('runtime_http_response_size_bytes', {
35
35
  description: 'The outgoing response sizes (only applicable when the response isn\'t a stream).',
36
- unit: 'bytes'
36
+ unit: 'bytes',
37
37
  });
38
38
  };
39
39
  const createOtelTotalAbortedRequestsInstrument = async () => {
40
40
  const metricsClient = await (0, client_1.getMetricClient)();
41
41
  return metricsClient.createCounter('runtime_http_aborted_requests_total', {
42
42
  description: 'The total number of HTTP requests aborted.',
43
- unit: '1'
43
+ unit: '1',
44
44
  });
45
45
  };
46
46
  class OtelInstrumentsSingleton {
47
- constructor() { }
48
47
  static getInstance() {
49
48
  if (!OtelInstrumentsSingleton.instance) {
50
49
  OtelInstrumentsSingleton.instance = new OtelInstrumentsSingleton();
51
50
  }
52
51
  return OtelInstrumentsSingleton.instance;
53
52
  }
53
+ constructor() { }
54
54
  async getInstruments() {
55
55
  if (this.instruments) {
56
56
  return this.instruments;
@@ -73,19 +73,19 @@ class OtelInstrumentsSingleton {
73
73
  }
74
74
  }
75
75
  async initializeInstruments() {
76
- const [concurrentRequests, requestTimings, totalRequests, responseSizes, abortedRequests] = await Promise.all([
76
+ const [concurrentRequests, requestTimings, totalRequests, responseSizes, abortedRequests,] = await Promise.all([
77
77
  createOtelConcurrentRequestsInstrument(),
78
78
  createOtelRequestsTimingsInstrument(),
79
79
  createOtelTotalRequestsInstrument(),
80
80
  createOtelRequestsResponseSizesInstrument(),
81
- createOtelTotalAbortedRequestsInstrument()
81
+ createOtelTotalAbortedRequestsInstrument(),
82
82
  ]);
83
83
  return {
84
+ abortedRequests,
84
85
  concurrentRequests,
85
86
  requestTimings,
86
- totalRequests,
87
87
  responseSizes,
88
- abortedRequests
88
+ totalRequests,
89
89
  };
90
90
  }
91
91
  }
@@ -11,7 +11,7 @@ const addOtelRequestMetricsMiddleware = () => {
11
11
  try {
12
12
  return await Promise.race([
13
13
  (0, metrics_1.getOtelInstruments)(),
14
- new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout waiting for OpenTelemetry instruments initialization')), INSTRUMENTS_INITIALIZATION_TIMEOUT))
14
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout waiting for OpenTelemetry instruments initialization')), INSTRUMENTS_INITIALIZATION_TIMEOUT)),
15
15
  ]);
16
16
  }
17
17
  catch (error) {
@@ -1,12 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resetTelemetry = 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");
6
- const instrumentation_koa_1 = require("@opentelemetry/instrumentation-koa");
7
7
  const hostMetrics_1 = require("../metrics/instruments/hostMetrics");
8
8
  const APPLICATION_ID = constants_1.APP.ID || 'vtex-io-app';
9
9
  class TelemetryClientSingleton {
10
+ static getInstance() {
11
+ if (!TelemetryClientSingleton.instance) {
12
+ TelemetryClientSingleton.instance = new TelemetryClientSingleton();
13
+ }
14
+ return TelemetryClientSingleton.instance;
15
+ }
10
16
  constructor() {
11
17
  this.initializationPromise = undefined;
12
18
  this.initializeTracesClient = async (telemetryClient) => await telemetryClient.newTracesClient({
@@ -18,8 +24,8 @@ class TelemetryClientSingleton {
18
24
  exporter: diagnostics_nodejs_1.Exporters.CreateExporter(diagnostics_nodejs_1.Exporters.CreateMetricsExporterConfig({
19
25
  endpoint: constants_1.OTEL_EXPORTER_OTLP_ENDPOINT,
20
26
  interval: 60,
21
- timeoutSeconds: 60,
22
27
  temporality: 'delta',
28
+ timeoutSeconds: 60,
23
29
  }), 'otlp'),
24
30
  });
25
31
  this.initializeLogsClient = async (telemetryClient) => await telemetryClient.newLogsClient({
@@ -29,17 +35,23 @@ class TelemetryClientSingleton {
29
35
  loggerName: `node-vtex-api-${APPLICATION_ID}`,
30
36
  });
31
37
  }
32
- static getInstance() {
33
- if (!TelemetryClientSingleton.instance) {
34
- TelemetryClientSingleton.instance = new TelemetryClientSingleton();
38
+ async getTelemetryClients() {
39
+ if (this.telemetryClients) {
40
+ return this.telemetryClients;
35
41
  }
36
- return TelemetryClientSingleton.instance;
42
+ if (this.initializationPromise) {
43
+ return this.initializationPromise;
44
+ }
45
+ this.initializationPromise = this.initializeTelemetryClients();
46
+ return this.initializationPromise;
47
+ }
48
+ reset() {
49
+ this.telemetryClients = undefined;
50
+ this.initializationPromise = undefined;
37
51
  }
38
52
  async initializeTelemetryClients() {
39
53
  try {
40
54
  const telemetryClient = await (0, diagnostics_nodejs_1.NewTelemetryClient)(constants_1.DK_APP_ID, 'node-vtex-api', APPLICATION_ID, {
41
- // Use built-in no-op functionality when telemetry is disabled
42
- noop: !constants_1.DIAGNOSTICS_TELEMETRY_ENABLED,
43
55
  additionalAttrs: {
44
56
  [constants_1.AttributeKeys.VTEX_IO_APP_ID]: APPLICATION_ID,
45
57
  'vendor': constants_1.APP.VENDOR,
@@ -47,6 +59,8 @@ class TelemetryClientSingleton {
47
59
  [constants_1.AttributeKeys.VTEX_IO_WORKSPACE_NAME]: constants_1.WORKSPACE,
48
60
  [constants_1.AttributeKeys.VTEX_IO_WORKSPACE_TYPE]: constants_1.PRODUCTION ? 'production' : 'development',
49
61
  },
62
+ // Use built-in no-op functionality when telemetry is disabled
63
+ noop: !constants_1.DIAGNOSTICS_TELEMETRY_ENABLED,
50
64
  });
51
65
  const [tracesClient, metricsClient, logsClient] = await Promise.all([
52
66
  this.initializeTracesClient(telemetryClient),
@@ -59,8 +73,8 @@ class TelemetryClientSingleton {
59
73
  ...diagnostics_nodejs_1.Instrumentation.CommonInstrumentations.minimal(),
60
74
  new instrumentation_koa_1.KoaInstrumentation(),
61
75
  new hostMetrics_1.HostMetricsInstrumentation({
62
- name: 'host-metrics-instrumentation',
63
76
  meterProvider: metricsClient.provider(),
77
+ name: 'host-metrics-instrumentation',
64
78
  }),
65
79
  ];
66
80
  telemetryClient.registerInstrumentations(instrumentations);
@@ -81,20 +95,6 @@ class TelemetryClientSingleton {
81
95
  this.initializationPromise = undefined;
82
96
  }
83
97
  }
84
- async getTelemetryClients() {
85
- if (this.telemetryClients) {
86
- return this.telemetryClients;
87
- }
88
- if (this.initializationPromise) {
89
- return this.initializationPromise;
90
- }
91
- this.initializationPromise = this.initializeTelemetryClients();
92
- return this.initializationPromise;
93
- }
94
- reset() {
95
- this.telemetryClients = undefined;
96
- this.initializationPromise = undefined;
97
- }
98
98
  }
99
99
  async function initializeTelemetry() {
100
100
  return TelemetryClientSingleton.getInstance().getTelemetryClients();
@@ -1,3 +1,4 @@
1
1
  import Koa from 'koa';
2
2
  import { ServiceJSON } from './runtime/typings';
3
+ export declare const onMessage: (service: ServiceJSON) => (message: any) => void;
3
4
  export declare const startWorker: (serviceJSON: ServiceJSON) => Koa<Koa.DefaultState, Koa.DefaultContext>;
@@ -3,7 +3,7 @@ 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.startWorker = void 0;
6
+ exports.startWorker = exports.onMessage = void 0;
7
7
  const diagnostics_nodejs_1 = require("@vtex/diagnostics-nodejs");
8
8
  const http_1 = require("http");
9
9
  const koa_1 = __importDefault(require("koa"));
@@ -14,8 +14,9 @@ const constants_1 = require("../../constants");
14
14
  const loaders_1 = require("../loaders");
15
15
  const console_1 = require("../logger/console");
16
16
  const loggerTypes_1 = require("../logger/loggerTypes");
17
- const requestMetricsMiddleware_1 = require("../metrics/requestMetricsMiddleware");
17
+ const clusterMetricsAggregator_1 = require("../metrics/clusterMetricsAggregator");
18
18
  const otelRequestMetricsMiddleware_1 = require("../metrics/otelRequestMetricsMiddleware");
19
+ const requestMetricsMiddleware_1 = require("../metrics/requestMetricsMiddleware");
19
20
  const TracerSingleton_1 = require("../tracing/TracerSingleton");
20
21
  const tracingMiddlewares_1 = require("../tracing/tracingMiddlewares");
21
22
  const listeners_1 = require("./listeners");
@@ -55,6 +56,12 @@ const onMessage = (service) => (message) => {
55
56
  else if ((0, statusTrack_1.isStatusTrack)(message)) {
56
57
  (0, statusTrack_1.trackStatus)();
57
58
  }
59
+ else if ((0, clusterMetricsAggregator_1.isAggMetricsResponse)(message)) {
60
+ (0, clusterMetricsAggregator_1.handleMasterMetricsResponse)(message);
61
+ }
62
+ else if ((0, clusterMetricsAggregator_1.isPromClientMessage)(message)) {
63
+ // Handled by prom-client's own worker listener; ignore here.
64
+ }
58
65
  else {
59
66
  listeners_1.logger.warn({
60
67
  content: message,
@@ -63,6 +70,7 @@ const onMessage = (service) => (message) => {
63
70
  });
64
71
  }
65
72
  };
73
+ exports.onMessage = onMessage;
66
74
  const createAppHttpHandlers = ({ config: { routes, clients } }, serviceJSON, globalLimiter) => {
67
75
  if (routes && clients) {
68
76
  return Object.keys(routes).reduce((acc, routeId) => {
@@ -154,12 +162,17 @@ const startWorker = (serviceJSON) => {
154
162
  var _a, _b;
155
163
  (0, listeners_1.addProcessListeners)();
156
164
  const tracer = TracerSingleton_1.TracerSingleton.getTracer();
165
+ // In multi-worker mode install prom-client's worker-side cluster responder so
166
+ // the master can collect this worker's registry for the aggregated /metrics.
167
+ if (serviceJSON.workers > 1) {
168
+ (0, clusterMetricsAggregator_1.ensureWorkerAggregatorRegistry)();
169
+ }
157
170
  const app = new koa_1.default();
158
171
  app.proxy = true;
159
172
  app
160
173
  .use(error_1.error)
161
174
  .use(diagnostics_nodejs_1.Instrumentation.Middlewares.ContextMiddlewares.Koa.ContextPropagationMiddleware())
162
- .use((0, middlewares_1.prometheusLoggerMiddleware)())
175
+ .use((0, middlewares_1.prometheusLoggerMiddleware)(serviceJSON.workers))
163
176
  .use((0, tracingMiddlewares_1.addTracingMiddleware)(tracer))
164
177
  .use((0, requestMetricsMiddleware_1.addRequestMetricsMiddleware)())
165
178
  .use((0, otelRequestMetricsMiddleware_1.addOtelRequestMetricsMiddleware)())
@@ -184,7 +197,7 @@ const startWorker = (serviceJSON) => {
184
197
  ]
185
198
  .filter(x => x != null)
186
199
  // TODO: Fix ramda typings. Apparently there was an update that broke things
187
- .reduce(ramda_1.mergeDeepRight);
200
+ .reduce((acc, handler) => (0, ramda_1.mergeDeepRight)(acc, handler), {});
188
201
  if (httpHandlers === null || httpHandlers === void 0 ? void 0 : httpHandlers.pub) {
189
202
  const publicHandlersRouter = (0, router_2.routerFromPublicHttpHandlers)(httpHandlers.pub);
190
203
  app.use(publicHandlersRouter);
@@ -193,7 +206,7 @@ const startWorker = (serviceJSON) => {
193
206
  const privateHandlersRouter = routerFromPrivateHttpHandlers(httpHandlers.pvt);
194
207
  app.use(privateHandlersRouter.routes());
195
208
  }
196
- process.on('message', onMessage(serviceJSON));
209
+ process.on('message', (0, exports.onMessage)(serviceJSON));
197
210
  return app;
198
211
  };
199
212
  exports.startWorker = startWorker;
@@ -1,4 +1,4 @@
1
1
  import { ServiceContext } from '../typings';
2
2
  export declare function recorderMiddleware(ctx: ServiceContext, next: () => Promise<void>): Promise<void>;
3
3
  export declare const addMetricsLoggerMiddleware: () => (ctx: ServiceContext, next: () => Promise<void>) => Promise<void>;
4
- export declare const prometheusLoggerMiddleware: () => (ctx: ServiceContext, next: () => Promise<void>) => Promise<void>;
4
+ export declare const prometheusLoggerMiddleware: (workers?: number) => (ctx: ServiceContext, next: () => Promise<void>) => Promise<void>;
@@ -4,6 +4,7 @@ exports.prometheusLoggerMiddleware = exports.addMetricsLoggerMiddleware = export
4
4
  const prom_client_1 = require("prom-client");
5
5
  const constants_1 = require("../../../../constants");
6
6
  const metricsLogger_1 = require("../../../logger/metricsLogger");
7
+ const clusterMetricsAggregator_1 = require("../../../metrics/clusterMetricsAggregator");
7
8
  const EventLoopLagMeasurer_1 = require("../../../tracing/metrics/measurers/EventLoopLagMeasurer");
8
9
  const recorder_1 = require("../utils/recorder");
9
10
  async function recorderMiddleware(ctx, next) {
@@ -22,10 +23,14 @@ const addMetricsLoggerMiddleware = () => {
22
23
  };
23
24
  };
24
25
  exports.addMetricsLoggerMiddleware = addMetricsLoggerMiddleware;
25
- const prometheusLoggerMiddleware = () => {
26
+ const prometheusLoggerMiddleware = (workers = 1) => {
26
27
  (0, prom_client_1.collectDefaultMetrics)();
27
28
  const eventLoopLagMeasurer = new EventLoopLagMeasurer_1.EventLoopLagMeasurer();
28
29
  eventLoopLagMeasurer.start();
30
+ // In multi-worker mode each worker holds only its own registry, so /metrics
31
+ // must serve the cluster-wide aggregate requested from the master over IPC.
32
+ // In single-worker mode (LINKED / workers:1) the local registry is complete.
33
+ const isMultiWorker = workers > 1;
29
34
  return async (ctx, next) => {
30
35
  if (ctx.request.path !== '/metrics') {
31
36
  return next();
@@ -36,7 +41,7 @@ const prometheusLoggerMiddleware = () => {
36
41
  }
37
42
  await eventLoopLagMeasurer.updateInstrumentsAndReset();
38
43
  ctx.set('Content-Type', prom_client_1.register.contentType);
39
- ctx.body = await prom_client_1.register.metrics();
44
+ ctx.body = isMultiWorker ? await (0, clusterMetricsAggregator_1.requestAggregatedMetrics)() : await prom_client_1.register.metrics();
40
45
  ctx.status = 200;
41
46
  };
42
47
  };
@@ -50,7 +50,7 @@ async function auth(ctx, authArgs) {
50
50
  }
51
51
  }
52
52
  function parseArgs(authArgs) {
53
- if (authArgs.scope == 'PUBLIC') {
53
+ if (authArgs.scope === 'PUBLIC') {
54
54
  return authArgs;
55
55
  }
56
56
  if (!authArgs.productCode || !authArgs.resourceCode) {
@@ -63,7 +63,7 @@ class Auth extends graphql_tools_1.SchemaDirectiveVisitor {
63
63
  const { resolve = graphql_1.defaultFieldResolver } = field;
64
64
  field.resolve = async (root, args, ctx, info) => {
65
65
  const authArgs = parseArgs(this.args);
66
- if (!authArgs.scope || authArgs.scope == 'PRIVATE') {
66
+ if (!authArgs.scope || authArgs.scope === 'PRIVATE') {
67
67
  await auth(ctx, authArgs);
68
68
  }
69
69
  return resolve(root, args, ctx, info);
@@ -61,8 +61,8 @@ async function timings(ctx, next) {
61
61
  // We only need to provide the response-specific attributes here
62
62
  if (global.diagnosticsMetrics) {
63
63
  const responseAttributes = {
64
- status_code: statusCode,
65
64
  status,
65
+ status_code: statusCode,
66
66
  };
67
67
  // Record latency histogram (record all requests, not just successful ones)
68
68
  global.diagnosticsMetrics.recordLatency(total, responseAttributes);
@@ -3,7 +3,7 @@
3
3
  // It is used to build a full URL by combining a base URL with a relative URL.
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  // It is originally not intended to be used directly, but rather as a utility function within the Axios library.
6
- // Since Axios 1.0, this function is not exported by default, and the team does not recommend using it directly,
6
+ // Since Axios 1.0, this function is not exported by default, and the team does not recommend using it directly,
7
7
  // as it may not be available in future versions.
8
8
  // The function takes a base URL and a relative URL as input, and returns the full URL.
9
9
  // It handles both absolute and relative URLs, and ensures that the resulting URL is properly formatted.
@@ -19,8 +19,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
19
19
  * @returns {string} The combined full path
20
20
  */
21
21
  function default_1(baseURL, requestedURL, allowAbsoluteUrls) {
22
- let isRelativeUrl = !isAbsoluteURL(requestedURL);
23
- if (baseURL && (isRelativeUrl || allowAbsoluteUrls == false)) {
22
+ const isRelativeUrl = !isAbsoluteURL(requestedURL);
23
+ if (baseURL && (isRelativeUrl || allowAbsoluteUrls === false)) {
24
24
  return combineURLs(baseURL, requestedURL);
25
25
  }
26
26
  return requestedURL;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtex/api",
3
- "version": "7.3.1",
3
+ "version": "7.4.1-beta.1",
4
4
  "description": "VTEX I/O API client",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
@@ -19,7 +19,7 @@
19
19
  "format-lint": "yarn format && yarn lint",
20
20
  "prepublishOnly": "bash ./scripts/publishLock.sh",
21
21
  "ci:build": "yarn build && yarn gen",
22
- "ci:test": "yarn test --ci",
22
+ "ci:test": "yarn test --ci --coverage",
23
23
  "ci:prettier-check": "prettier --check --config ./.prettierrc \"src/**/*.ts\" \"src/**/*.js\""
24
24
  },
25
25
  "jest": {
@@ -1,59 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LicenseManager = void 0;
4
- const qs_1 = require("qs");
5
- const JanusClient_1 = require("./JanusClient");
6
- const TWO_MINUTES_S = 2 * 60;
7
- const BASE_URL = '/api/license-manager';
8
- const routes = {
9
- accountData: `${BASE_URL}/account`,
10
- resourceAccess: `${BASE_URL}/resources`,
11
- topbarData: `${BASE_URL}/site/pvt/newtopbar`,
12
- };
13
- const inflightKey = ({ baseURL, url, params }) => {
14
- return baseURL + url + (0, qs_1.stringify)(params, { arrayFormat: 'repeat', addQueryPrefix: true });
15
- };
16
- class LicenseManager extends JanusClient_1.JanusClient {
17
- getAccountData(VtexIdclientAutCookie, tracingConfig) {
18
- const metric = 'lm-account-data';
19
- return this.http.get(routes.accountData, {
20
- forceMaxAge: TWO_MINUTES_S,
21
- headers: {
22
- VtexIdclientAutCookie,
23
- },
24
- inflightKey,
25
- metric,
26
- tracing: {
27
- requestSpanNameSuffix: metric,
28
- ...tracingConfig === null || tracingConfig === void 0 ? void 0 : tracingConfig.tracing,
29
- },
30
- });
31
- }
32
- getTopbarData(VtexIdclientAutCookie, tracingConfig) {
33
- const metric = 'lm-topbar-data';
34
- return this.http.get(routes.topbarData, {
35
- headers: {
36
- VtexIdclientAutCookie,
37
- },
38
- metric,
39
- tracing: {
40
- requestSpanNameSuffix: metric,
41
- ...tracingConfig === null || tracingConfig === void 0 ? void 0 : tracingConfig.tracing,
42
- },
43
- });
44
- }
45
- canAccessResource(VtexIdclientAutCookie, resourceKey, tracingConfig) {
46
- const metric = 'lm-resource-access';
47
- return this.http.get(`${routes.resourceAccess}/${resourceKey}/access`, {
48
- headers: {
49
- VtexIdclientAutCookie,
50
- },
51
- metric,
52
- tracing: {
53
- requestSpanNameSuffix: metric,
54
- ...tracingConfig === null || tracingConfig === void 0 ? void 0 : tracingConfig.tracing,
55
- },
56
- }).then(() => true, () => false);
57
- }
58
- }
59
- exports.LicenseManager = LicenseManager;