@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.
- package/CHANGELOG.md +11 -0
- package/README.md +3 -0
- package/lib/clients/IOClients.d.ts +2 -1
- package/lib/clients/IOClients.js +3 -0
- package/lib/clients/janus/Catalog/index.d.ts +7 -0
- package/lib/clients/janus/Catalog/index.js +37 -0
- package/lib/clients/janus/Catalog/types.d.ts +5 -0
- package/lib/clients/janus/Catalog/types.js +2 -0
- package/lib/clients/janus/{LicenseManager.d.ts → LicenseManager/index.d.ts} +6 -2
- package/lib/clients/janus/LicenseManager/index.js +91 -0
- package/lib/clients/janus/LicenseManager/types.d.ts +16 -0
- package/lib/clients/janus/LicenseManager/types.js +2 -0
- package/lib/clients/janus/LicenseManager/utils.d.ts +6 -0
- package/lib/clients/janus/LicenseManager/utils.js +40 -0
- package/lib/clients/janus/index.d.ts +1 -0
- package/lib/clients/janus/index.js +1 -0
- package/lib/service/master.d.ts +2 -0
- package/lib/service/master.js +15 -2
- package/lib/service/metrics/clusterMetricsAggregator.d.ts +72 -0
- package/lib/service/metrics/clusterMetricsAggregator.js +164 -0
- package/lib/service/metrics/metrics.d.ts +2 -2
- package/lib/service/metrics/metrics.js +13 -13
- package/lib/service/metrics/otelRequestMetricsMiddleware.js +1 -1
- package/lib/service/telemetry/client.js +23 -23
- package/lib/service/worker/index.d.ts +1 -0
- package/lib/service/worker/index.js +18 -5
- package/lib/service/worker/runtime/builtIn/middlewares.d.ts +1 -1
- package/lib/service/worker/runtime/builtIn/middlewares.js +7 -2
- package/lib/service/worker/runtime/graphql/schema/schemaDirectives/Auth.js +2 -2
- package/lib/service/worker/runtime/http/middlewares/timings.js +1 -1
- package/lib/utils/buildFullPath.js +3 -3
- package/package.json +2 -2
- 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
|
[](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
|
}
|
package/lib/clients/IOClients.js
CHANGED
|
@@ -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;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { RequestTracingConfig } from '
|
|
2
|
-
import { JanusClient } from '
|
|
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,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;
|
package/lib/service/master.d.ts
CHANGED
package/lib/service/master.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
33
|
-
if (
|
|
34
|
-
|
|
38
|
+
async getTelemetryClients() {
|
|
39
|
+
if (this.telemetryClients) {
|
|
40
|
+
return this.telemetryClients;
|
|
35
41
|
}
|
|
36
|
-
|
|
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();
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
23
|
-
if (baseURL && (isRelativeUrl || allowAbsoluteUrls
|
|
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
|
+
"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;
|