@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 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.0]
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. Disabled by default; opt in per app with
15
- `VTEX_DIAGNOSTICS_TELEMETRY_ENABLED=true`.
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
- if (ctx.config.retryCount) {
74
- const retryCount = ctx.config.retryCount;
75
- if (retryCount > 0) {
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);
@@ -1,2 +1,10 @@
1
- export declare const startApp: () => void;
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
+ }
@@ -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
- ctx.graphql.status = failedToResolve ? 'error' : 'success';
24
+ const status = failedToResolve ? 'error' : 'success';
25
+ ctx.graphql.status = status;
25
26
  const payload = {
26
- [ctx.graphql.status]: 1,
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
- // Errors will be caught by the next middleware so we don't have to catch.
37
- await next();
38
- const { status: statusCode, vtex: { route: { id } }, timings: { total }, vtex } = ctx;
39
- const totalMillis = (0, time_1.hrToMillis)(total);
40
- console.log(log(ctx, totalMillis));
41
- console.log(logBillingInfo(vtex, totalMillis));
42
- const status = (0, status_1.statusLabel)(statusCode);
43
- // Only batch successful responses so metrics don't consider errors
44
- metrics.batch(`http-handler-${id}`, status === 'success' ? total : undefined, { [status]: 1 });
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtex/api",
3
- "version": "6.53.0-beta.0",
3
+ "version": "6.53.0-beta.1",
4
4
  "description": "VTEX I/O API client",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",