@vtex/api
Version:
VTEX I/O API client
160 lines (159 loc) • 7.88 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.metricsMiddleware = void 0;
const constants_1 = require("../../constants");
const RequestCancelledError_1 = require("../../errors/RequestCancelledError");
const requestStats_1 = require("../../service/worker/runtime/http/middlewares/requestStats");
const utils_1 = require("../../utils");
const retry_1 = require("../../utils/retry");
const status_1 = require("../../utils/status");
const DEFAULT_ACCOUNT = 'unknown';
const metricsMiddleware = ({ metrics, serverTiming, name }) => {
const serverTimingStart = process.hrtime();
const serverTimingLabel = (0, utils_1.shrinkTimings)((0, utils_1.formatTimingName)({
hopNumber: 0,
source: process.env.VTEX_APP_NAME,
target: name || 'unknown',
}));
return async (ctx, next) => {
var _a, _b;
const start = process.hrtime();
let status = 'unknown';
let errorCode;
let errorStatus;
try {
if (ctx.config.verbose && ctx.config.label) {
console.log(`VERBOSE: ${name}.${ctx.config.label}`, `start`);
}
await next();
if (ctx.config.metric && ctx.response && ctx.response.status) {
status = (0, status_1.statusLabel)(ctx.response.status);
}
}
catch (err) {
const isCancelled = (err.message === requestStats_1.cancelMessage);
if (ctx.config.metric) {
errorCode = err.code;
errorStatus = err.response && err.response.status;
if (err.code === 'ECONNABORTED') {
status = 'aborted';
}
else if (err.response && err.response.data && err.response.data.code === retry_1.TIMEOUT_CODE) {
status = 'timeout';
}
else if (err.response && err.response.status) {
status = (0, status_1.statusLabel)(err.response.status);
}
else if (isCancelled) {
status = 'cancelled';
}
else {
status = 'error';
}
}
throw isCancelled
? new RequestCancelledError_1.RequestCancelledError(err.message)
: err;
}
finally {
if (ctx.config.metric && metrics) {
const label = `http-client-${ctx.config.metric}`;
const extensions = {};
Object.assign(extensions, { [status]: 1 });
// Determine cache state for diagnostics metrics
let cacheState = 'none';
if (ctx.cacheHit) {
Object.assign(extensions, ctx.cacheHit, { [`${status}-hit`]: 1 });
cacheState = 'hit';
}
else if (!ctx.inflightHit && !ctx.memoizedHit) {
// Lets us know how many calls passed through to origin
Object.assign(extensions, { [`${status}-miss`]: 1 });
cacheState = 'miss';
}
if (ctx.inflightHit) {
Object.assign(extensions, { [`${status}-inflight`]: 1 });
cacheState = 'inflight';
}
if (ctx.memoizedHit) {
Object.assign(extensions, { [`${status}-memoized`]: 1 });
cacheState = 'memoized';
}
const retryCount = ctx.config.retryCount || 0;
if (retryCount > 0) {
extensions[`retry-${status}-${retryCount}`] = 1;
}
const end = status === 'success' && !ctx.cacheHit && !ctx.inflightHit && !ctx.memoizedHit
? process.hrtime(start)
: undefined;
// Legacy metrics (backward compatibility)
metrics.batch(label, end, extensions);
// New diagnostics metrics with stable names and attributes
if (global.diagnosticsMetrics) {
const elapsed = process.hrtime(start);
const rawStatusCode = ((_a = ctx.response) === null || _a === void 0 ? void 0 : _a.status) || errorStatus;
// Extract account from request headers, fallback to default
const account = ((_b = ctx.config.headers) === null || _b === void 0 ? void 0 : _b[constants_1.HeaderKeys.ACCOUNT]) || DEFAULT_ACCOUNT;
const baseAttributes = {
[constants_1.AttributeKeys.VTEX_ACCOUNT_NAME]: account,
component: 'http-client',
client_metric: ctx.config.metric,
status_code: rawStatusCode,
status,
};
// Record latency histogram with all context
global.diagnosticsMetrics.recordLatency(elapsed, {
...baseAttributes,
cache_state: cacheState,
});
// Increment counters for different event types (replaces extensions behavior)
// Main request counter with status as attribute
global.diagnosticsMetrics.incrementCounter('http_client_requests_total', 1, baseAttributes);
// Cache counter with cache_state as attribute (replaces extensions like 'success-hit', 'error-miss')
if (cacheState !== 'none') {
global.diagnosticsMetrics.incrementCounter('http_client_cache_total', 1, {
...baseAttributes,
cache_state: cacheState,
});
}
// Retry counter (replaces 'retry-{status}-{count}' extensions)
if (retryCount > 0) {
global.diagnosticsMetrics.incrementCounter('http_client_requests_retried_total', 1, {
...baseAttributes,
retry_count: retryCount,
});
}
}
else {
console.warn('DiagnosticsMetrics not available. HTTP client metrics not reported.');
}
if (ctx.config.verbose) {
console.log(`VERBOSE: ${name}.${ctx.config.label}`, {
...extensions,
...errorCode || errorStatus ? { errorCode, errorStatus } : null,
millis: end
? (0, utils_1.hrToMillis)(end)
: extensions.revalidated || extensions.router || status !== 'success'
? (0, utils_1.hrToMillis)(process.hrtime(start))
: '(from cache)',
status: ctx.response && ctx.response.status,
headers: ctx.response && ctx.response.headers,
});
}
}
else {
if (ctx.config.verbose) {
console.warn(`PROTIP: Please add a metric property to ${name} client request to get metrics in Splunk`, { baseURL: ctx.config.baseURL, url: ctx.config.url });
}
}
if (serverTiming) {
// Timings in the client's perspective
const dur = (0, utils_1.hrToMillis)(process.hrtime(serverTimingStart));
if (!serverTiming[serverTimingLabel] || Number(serverTiming[serverTimingLabel]) < dur) {
serverTiming[serverTimingLabel] = `${dur}`;
}
}
}
};
};
exports.metricsMiddleware = metricsMiddleware;