@vtex/api 6.53.0-beta.0 → 6.53.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -3
- package/lib/HttpClient/middlewares/metrics.js +52 -5
- package/lib/HttpClient/middlewares/request/HttpAgentSingleton.d.ts +5 -0
- package/lib/HttpClient/middlewares/request/HttpAgentSingleton.js +15 -0
- package/lib/service/index.d.ts +9 -1
- package/lib/service/index.js +5 -1
- package/lib/service/worker/runtime/graphql/schema/schemaDirectives/Metric.js +20 -2
- package/lib/service/worker/runtime/http/middlewares/requestStats.js +42 -2
- package/lib/service/worker/runtime/http/middlewares/timings.js +49 -9
- package/lib/service/worker/runtime/statusTrack.js +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,14 +5,17 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
6
6
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [6.53.0-beta.
|
|
8
|
+
## [6.53.0-beta.1]
|
|
9
9
|
### Added
|
|
10
10
|
- Diagnostics metrics observability, backported from the `master` (7.x) line: `DiagnosticsMetrics`
|
|
11
11
|
(`recordLatency`, `incrementCounter`, `setGauge`, `runWithBaseAttributes`), a split
|
|
12
12
|
traces/metrics/logs telemetry client (`@vtex/diagnostics-nodejs@0.1.8-io`,
|
|
13
13
|
`@vtex/diagnostics-semconv`), cluster resource attributes, and automatic Koa + host-metrics
|
|
14
|
-
instrumentation.
|
|
15
|
-
|
|
14
|
+
instrumentation. Metrics are wired into the request pipeline itself, not just exposed as a
|
|
15
|
+
library API: HTTP handler latency/counters, request closed/aborted/total counters, outbound
|
|
16
|
+
HTTP client metrics, HTTP agent socket gauges, and the `@metric` GraphQL directive all emit
|
|
17
|
+
through `DiagnosticsMetrics` at the same points `master` does. Disabled by default; opt in
|
|
18
|
+
per app with `VTEX_DIAGNOSTICS_TELEMETRY_ENABLED=true`.
|
|
16
19
|
|
|
17
20
|
## [6.52.0]
|
|
18
21
|
### Added
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.metricsMiddleware = void 0;
|
|
4
|
+
const constants_1 = require("../../constants");
|
|
4
5
|
const RequestCancelledError_1 = require("../../errors/RequestCancelledError");
|
|
5
6
|
const requestStats_1 = require("../../service/worker/runtime/http/middlewares/requestStats");
|
|
6
7
|
const utils_1 = require("../../utils");
|
|
7
8
|
const retry_1 = require("../../utils/retry");
|
|
8
9
|
const status_1 = require("../../utils/status");
|
|
10
|
+
const DEFAULT_ACCOUNT = 'unknown';
|
|
9
11
|
const metricsMiddleware = ({ metrics, serverTiming, name }) => {
|
|
10
12
|
const serverTimingStart = process.hrtime();
|
|
11
13
|
const serverTimingLabel = (0, utils_1.shrinkTimings)((0, utils_1.formatTimingName)({
|
|
@@ -14,6 +16,7 @@ const metricsMiddleware = ({ metrics, serverTiming, name }) => {
|
|
|
14
16
|
target: name || 'unknown',
|
|
15
17
|
}));
|
|
16
18
|
return async (ctx, next) => {
|
|
19
|
+
var _a, _b;
|
|
17
20
|
const start = process.hrtime();
|
|
18
21
|
let status = 'unknown';
|
|
19
22
|
let errorCode;
|
|
@@ -57,29 +60,73 @@ const metricsMiddleware = ({ metrics, serverTiming, name }) => {
|
|
|
57
60
|
const label = `http-client-${ctx.config.metric}`;
|
|
58
61
|
const extensions = {};
|
|
59
62
|
Object.assign(extensions, { [status]: 1 });
|
|
63
|
+
// Determine cache state for diagnostics metrics
|
|
64
|
+
let cacheState = 'none';
|
|
60
65
|
if (ctx.cacheHit) {
|
|
61
66
|
Object.assign(extensions, ctx.cacheHit, { [`${status}-hit`]: 1 });
|
|
67
|
+
cacheState = 'hit';
|
|
62
68
|
}
|
|
63
69
|
else if (!ctx.inflightHit && !ctx.memoizedHit) {
|
|
64
70
|
// Lets us know how many calls passed through to origin
|
|
65
71
|
Object.assign(extensions, { [`${status}-miss`]: 1 });
|
|
72
|
+
cacheState = 'miss';
|
|
66
73
|
}
|
|
67
74
|
if (ctx.inflightHit) {
|
|
68
75
|
Object.assign(extensions, { [`${status}-inflight`]: 1 });
|
|
76
|
+
cacheState = 'inflight';
|
|
69
77
|
}
|
|
70
78
|
if (ctx.memoizedHit) {
|
|
71
79
|
Object.assign(extensions, { [`${status}-memoized`]: 1 });
|
|
80
|
+
cacheState = 'memoized';
|
|
72
81
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
extensions[`retry-${status}-${retryCount}`] = 1;
|
|
77
|
-
}
|
|
82
|
+
const retryCount = ctx.config.retryCount || 0;
|
|
83
|
+
if (retryCount > 0) {
|
|
84
|
+
extensions[`retry-${status}-${retryCount}`] = 1;
|
|
78
85
|
}
|
|
79
86
|
const end = status === 'success' && !ctx.cacheHit && !ctx.inflightHit && !ctx.memoizedHit
|
|
80
87
|
? process.hrtime(start)
|
|
81
88
|
: undefined;
|
|
89
|
+
// Legacy metrics (backward compatibility)
|
|
82
90
|
metrics.batch(label, end, extensions);
|
|
91
|
+
// New diagnostics metrics with stable names and attributes
|
|
92
|
+
if (global.diagnosticsMetrics) {
|
|
93
|
+
const elapsed = process.hrtime(start);
|
|
94
|
+
const rawStatusCode = ((_a = ctx.response) === null || _a === void 0 ? void 0 : _a.status) || errorStatus;
|
|
95
|
+
// Extract account from request headers, fallback to default
|
|
96
|
+
const account = ((_b = ctx.config.headers) === null || _b === void 0 ? void 0 : _b[constants_1.ACCOUNT_HEADER]) || DEFAULT_ACCOUNT;
|
|
97
|
+
const baseAttributes = {
|
|
98
|
+
[constants_1.AttributeKeys.VTEX_ACCOUNT_NAME]: account,
|
|
99
|
+
component: 'http-client',
|
|
100
|
+
client_metric: ctx.config.metric,
|
|
101
|
+
status_code: rawStatusCode,
|
|
102
|
+
status,
|
|
103
|
+
};
|
|
104
|
+
// Record latency histogram with all context
|
|
105
|
+
global.diagnosticsMetrics.recordLatency(elapsed, {
|
|
106
|
+
...baseAttributes,
|
|
107
|
+
cache_state: cacheState,
|
|
108
|
+
});
|
|
109
|
+
// Increment counters for different event types (replaces extensions behavior)
|
|
110
|
+
// Main request counter with status as attribute
|
|
111
|
+
global.diagnosticsMetrics.incrementCounter('http_client_requests_total', 1, baseAttributes);
|
|
112
|
+
// Cache counter with cache_state as attribute (replaces extensions like 'success-hit', 'error-miss')
|
|
113
|
+
if (cacheState !== 'none') {
|
|
114
|
+
global.diagnosticsMetrics.incrementCounter('http_client_cache_total', 1, {
|
|
115
|
+
...baseAttributes,
|
|
116
|
+
cache_state: cacheState,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// Retry counter (replaces 'retry-{status}-{count}' extensions)
|
|
120
|
+
if (retryCount > 0) {
|
|
121
|
+
global.diagnosticsMetrics.incrementCounter('http_client_requests_retried_total', 1, {
|
|
122
|
+
...baseAttributes,
|
|
123
|
+
retry_count: retryCount,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
console.warn('DiagnosticsMetrics not available. HTTP client metrics not reported.');
|
|
129
|
+
}
|
|
83
130
|
if (ctx.config.verbose) {
|
|
84
131
|
console.log(`VERBOSE: ${name}.${ctx.config.label}`, {
|
|
85
132
|
...extensions,
|
|
@@ -6,6 +6,11 @@ export declare class HttpAgentSingleton {
|
|
|
6
6
|
pendingRequests: number;
|
|
7
7
|
sockets: number;
|
|
8
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* Update HTTP agent statistics as diagnostics metrics (gauges).
|
|
11
|
+
* This method should be called periodically to export current HTTP agent state.
|
|
12
|
+
*/
|
|
13
|
+
static updateHttpAgentMetrics(): void;
|
|
9
14
|
private static httpAgent;
|
|
10
15
|
private static count;
|
|
11
16
|
}
|
|
@@ -19,6 +19,21 @@ class HttpAgentSingleton {
|
|
|
19
19
|
sockets,
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Update HTTP agent statistics as diagnostics metrics (gauges).
|
|
24
|
+
* This method should be called periodically to export current HTTP agent state.
|
|
25
|
+
*/
|
|
26
|
+
static updateHttpAgentMetrics() {
|
|
27
|
+
if (!global.diagnosticsMetrics) {
|
|
28
|
+
console.warn('DiagnosticsMetrics not available. HTTP agent metrics not reported.');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const stats = HttpAgentSingleton.httpAgentStats();
|
|
32
|
+
// Report HTTP agent stats as gauges (current state)
|
|
33
|
+
global.diagnosticsMetrics.setGauge('http_agent_sockets_current', stats.sockets, {});
|
|
34
|
+
global.diagnosticsMetrics.setGauge('http_agent_free_sockets_current', stats.freeSockets, {});
|
|
35
|
+
global.diagnosticsMetrics.setGauge('http_agent_pending_requests_current', stats.pendingRequests, {});
|
|
36
|
+
}
|
|
22
37
|
static count(obj) {
|
|
23
38
|
try {
|
|
24
39
|
return Object.values(obj).reduce((acc, val) => acc += val.length, 0);
|
package/lib/service/index.d.ts
CHANGED
|
@@ -1,2 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import { DiagnosticsMetrics } from '../metrics/DiagnosticsMetrics';
|
|
2
|
+
export declare const startApp: () => Promise<void>;
|
|
2
3
|
export { appPath } from './loaders';
|
|
4
|
+
declare global {
|
|
5
|
+
namespace NodeJS {
|
|
6
|
+
interface Global {
|
|
7
|
+
diagnosticsMetrics: DiagnosticsMetrics;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
package/lib/service/index.js
CHANGED
|
@@ -6,11 +6,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.appPath = exports.startApp = void 0;
|
|
7
7
|
const cluster_1 = __importDefault(require("cluster"));
|
|
8
8
|
const constants_1 = require("../constants");
|
|
9
|
+
const DiagnosticsMetrics_1 = require("../metrics/DiagnosticsMetrics");
|
|
9
10
|
const loaders_1 = require("./loaders");
|
|
10
11
|
const logger_1 = require("./logger");
|
|
11
12
|
const master_1 = require("./master");
|
|
13
|
+
const telemetry_1 = require("./telemetry");
|
|
12
14
|
const worker_1 = require("./worker");
|
|
13
|
-
const startApp = () => {
|
|
15
|
+
const startApp = async () => {
|
|
16
|
+
await (0, telemetry_1.initializeTelemetry)();
|
|
17
|
+
global.diagnosticsMetrics = new DiagnosticsMetrics_1.DiagnosticsMetrics();
|
|
14
18
|
const serviceJSON = (0, loaders_1.getServiceJSON)();
|
|
15
19
|
try {
|
|
16
20
|
// if it is a master process then call setting up worker process
|
|
@@ -21,11 +21,29 @@ class Metric extends graphql_tools_1.SchemaDirectiveVisitor {
|
|
|
21
21
|
result = error;
|
|
22
22
|
failedToResolve = true;
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
const status = failedToResolve ? 'error' : 'success';
|
|
25
|
+
ctx.graphql.status = status;
|
|
25
26
|
const payload = {
|
|
26
|
-
[
|
|
27
|
+
[status]: 1,
|
|
27
28
|
};
|
|
29
|
+
// Legacy metrics (backward compatibility)
|
|
28
30
|
metrics.batch(`graphql-metric-${name}`, failedToResolve ? undefined : ellapsed, payload);
|
|
31
|
+
// New diagnostics metrics with stable names and attributes
|
|
32
|
+
if (global.diagnosticsMetrics) {
|
|
33
|
+
const attributes = {
|
|
34
|
+
[__1.AttributeKeys.VTEX_ACCOUNT_NAME]: ctx.vtex.account,
|
|
35
|
+
component: 'graphql',
|
|
36
|
+
field_name: name,
|
|
37
|
+
status,
|
|
38
|
+
};
|
|
39
|
+
// Record latency histogram (record all requests, not just successful ones)
|
|
40
|
+
global.diagnosticsMetrics.recordLatency(ellapsed, attributes);
|
|
41
|
+
// Increment counter (status is an attribute, not in metric name)
|
|
42
|
+
global.diagnosticsMetrics.incrementCounter('graphql_field_requests_total', 1, attributes);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
console.warn('DiagnosticsMetrics not available. GraphQL field metrics not reported.');
|
|
46
|
+
}
|
|
29
47
|
if (failedToResolve) {
|
|
30
48
|
throw result;
|
|
31
49
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.trackIncomingRequestStats = exports.incomingRequestStats = exports.cancelMessage = void 0;
|
|
4
|
+
const constants_1 = require("../../../../../constants");
|
|
4
5
|
exports.cancelMessage = 'Request cancelled';
|
|
5
6
|
class IncomingRequestStats {
|
|
6
7
|
constructor() {
|
|
@@ -22,20 +23,59 @@ class IncomingRequestStats {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
exports.incomingRequestStats = new IncomingRequestStats();
|
|
25
|
-
const requestClosed = () => {
|
|
26
|
+
const requestClosed = (ctx) => () => {
|
|
26
27
|
exports.incomingRequestStats.closed++;
|
|
28
|
+
// Report to diagnostics metrics (cumulative counter)
|
|
29
|
+
const { status: statusCode, vtex: { account, route: { id, type } } } = ctx;
|
|
30
|
+
if (global.diagnosticsMetrics) {
|
|
31
|
+
global.diagnosticsMetrics.incrementCounter('http_server_requests_closed_total', 1, {
|
|
32
|
+
[constants_1.AttributeKeys.VTEX_ACCOUNT_NAME]: account,
|
|
33
|
+
route_id: id,
|
|
34
|
+
route_type: type,
|
|
35
|
+
status_code: statusCode,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
console.warn('DiagnosticsMetrics not available. Request closed metric not reported.');
|
|
40
|
+
}
|
|
27
41
|
};
|
|
28
42
|
const requestAborted = (ctx) => () => {
|
|
29
43
|
exports.incomingRequestStats.aborted++;
|
|
44
|
+
// Report to diagnostics metrics (cumulative counter)
|
|
45
|
+
const { status: statusCode, vtex: { account, route: { id, type } } } = ctx;
|
|
46
|
+
if (global.diagnosticsMetrics) {
|
|
47
|
+
global.diagnosticsMetrics.incrementCounter('http_server_requests_aborted_total', 1, {
|
|
48
|
+
[constants_1.AttributeKeys.VTEX_ACCOUNT_NAME]: account,
|
|
49
|
+
route_id: id,
|
|
50
|
+
route_type: type,
|
|
51
|
+
status_code: statusCode,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
console.warn('DiagnosticsMetrics not available. Request aborted metric not reported.');
|
|
56
|
+
}
|
|
30
57
|
if (ctx.vtex.cancellation && ctx.vtex.cancellation.cancelable) {
|
|
31
58
|
ctx.vtex.cancellation.source.cancel(exports.cancelMessage);
|
|
32
59
|
ctx.vtex.cancellation.cancelled = true;
|
|
33
60
|
}
|
|
34
61
|
};
|
|
35
62
|
async function trackIncomingRequestStats(ctx, next) {
|
|
36
|
-
ctx.req.on('close', requestClosed);
|
|
63
|
+
ctx.req.on('close', requestClosed(ctx));
|
|
37
64
|
ctx.req.on('aborted', requestAborted(ctx));
|
|
38
65
|
exports.incomingRequestStats.total++;
|
|
66
|
+
// Report total requests to diagnostics metrics (cumulative counter)
|
|
67
|
+
const { status: statusCode, vtex: { account, route: { id, type } } } = ctx;
|
|
68
|
+
if (global.diagnosticsMetrics) {
|
|
69
|
+
global.diagnosticsMetrics.incrementCounter('http_server_requests_total', 1, {
|
|
70
|
+
[constants_1.AttributeKeys.VTEX_ACCOUNT_NAME]: account,
|
|
71
|
+
route_id: id,
|
|
72
|
+
route_type: type,
|
|
73
|
+
status_code: statusCode,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
console.warn('DiagnosticsMetrics not available. Request total metric not reported.');
|
|
78
|
+
}
|
|
39
79
|
await next();
|
|
40
80
|
}
|
|
41
81
|
exports.trackIncomingRequestStats = trackIncomingRequestStats;
|
|
@@ -33,14 +33,54 @@ const logBillingInfo = ({ account, workspace, production, route: { id, type } },
|
|
|
33
33
|
'workspace': workspace,
|
|
34
34
|
});
|
|
35
35
|
async function timings(ctx, next) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
const { vtex: { route: { id, type } }, vtex } = ctx;
|
|
37
|
+
// Set base attributes for all metrics recorded during this request.
|
|
38
|
+
// This includes metrics recorded by VTEX IO apps during handler execution.
|
|
39
|
+
// These attributes will be automatically merged with custom attributes.
|
|
40
|
+
const baseAttributes = {
|
|
41
|
+
[constants_1.AttributeKeys.VTEX_ACCOUNT_NAME]: vtex.account,
|
|
42
|
+
component: 'http-handler',
|
|
43
|
+
route_id: id,
|
|
44
|
+
route_type: type,
|
|
45
|
+
};
|
|
46
|
+
// Wrap the request handling with base attributes context.
|
|
47
|
+
// All metrics recorded during next() will automatically include these attributes.
|
|
48
|
+
const executeWithBaseAttributes = async () => {
|
|
49
|
+
// Errors will be caught by the next middleware so we don't have to catch.
|
|
50
|
+
await next();
|
|
51
|
+
const { status: statusCode, timings: { total } } = ctx;
|
|
52
|
+
const totalMillis = (0, time_1.hrToMillis)(total);
|
|
53
|
+
console.log(log(ctx, totalMillis));
|
|
54
|
+
console.log(logBillingInfo(vtex, totalMillis));
|
|
55
|
+
const status = (0, status_1.statusLabel)(statusCode);
|
|
56
|
+
// Legacy metrics (backward compatibility)
|
|
57
|
+
// Only batch successful responses so metrics don't consider errors
|
|
58
|
+
metrics.batch(`http-handler-${id}`, status === 'success' ? total : undefined, { [status]: 1 });
|
|
59
|
+
// New diagnostics metrics with stable names and attributes
|
|
60
|
+
// Note: base attributes (account, route_id, route_type) are automatically merged
|
|
61
|
+
// We only need to provide the response-specific attributes here
|
|
62
|
+
if (global.diagnosticsMetrics) {
|
|
63
|
+
const responseAttributes = {
|
|
64
|
+
status,
|
|
65
|
+
status_code: statusCode,
|
|
66
|
+
};
|
|
67
|
+
// Record latency histogram (record all requests, not just successful ones)
|
|
68
|
+
global.diagnosticsMetrics.recordLatency(total, responseAttributes);
|
|
69
|
+
// Increment counter (status is an attribute, not in metric name)
|
|
70
|
+
global.diagnosticsMetrics.incrementCounter('http_handler_requests_total', 1, responseAttributes);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
console.warn('DiagnosticsMetrics not available. HTTP handler metrics not reported.');
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
// If diagnosticsMetrics is available, run with base attributes context
|
|
77
|
+
// Otherwise, run without context (fallback for graceful degradation)
|
|
78
|
+
if (global.diagnosticsMetrics) {
|
|
79
|
+
await global.diagnosticsMetrics.runWithBaseAttributes(baseAttributes, executeWithBaseAttributes);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.warn('DiagnosticsMetrics not available. HTTP handler metrics not reported.');
|
|
83
|
+
await executeWithBaseAttributes();
|
|
84
|
+
}
|
|
45
85
|
}
|
|
46
86
|
exports.timings = timings;
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.broadcastStatusTrack = exports.trackStatus = exports.statusTrackHandler = exports.isStatusTrackBroadcast = exports.isStatusTrack = void 0;
|
|
7
7
|
const cluster_1 = __importDefault(require("cluster"));
|
|
8
|
+
const HttpAgentSingleton_1 = require("../../../HttpClient/middlewares/request/HttpAgentSingleton");
|
|
8
9
|
const constants_1 = require("../../../constants");
|
|
9
10
|
const BROADCAST_STATUS_TRACK = 'broadcastStatusTrack';
|
|
10
11
|
const STATUS_TRACK = 'statusTrack';
|
|
@@ -26,6 +27,8 @@ const statusTrackHandler = async (ctx) => {
|
|
|
26
27
|
};
|
|
27
28
|
exports.statusTrackHandler = statusTrackHandler;
|
|
28
29
|
const trackStatus = () => {
|
|
30
|
+
// Update diagnostics metrics (gauges for HTTP agent stats)
|
|
31
|
+
HttpAgentSingleton_1.HttpAgentSingleton.updateHttpAgentMetrics();
|
|
29
32
|
// Flushing resets the metric accumulators, the CPU usage baseline and the
|
|
30
33
|
// incoming request stats, so it must keep running even though nothing
|
|
31
34
|
// consumes the returned metrics anymore.
|