@vtex/api 7.2.4 → 7.2.5
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/lib/HttpClient/middlewares/metrics.js +47 -5
- package/lib/HttpClient/middlewares/request/HttpAgentSingleton.d.ts +5 -0
- package/lib/HttpClient/middlewares/request/HttpAgentSingleton.js +15 -0
- package/lib/constants.d.ts +1 -0
- package/lib/constants.js +2 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/metrics/DiagnosticsMetrics.d.ts +94 -0
- package/lib/metrics/DiagnosticsMetrics.js +206 -0
- package/lib/service/index.d.ts +2 -0
- package/lib/service/index.js +6 -1
- package/lib/service/worker/runtime/graphql/schema/schemaDirectives/Metric.js +19 -2
- package/lib/service/worker/runtime/http/middlewares/requestStats.js +38 -2
- package/lib/service/worker/runtime/http/middlewares/timings.js +19 -1
- package/lib/service/worker/runtime/statusTrack.js +4 -0
- package/package.json +1 -1
|
@@ -14,6 +14,7 @@ const metricsMiddleware = ({ metrics, serverTiming, name }) => {
|
|
|
14
14
|
target: name || 'unknown',
|
|
15
15
|
}));
|
|
16
16
|
return async (ctx, next) => {
|
|
17
|
+
var _a;
|
|
17
18
|
const start = process.hrtime();
|
|
18
19
|
let status = 'unknown';
|
|
19
20
|
let errorCode;
|
|
@@ -57,29 +58,70 @@ const metricsMiddleware = ({ metrics, serverTiming, name }) => {
|
|
|
57
58
|
const label = `http-client-${ctx.config.metric}`;
|
|
58
59
|
const extensions = {};
|
|
59
60
|
Object.assign(extensions, { [status]: 1 });
|
|
61
|
+
// Determine cache state for diagnostics metrics
|
|
62
|
+
let cacheState = 'none';
|
|
60
63
|
if (ctx.cacheHit) {
|
|
61
64
|
Object.assign(extensions, ctx.cacheHit, { [`${status}-hit`]: 1 });
|
|
65
|
+
cacheState = 'hit';
|
|
62
66
|
}
|
|
63
67
|
else if (!ctx.inflightHit && !ctx.memoizedHit) {
|
|
64
68
|
// Lets us know how many calls passed through to origin
|
|
65
69
|
Object.assign(extensions, { [`${status}-miss`]: 1 });
|
|
70
|
+
cacheState = 'miss';
|
|
66
71
|
}
|
|
67
72
|
if (ctx.inflightHit) {
|
|
68
73
|
Object.assign(extensions, { [`${status}-inflight`]: 1 });
|
|
74
|
+
cacheState = 'inflight';
|
|
69
75
|
}
|
|
70
76
|
if (ctx.memoizedHit) {
|
|
71
77
|
Object.assign(extensions, { [`${status}-memoized`]: 1 });
|
|
78
|
+
cacheState = 'memoized';
|
|
72
79
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
extensions[`retry-${status}-${retryCount}`] = 1;
|
|
77
|
-
}
|
|
80
|
+
const retryCount = ctx.config.retryCount || 0;
|
|
81
|
+
if (retryCount > 0) {
|
|
82
|
+
extensions[`retry-${status}-${retryCount}`] = 1;
|
|
78
83
|
}
|
|
79
84
|
const end = status === 'success' && !ctx.cacheHit && !ctx.inflightHit && !ctx.memoizedHit
|
|
80
85
|
? process.hrtime(start)
|
|
81
86
|
: undefined;
|
|
87
|
+
// Legacy metrics (backward compatibility)
|
|
82
88
|
metrics.batch(label, end, extensions);
|
|
89
|
+
// New diagnostics metrics with stable names and attributes
|
|
90
|
+
if (global.diagnosticsMetrics) {
|
|
91
|
+
const elapsed = process.hrtime(start);
|
|
92
|
+
const rawStatusCode = ((_a = ctx.response) === null || _a === void 0 ? void 0 : _a.status) || errorStatus;
|
|
93
|
+
const baseAttributes = {
|
|
94
|
+
component: 'http-client',
|
|
95
|
+
client_metric: ctx.config.metric,
|
|
96
|
+
status_code: rawStatusCode,
|
|
97
|
+
status,
|
|
98
|
+
};
|
|
99
|
+
// Record latency histogram with all context
|
|
100
|
+
global.diagnosticsMetrics.recordLatency(elapsed, {
|
|
101
|
+
...baseAttributes,
|
|
102
|
+
cache_state: cacheState,
|
|
103
|
+
});
|
|
104
|
+
// Increment counters for different event types (replaces extensions behavior)
|
|
105
|
+
// Main request counter with status as attribute
|
|
106
|
+
global.diagnosticsMetrics.incrementCounter('http_client_requests_total', 1, baseAttributes);
|
|
107
|
+
// Cache counter with cache_state as attribute (replaces extensions like 'success-hit', 'error-miss')
|
|
108
|
+
if (cacheState !== 'none') {
|
|
109
|
+
global.diagnosticsMetrics.incrementCounter('http_client_cache_total', 1, {
|
|
110
|
+
...baseAttributes,
|
|
111
|
+
cache_state: cacheState,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// Retry counter (replaces 'retry-{status}-{count}' extensions)
|
|
115
|
+
if (retryCount > 0) {
|
|
116
|
+
global.diagnosticsMetrics.incrementCounter('http_client_requests_retried_total', 1, {
|
|
117
|
+
...baseAttributes,
|
|
118
|
+
retry_count: retryCount,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
console.warn('DiagnosticsMetrics not available. HTTP client metrics not reported.');
|
|
124
|
+
}
|
|
83
125
|
if (ctx.config.verbose) {
|
|
84
126
|
console.log(`VERBOSE: ${name}.${ctx.config.label}`, {
|
|
85
127
|
...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/constants.d.ts
CHANGED
|
@@ -130,6 +130,7 @@ export declare const PRODUCTION: boolean;
|
|
|
130
130
|
export declare const INSPECT_DEBUGGER_PORT = 5858;
|
|
131
131
|
export declare const cancellableMethods: Set<string>;
|
|
132
132
|
export declare const LOG_CLIENT_INIT_TIMEOUT_MS = 5000;
|
|
133
|
+
export declare const METRIC_CLIENT_INIT_TIMEOUT_MS = 5000;
|
|
133
134
|
export declare const OTEL_EXPORTER_OTLP_ENDPOINT: string;
|
|
134
135
|
export declare const DK_APP_ID: string;
|
|
135
136
|
export declare const DIAGNOSTICS_TELEMETRY_ENABLED: boolean;
|
package/lib/constants.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.LOG_CLIENT_INIT_TIMEOUT_MS = exports.cancellableMethods = exports.INSPECT_DEBUGGER_PORT = exports.PRODUCTION = exports.WORKSPACE = exports.ACCOUNT = exports.NODE_ENV = exports.APP = exports.PUBLIC_ENDPOINT = exports.REGION = exports.LINKED = exports.MAX_WORKERS = exports.HTTP_SERVER_PORT = exports.MAX_AGE = exports.UP_SIGNAL = exports.BODY_HASH = exports.PROVIDER_HEADER = exports.TRACE_ID_HEADER = exports.COLOSSUS_PARAMS_HEADER = exports.COLOSSUS_ROUTE_ID_HEADER = exports.COLOSSUS_ROUTE_DECLARER_HEADER = exports.EVENT_HANDLER_ID_HEADER = exports.EVENT_SUBJECT_HEADER = exports.EVENT_SENDER_HEADER = exports.EVENT_KEY_HEADER = exports.WORKSPACE_HEADER = exports.WORKSPACE_IS_PRODUCTION_HEADER = exports.PLATFORM_HEADER = exports.OPERATION_ID_HEADER = exports.ROUTER_CACHE_HEADER = exports.REQUEST_ID_HEADER = exports.CREDENTIAL_HEADER = exports.ACCOUNT_HEADER = exports.ETAG_HEADER = exports.META_HEADER_BUCKET = exports.META_HEADER = exports.BINDING_HEADER = exports.TENANT_HEADER = exports.FORWARDED_HOST_HEADER = exports.LOCALE_HEADER = exports.PRODUCT_HEADER = exports.SESSION_HEADER = exports.SEGMENT_HEADER = exports.CACHE_CONTROL_HEADER = exports.AttributeKeys = exports.HeaderKeys = exports.PID = exports.IS_IO = exports.DEFAULT_WORKSPACE = exports.NODE_VTEX_API_VERSION = void 0;
|
|
4
|
-
exports.DIAGNOSTICS_TELEMETRY_ENABLED = exports.DK_APP_ID = exports.OTEL_EXPORTER_OTLP_ENDPOINT = void 0;
|
|
4
|
+
exports.DIAGNOSTICS_TELEMETRY_ENABLED = exports.DK_APP_ID = exports.OTEL_EXPORTER_OTLP_ENDPOINT = exports.METRIC_CLIENT_INIT_TIMEOUT_MS = void 0;
|
|
5
5
|
const app_1 = require("./utils/app");
|
|
6
6
|
const diagnostics_semconv_1 = require("@vtex/diagnostics-semconv");
|
|
7
7
|
// tslint:disable-next-line
|
|
@@ -141,6 +141,7 @@ exports.PRODUCTION = process.env.VTEX_PRODUCTION === 'true';
|
|
|
141
141
|
exports.INSPECT_DEBUGGER_PORT = 5858;
|
|
142
142
|
exports.cancellableMethods = new Set(['GET', 'OPTIONS', 'HEAD']);
|
|
143
143
|
exports.LOG_CLIENT_INIT_TIMEOUT_MS = 5000;
|
|
144
|
+
exports.METRIC_CLIENT_INIT_TIMEOUT_MS = 5000;
|
|
144
145
|
exports.OTEL_EXPORTER_OTLP_ENDPOINT = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
145
146
|
exports.DK_APP_ID = process.env.NODE_VTEX_API_DK_APP_ID || "apps-team";
|
|
146
147
|
exports.DIAGNOSTICS_TELEMETRY_ENABLED = process.env.VTEX_DIAGNOSTICS_TELEMETRY_ENABLED === 'true';
|
package/lib/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from './clients';
|
|
|
3
3
|
export * from './errors';
|
|
4
4
|
export * from './HttpClient';
|
|
5
5
|
export * from './metrics/MetricsAccumulator';
|
|
6
|
+
export * from './metrics/DiagnosticsMetrics';
|
|
6
7
|
export * from './responses';
|
|
7
8
|
export * from './service/worker/runtime/Service';
|
|
8
9
|
export * from './service/worker/runtime/method';
|
package/lib/index.js
CHANGED
|
@@ -20,6 +20,7 @@ __exportStar(require("./clients"), exports);
|
|
|
20
20
|
__exportStar(require("./errors"), exports);
|
|
21
21
|
__exportStar(require("./HttpClient"), exports);
|
|
22
22
|
__exportStar(require("./metrics/MetricsAccumulator"), exports);
|
|
23
|
+
__exportStar(require("./metrics/DiagnosticsMetrics"), exports);
|
|
23
24
|
__exportStar(require("./responses"), exports);
|
|
24
25
|
__exportStar(require("./service/worker/runtime/Service"), exports);
|
|
25
26
|
__exportStar(require("./service/worker/runtime/method"), exports);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Attributes } from '@opentelemetry/api';
|
|
2
|
+
/**
|
|
3
|
+
* DiagnosticsMetrics provides a high-level API for recording metrics using
|
|
4
|
+
* the @vtex/diagnostics-nodejs library. It completely abstracts instrument
|
|
5
|
+
* management, bucket configuration, and lifecycle.
|
|
6
|
+
*
|
|
7
|
+
* Uses a single histogram for all latency measurements with attributes to differentiate.
|
|
8
|
+
* This follows OpenTelemetry best practices and reduces metric cardinality.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const diagnosticsMetrics = new DiagnosticsMetrics()
|
|
13
|
+
* diagnosticsMetrics.initMetricClient()
|
|
14
|
+
*
|
|
15
|
+
* // Record latency with operation type in attributes
|
|
16
|
+
* const start = process.hrtime()
|
|
17
|
+
* // ... do work ...
|
|
18
|
+
* diagnosticsMetrics.recordLatency(process.hrtime(start), { operation: 'api-call', status: '2xx' })
|
|
19
|
+
*
|
|
20
|
+
* // Or from milliseconds
|
|
21
|
+
* diagnosticsMetrics.recordLatency(42.5, { operation: 'db-query', status: 'success' })
|
|
22
|
+
*
|
|
23
|
+
* // Increment a counter
|
|
24
|
+
* diagnosticsMetrics.incrementCounter('http_requests_total', 1, { method: 'GET', status: '2xx' })
|
|
25
|
+
*
|
|
26
|
+
* // Set a gauge value
|
|
27
|
+
* diagnosticsMetrics.setGauge('cache_items_current', 1024, { cache: 'pages' })
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare class DiagnosticsMetrics {
|
|
31
|
+
private metricsClient;
|
|
32
|
+
private clientInitPromise;
|
|
33
|
+
private latencyHistogram;
|
|
34
|
+
private counters;
|
|
35
|
+
private gauges;
|
|
36
|
+
constructor();
|
|
37
|
+
/**
|
|
38
|
+
* Initialize the metrics client with timeout handling.
|
|
39
|
+
* Called automatically in constructor.
|
|
40
|
+
*/
|
|
41
|
+
private initMetricClient;
|
|
42
|
+
/**
|
|
43
|
+
* Create the single shared histogram for all latency measurements.
|
|
44
|
+
* Called after metric client is initialized.
|
|
45
|
+
*/
|
|
46
|
+
private createLatencyHistogram;
|
|
47
|
+
/**
|
|
48
|
+
* Record a latency measurement using the single shared histogram.
|
|
49
|
+
* Accepts either an hrtime tuple from process.hrtime() or milliseconds as a number.
|
|
50
|
+
* Use attributes to differentiate between different operations.
|
|
51
|
+
*
|
|
52
|
+
* @param value Either [seconds, nanoseconds] from process.hrtime() or milliseconds
|
|
53
|
+
* @param attributes Attributes including 'operation' to identify the operation type (max of MAX_ATTRIBUTES attributes)
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* const start = process.hrtime()
|
|
58
|
+
* // ... do work ...
|
|
59
|
+
* diagnosticsMetrics.recordLatency(process.hrtime(start), { operation: 'api-call', status: '2xx' })
|
|
60
|
+
*
|
|
61
|
+
* // Or with milliseconds
|
|
62
|
+
* diagnosticsMetrics.recordLatency(42.5, { operation: 'db-query', status: 'success' })
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
recordLatency(value: [number, number] | number, attributes?: Attributes): void;
|
|
66
|
+
/**
|
|
67
|
+
* Increment a counter by a specific value.
|
|
68
|
+
* Multiple counters are stored by name since counters represent different types of events.
|
|
69
|
+
*
|
|
70
|
+
* @param name Counter name (e.g., 'http_requests_total', 'cache_hits_total')
|
|
71
|
+
* @param value Amount to increment by (typically 1)
|
|
72
|
+
* @param attributes Optional attributes for the counter (max of MAX_ATTRIBUTES attributes, e.g., { method: 'GET', status: '2xx' })
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* diagnosticsMetrics.incrementCounter('http_requests_total', 1, { method: 'GET', status: '2xx' })
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
incrementCounter(name: string, value: number, attributes?: Attributes): void;
|
|
80
|
+
/**
|
|
81
|
+
* Set a gauge to a specific value (current state).
|
|
82
|
+
* Multiple gauges are stored by name since gauges represent different types of measurements.
|
|
83
|
+
*
|
|
84
|
+
* @param name Gauge name (e.g., 'cache_items_current', 'memory_usage_bytes')
|
|
85
|
+
* @param value Current value
|
|
86
|
+
* @param attributes Optional attributes for the gauge (max of MAX_ATTRIBUTES attributes, e.g., { cache: 'pages' })
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```typescript
|
|
90
|
+
* diagnosticsMetrics.setGauge('cache_items_current', 1024, { cache: 'pages' })
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
setGauge(name: string, value: number, attributes?: Attributes): void;
|
|
94
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DiagnosticsMetrics = void 0;
|
|
4
|
+
const client_1 = require("../service/metrics/client");
|
|
5
|
+
const constants_1 = require("../constants");
|
|
6
|
+
/**
|
|
7
|
+
* Maximum number of attributes allowed per metric to control cardinality.
|
|
8
|
+
* This limit applies to external usage (VTEX IO Apps) to prevent unbounded metric dimensions.
|
|
9
|
+
*/
|
|
10
|
+
const MAX_ATTRIBUTES = 5;
|
|
11
|
+
/**
|
|
12
|
+
* Converts an hrtime tuple [seconds, nanoseconds] to milliseconds.
|
|
13
|
+
*/
|
|
14
|
+
function hrtimeToMillis(hrtime) {
|
|
15
|
+
return (hrtime[0] * 1e3) + (hrtime[1] / 1e6);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Limits the number of attributes to prevent high cardinality.
|
|
19
|
+
* Takes the first MAX_ATTRIBUTES entries if the limit is exceeded.
|
|
20
|
+
*
|
|
21
|
+
* @param attributes Optional attributes object
|
|
22
|
+
* @returns Limited attributes object or undefined
|
|
23
|
+
*/
|
|
24
|
+
function limitAttributes(attributes) {
|
|
25
|
+
if (!attributes) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const entries = Object.entries(attributes);
|
|
29
|
+
if (entries.length <= MAX_ATTRIBUTES) {
|
|
30
|
+
return attributes;
|
|
31
|
+
}
|
|
32
|
+
if (constants_1.LINKED) {
|
|
33
|
+
console.warn(`Attribute limit exceeded: ${entries.length} attributes provided, using only the first ${MAX_ATTRIBUTES}. ` +
|
|
34
|
+
`Consider reducing the number of attributes to avoid high cardinality.`);
|
|
35
|
+
}
|
|
36
|
+
return Object.fromEntries(entries.slice(0, MAX_ATTRIBUTES));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* DiagnosticsMetrics provides a high-level API for recording metrics using
|
|
40
|
+
* the @vtex/diagnostics-nodejs library. It completely abstracts instrument
|
|
41
|
+
* management, bucket configuration, and lifecycle.
|
|
42
|
+
*
|
|
43
|
+
* Uses a single histogram for all latency measurements with attributes to differentiate.
|
|
44
|
+
* This follows OpenTelemetry best practices and reduces metric cardinality.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* const diagnosticsMetrics = new DiagnosticsMetrics()
|
|
49
|
+
* diagnosticsMetrics.initMetricClient()
|
|
50
|
+
*
|
|
51
|
+
* // Record latency with operation type in attributes
|
|
52
|
+
* const start = process.hrtime()
|
|
53
|
+
* // ... do work ...
|
|
54
|
+
* diagnosticsMetrics.recordLatency(process.hrtime(start), { operation: 'api-call', status: '2xx' })
|
|
55
|
+
*
|
|
56
|
+
* // Or from milliseconds
|
|
57
|
+
* diagnosticsMetrics.recordLatency(42.5, { operation: 'db-query', status: 'success' })
|
|
58
|
+
*
|
|
59
|
+
* // Increment a counter
|
|
60
|
+
* diagnosticsMetrics.incrementCounter('http_requests_total', 1, { method: 'GET', status: '2xx' })
|
|
61
|
+
*
|
|
62
|
+
* // Set a gauge value
|
|
63
|
+
* diagnosticsMetrics.setGauge('cache_items_current', 1024, { cache: 'pages' })
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
class DiagnosticsMetrics {
|
|
67
|
+
constructor() {
|
|
68
|
+
this.counters = new Map();
|
|
69
|
+
this.gauges = new Map();
|
|
70
|
+
this.initMetricClient();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Initialize the metrics client with timeout handling.
|
|
74
|
+
* Called automatically in constructor.
|
|
75
|
+
*/
|
|
76
|
+
initMetricClient() {
|
|
77
|
+
if (this.clientInitPromise) {
|
|
78
|
+
return this.clientInitPromise;
|
|
79
|
+
}
|
|
80
|
+
this.clientInitPromise = (async () => {
|
|
81
|
+
try {
|
|
82
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
83
|
+
setTimeout(() => reject(new Error('Metric client initialization timeout')), constants_1.METRIC_CLIENT_INIT_TIMEOUT_MS);
|
|
84
|
+
});
|
|
85
|
+
this.metricsClient = await Promise.race([
|
|
86
|
+
(0, client_1.getMetricClient)(),
|
|
87
|
+
timeoutPromise
|
|
88
|
+
]);
|
|
89
|
+
// Create the single latency histogram after client is ready
|
|
90
|
+
this.createLatencyHistogram();
|
|
91
|
+
return this.metricsClient;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
console.error('Failed to initialize metric client:', error);
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
})();
|
|
98
|
+
return this.clientInitPromise;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Create the single shared histogram for all latency measurements.
|
|
102
|
+
* Called after metric client is initialized.
|
|
103
|
+
*/
|
|
104
|
+
createLatencyHistogram() {
|
|
105
|
+
if (!this.metricsClient) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
this.latencyHistogram = this.metricsClient.createHistogram('io_app_operation_duration_milliseconds', {
|
|
109
|
+
description: 'Duration of VTEX IO app operations in milliseconds',
|
|
110
|
+
unit: 'ms',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Record a latency measurement using the single shared histogram.
|
|
115
|
+
* Accepts either an hrtime tuple from process.hrtime() or milliseconds as a number.
|
|
116
|
+
* Use attributes to differentiate between different operations.
|
|
117
|
+
*
|
|
118
|
+
* @param value Either [seconds, nanoseconds] from process.hrtime() or milliseconds
|
|
119
|
+
* @param attributes Attributes including 'operation' to identify the operation type (max of MAX_ATTRIBUTES attributes)
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```typescript
|
|
123
|
+
* const start = process.hrtime()
|
|
124
|
+
* // ... do work ...
|
|
125
|
+
* diagnosticsMetrics.recordLatency(process.hrtime(start), { operation: 'api-call', status: '2xx' })
|
|
126
|
+
*
|
|
127
|
+
* // Or with milliseconds
|
|
128
|
+
* diagnosticsMetrics.recordLatency(42.5, { operation: 'db-query', status: 'success' })
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
recordLatency(value, attributes) {
|
|
132
|
+
if (!this.latencyHistogram) {
|
|
133
|
+
console.warn('DiagnosticsMetrics not initialized. Call initialize() first.');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// Convert hrtime to milliseconds if needed
|
|
137
|
+
const milliseconds = Array.isArray(value) ? hrtimeToMillis(value) : value;
|
|
138
|
+
// Limit attributes to prevent high cardinality
|
|
139
|
+
const limitedAttributes = limitAttributes(attributes);
|
|
140
|
+
// Record to the single shared histogram with attributes
|
|
141
|
+
this.latencyHistogram.record(milliseconds, limitedAttributes);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Increment a counter by a specific value.
|
|
145
|
+
* Multiple counters are stored by name since counters represent different types of events.
|
|
146
|
+
*
|
|
147
|
+
* @param name Counter name (e.g., 'http_requests_total', 'cache_hits_total')
|
|
148
|
+
* @param value Amount to increment by (typically 1)
|
|
149
|
+
* @param attributes Optional attributes for the counter (max of MAX_ATTRIBUTES attributes, e.g., { method: 'GET', status: '2xx' })
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* diagnosticsMetrics.incrementCounter('http_requests_total', 1, { method: 'GET', status: '2xx' })
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
incrementCounter(name, value, attributes) {
|
|
157
|
+
if (!this.metricsClient) {
|
|
158
|
+
console.warn('DiagnosticsMetrics not initialized. Call initialize() first.');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// Get or create counter instrument
|
|
162
|
+
if (!this.counters.has(name)) {
|
|
163
|
+
const counter = this.metricsClient.createCounter(name, {
|
|
164
|
+
description: `Counter for ${name}`,
|
|
165
|
+
unit: '1',
|
|
166
|
+
});
|
|
167
|
+
this.counters.set(name, counter);
|
|
168
|
+
}
|
|
169
|
+
// Limit attributes to prevent high cardinality
|
|
170
|
+
const limitedAttributes = limitAttributes(attributes);
|
|
171
|
+
// Increment the counter
|
|
172
|
+
this.counters.get(name).add(value, limitedAttributes);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Set a gauge to a specific value (current state).
|
|
176
|
+
* Multiple gauges are stored by name since gauges represent different types of measurements.
|
|
177
|
+
*
|
|
178
|
+
* @param name Gauge name (e.g., 'cache_items_current', 'memory_usage_bytes')
|
|
179
|
+
* @param value Current value
|
|
180
|
+
* @param attributes Optional attributes for the gauge (max of MAX_ATTRIBUTES attributes, e.g., { cache: 'pages' })
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```typescript
|
|
184
|
+
* diagnosticsMetrics.setGauge('cache_items_current', 1024, { cache: 'pages' })
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
setGauge(name, value, attributes) {
|
|
188
|
+
if (!this.metricsClient) {
|
|
189
|
+
console.warn('DiagnosticsMetrics not initialized. Call initialize() first.');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Get or create gauge instrument
|
|
193
|
+
if (!this.gauges.has(name)) {
|
|
194
|
+
const gauge = this.metricsClient.createGauge(name, {
|
|
195
|
+
description: `Gauge for ${name}`,
|
|
196
|
+
unit: '1',
|
|
197
|
+
});
|
|
198
|
+
this.gauges.set(name, gauge);
|
|
199
|
+
}
|
|
200
|
+
// Limit attributes to prevent high cardinality
|
|
201
|
+
const limitedAttributes = limitAttributes(attributes);
|
|
202
|
+
// Set the gauge value
|
|
203
|
+
this.gauges.get(name).set(value, limitedAttributes);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
exports.DiagnosticsMetrics = DiagnosticsMetrics;
|
package/lib/service/index.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { MetricsAccumulator } from '../metrics/MetricsAccumulator';
|
|
2
|
+
import { DiagnosticsMetrics } from '../metrics/DiagnosticsMetrics';
|
|
2
3
|
export declare const startApp: () => Promise<void>;
|
|
3
4
|
export { appPath } from './loaders';
|
|
4
5
|
declare global {
|
|
5
6
|
namespace NodeJS {
|
|
6
7
|
interface Global {
|
|
7
8
|
metrics: MetricsAccumulator;
|
|
9
|
+
diagnosticsMetrics: DiagnosticsMetrics;
|
|
8
10
|
}
|
|
9
11
|
}
|
|
10
12
|
}
|
package/lib/service/index.js
CHANGED
|
@@ -31,12 +31,17 @@ const telemetry_1 = require("./telemetry");
|
|
|
31
31
|
const cluster_1 = __importDefault(require("cluster"));
|
|
32
32
|
const constants_1 = require("../constants");
|
|
33
33
|
const MetricsAccumulator_1 = require("../metrics/MetricsAccumulator");
|
|
34
|
+
const DiagnosticsMetrics_1 = require("../metrics/DiagnosticsMetrics");
|
|
34
35
|
const loaders_1 = require("./loaders");
|
|
35
36
|
const logger_1 = require("./logger");
|
|
36
37
|
const startApp = async () => {
|
|
37
38
|
await (0, telemetry_1.initializeTelemetry)();
|
|
38
|
-
// Initialize
|
|
39
|
+
// Initialize both legacy and new metrics systems independently
|
|
40
|
+
// Legacy: MetricsAccumulator for backward compatibility
|
|
41
|
+
// External apps and internal code can continue using this during migration
|
|
39
42
|
global.metrics = new MetricsAccumulator_1.MetricsAccumulator();
|
|
43
|
+
// New: DiagnosticsMetrics for new diagnostics-based instrumentation
|
|
44
|
+
global.diagnosticsMetrics = new DiagnosticsMetrics_1.DiagnosticsMetrics();
|
|
40
45
|
const serviceJSON = (0, loaders_1.getServiceJSON)();
|
|
41
46
|
try {
|
|
42
47
|
// if it is a master process then call setting up worker process
|
|
@@ -21,11 +21,28 @@ 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
|
+
component: 'graphql',
|
|
35
|
+
field_name: name,
|
|
36
|
+
status,
|
|
37
|
+
};
|
|
38
|
+
// Record latency histogram (record all requests, not just successful ones)
|
|
39
|
+
global.diagnosticsMetrics.recordLatency(ellapsed, attributes);
|
|
40
|
+
// Increment counter (status is an attribute, not in metric name)
|
|
41
|
+
global.diagnosticsMetrics.incrementCounter('graphql_field_requests_total', 1, attributes);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
console.warn('DiagnosticsMetrics not available. GraphQL field metrics not reported.');
|
|
45
|
+
}
|
|
29
46
|
if (failedToResolve) {
|
|
30
47
|
throw result;
|
|
31
48
|
}
|
|
@@ -22,20 +22,56 @@ class IncomingRequestStats {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
exports.incomingRequestStats = new IncomingRequestStats();
|
|
25
|
-
const requestClosed = () => {
|
|
25
|
+
const requestClosed = (ctx) => () => {
|
|
26
26
|
exports.incomingRequestStats.closed++;
|
|
27
|
+
// Report to diagnostics metrics (cumulative counter)
|
|
28
|
+
const { status: statusCode, vtex: { route: { id, type } } } = ctx;
|
|
29
|
+
if (global.diagnosticsMetrics) {
|
|
30
|
+
global.diagnosticsMetrics.incrementCounter('http_server_requests_closed_total', 1, {
|
|
31
|
+
route_id: id,
|
|
32
|
+
route_type: type,
|
|
33
|
+
status_code: statusCode,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
console.warn('DiagnosticsMetrics not available. Request closed metric not reported.');
|
|
38
|
+
}
|
|
27
39
|
};
|
|
28
40
|
const requestAborted = (ctx) => () => {
|
|
29
41
|
exports.incomingRequestStats.aborted++;
|
|
42
|
+
// Report to diagnostics metrics (cumulative counter)
|
|
43
|
+
const { status: statusCode, vtex: { route: { id, type } } } = ctx;
|
|
44
|
+
if (global.diagnosticsMetrics) {
|
|
45
|
+
global.diagnosticsMetrics.incrementCounter('http_server_requests_aborted_total', 1, {
|
|
46
|
+
route_id: id,
|
|
47
|
+
route_type: type,
|
|
48
|
+
status_code: statusCode,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.warn('DiagnosticsMetrics not available. Request aborted metric not reported.');
|
|
53
|
+
}
|
|
30
54
|
if (ctx.vtex.cancellation && ctx.vtex.cancellation.cancelable) {
|
|
31
55
|
ctx.vtex.cancellation.source.cancel(exports.cancelMessage);
|
|
32
56
|
ctx.vtex.cancellation.cancelled = true;
|
|
33
57
|
}
|
|
34
58
|
};
|
|
35
59
|
async function trackIncomingRequestStats(ctx, next) {
|
|
36
|
-
ctx.req.on('close', requestClosed);
|
|
60
|
+
ctx.req.on('close', requestClosed(ctx));
|
|
37
61
|
ctx.req.on('aborted', requestAborted(ctx));
|
|
38
62
|
exports.incomingRequestStats.total++;
|
|
63
|
+
// Report total requests to diagnostics metrics (cumulative counter)
|
|
64
|
+
const { status: statusCode, vtex: { route: { id, type } } } = ctx;
|
|
65
|
+
if (global.diagnosticsMetrics) {
|
|
66
|
+
global.diagnosticsMetrics.incrementCounter('http_server_requests_total', 1, {
|
|
67
|
+
route_id: id,
|
|
68
|
+
route_type: type,
|
|
69
|
+
status_code: statusCode,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
console.warn('DiagnosticsMetrics not available. Request total metric not reported.');
|
|
74
|
+
}
|
|
39
75
|
await next();
|
|
40
76
|
}
|
|
41
77
|
exports.trackIncomingRequestStats = trackIncomingRequestStats;
|
|
@@ -35,12 +35,30 @@ const logBillingInfo = ({ account, workspace, production, route: { id, type } },
|
|
|
35
35
|
async function timings(ctx, next) {
|
|
36
36
|
// Errors will be caught by the next middleware so we don't have to catch.
|
|
37
37
|
await next();
|
|
38
|
-
const { status: statusCode, vtex: { route: { id } }, timings: { total }, vtex } = ctx;
|
|
38
|
+
const { status: statusCode, vtex: { route: { id, type } }, timings: { total }, vtex } = ctx;
|
|
39
39
|
const totalMillis = (0, time_1.hrToMillis)(total);
|
|
40
40
|
console.log(log(ctx, totalMillis));
|
|
41
41
|
console.log(logBillingInfo(vtex, totalMillis));
|
|
42
42
|
const status = (0, status_1.statusLabel)(statusCode);
|
|
43
|
+
// Legacy metrics (backward compatibility)
|
|
43
44
|
// Only batch successful responses so metrics don't consider errors
|
|
44
45
|
metrics.batch(`http-handler-${id}`, status === 'success' ? total : undefined, { [status]: 1 });
|
|
46
|
+
// New diagnostics metrics with stable names and attributes
|
|
47
|
+
if (global.diagnosticsMetrics) {
|
|
48
|
+
const attributes = {
|
|
49
|
+
component: 'http-handler',
|
|
50
|
+
route_id: id,
|
|
51
|
+
route_type: type,
|
|
52
|
+
status_code: statusCode,
|
|
53
|
+
status,
|
|
54
|
+
};
|
|
55
|
+
// Record latency histogram (record all requests, not just successful ones)
|
|
56
|
+
global.diagnosticsMetrics.recordLatency(total, attributes);
|
|
57
|
+
// Increment counter (status is an attribute, not in metric name)
|
|
58
|
+
global.diagnosticsMetrics.incrementCounter('http_handler_requests_total', 1, attributes);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.warn('DiagnosticsMetrics not available. HTTP handler metrics not reported.');
|
|
62
|
+
}
|
|
45
63
|
}
|
|
46
64
|
exports.timings = timings;
|
|
@@ -6,6 +6,7 @@ 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
8
|
const constants_1 = require("../../../constants");
|
|
9
|
+
const HttpAgentSingleton_1 = require("../../../HttpClient/middlewares/request/HttpAgentSingleton");
|
|
9
10
|
const BROADCAST_STATUS_TRACK = 'broadcastStatusTrack';
|
|
10
11
|
const STATUS_TRACK = 'statusTrack';
|
|
11
12
|
const isStatusTrack = (message) => message === STATUS_TRACK;
|
|
@@ -23,6 +24,9 @@ const statusTrackHandler = async (ctx) => {
|
|
|
23
24
|
};
|
|
24
25
|
exports.statusTrackHandler = statusTrackHandler;
|
|
25
26
|
const trackStatus = () => {
|
|
27
|
+
// Update diagnostics metrics (gauges for HTTP agent stats)
|
|
28
|
+
HttpAgentSingleton_1.HttpAgentSingleton.updateHttpAgentMetrics();
|
|
29
|
+
// Legacy status tracking (console.log export)
|
|
26
30
|
global.metrics.statusTrack().forEach(status => {
|
|
27
31
|
logStatus(status);
|
|
28
32
|
});
|