sdk_contrib/fetch/lib/fetch_p.js (113 lines of code) (raw):

/** * @module fetch_p */ /** * This module patches the global fetch instance for NodeJS 18+ */ const AWSXRay = require('aws-xray-sdk-core'); const utils = AWSXRay.utils; const getLogger = AWSXRay.getLogger; require('./subsegment_fetch'); /** * Wrap fetch global instance for recent NodeJS to automatically capture information for the segment. * This patches the built-in fetch function globally. * @param {boolean} downstreamXRayEnabled - when true, adds a "traced:true" property to the subsegment * so the AWS X-Ray service expects a corresponding segment from the downstream service. * @param {function} subsegmentCallback - a callback that is called with the subsegment, the fetch request, * the fetch response and any error issued, allowing custom annotations and metadata to be added. * @alias module:fetch_p.captureFetchGlobal */ function captureFetchGlobal(downstreamXRayEnabled, subsegmentCallback) { if (globalThis.fetch === undefined) { throw new Error('Global fetch is not available in NodeJS'); } if (!globalThis.__fetch) { globalThis.__fetch = globalThis.fetch; globalThis.fetch = enableCapture(globalThis.__fetch, globalThis.Request, downstreamXRayEnabled, subsegmentCallback); } return globalThis.fetch; } /** * Wrap fetch module to capture information for the segment. * This patches the fetch function distributed in node-fetch package. * @param {fetch} module - The fetch module * @param {boolean} downstreamXRayEnabled - when true, adds a "traced:true" property to the subsegment * so the AWS X-Ray service expects a corresponding segment from the downstream service. * @param {function} subsegmentCallback - a callback that is called with the subsegment, the fetch request, * the fetch response and any error issued, allowing custom annotations and metadata to be added. * @alias module:fetch_p.captureFetchModule */ function captureFetchModule(module, downstreamXRayEnabled, subsegmentCallback) { if (!module.default) { getLogger().warn('X-ray capture did not find fetch function in module'); return null; } if (!module.__fetch) { module.__fetch = module.default; module.default = enableCapture(module.__fetch, module.Request, downstreamXRayEnabled, subsegmentCallback); } return module.default; } /** * Return a fetch function that will pass segment information to the target host. * This does not change any globals * @param {function} baseFetchFunction fetch function to use as basis * @param {function} requestClass Request class to use. This should correspond to the supplied fetch function. * @param {boolean} downstreamXRayEnabled - when true, adds a "traced:true" property to the subsegment * so the AWS X-Ray service expects a corresponding segment from the downstream service. * @param {function} subsegmentCallback - a callback that is called with the subsegment, the fetch request, * the fetch response and any error issued, allowing custom annotations and metadata to be added. * @returns Response */ function enableCapture(baseFetchFunction, requestClass, downstreamXRayEnabled, subsegmentCallback) { const overridenFetchAsync = async (...args) => { const thisDownstreamXRayEnabled = !!downstreamXRayEnabled; const thisSubsegmentCallback = subsegmentCallback; // Standardize request information const request = typeof args[0] === 'object' ? args[0] : new requestClass(...args); // Facilitate the addition of Segment information via the request arguments const params = args.length > 1 ? args[1] : {}; // Short circuit if the HTTP is already being captured if (request.headers.has('X-Amzn-Trace-Id')) { return await baseFetchFunction(...args); } const url = new URL(request.url); const isAutomaticMode = AWSXRay.isAutomaticMode(); const parent = AWSXRay.resolveSegment(AWSXRay.resolveManualSegmentParams(params)); const hostname = url.hostname || url.host || 'Unknown host'; if (!parent) { let output = '[ host: ' + hostname + (request.method ? (', method: ' + request.method) : '') + ', path: ' + url.pathname + ' ]'; if (isAutomaticMode) { getLogger().info('RequestInit for request ' + output + ' is missing the sub/segment context for automatic mode. Ignoring.'); } else { getLogger().info('RequestInit for request ' + output + ' requires a segment object on the options params as "XRaySegment" for tracing in manual mode. Ignoring.'); } // Options are not modified, only parsed for logging. We can pass in the original arguments. return await baseFetchFunction(...args); } let subsegment; if (parent.notTraced) { subsegment = parent.addNewSubsegmentWithoutSampling(hostname); } else { subsegment = parent.addNewSubsegment(hostname); } subsegment.namespace = 'remote'; if (!parent.noOp) { request.headers.set('X-Amzn-Trace-Id', 'Root=' + (parent.segment ? parent.segment : parent).trace_id + ';Parent=' + subsegment.id + ';Sampled=' + (subsegment.notTraced ? '0' : '1')); } // Set up fetch call and capture any thrown errors const capturedFetch = async () => { const requestClone = request.clone(); let response; try { response = await baseFetchFunction(requestClone); if (thisSubsegmentCallback) { thisSubsegmentCallback(subsegment, requestClone, response); } const statusCode = response.status; if (statusCode === 429) { subsegment.addThrottleFlag(); } const cause = utils.getCauseTypeFromHttpStatus(statusCode); if (cause) { subsegment[cause] = true; } subsegment.addFetchRequestData(requestClone, response, thisDownstreamXRayEnabled); subsegment.close(); return response; } catch (e) { if (thisSubsegmentCallback) { thisSubsegmentCallback(subsegment, requestClone, response, e); } const madeItToDownstream = (e.code !== 'ECONNREFUSED'); subsegment.addErrorFlag(); subsegment.addFetchRequestData(requestClone, response, madeItToDownstream && thisDownstreamXRayEnabled); subsegment.close(e); throw (e); } }; if (isAutomaticMode) { const session = AWSXRay.getNamespace(); return await session.runPromise(async () => { AWSXRay.setSegment(subsegment); return await capturedFetch(); }); } else { return await capturedFetch(); } }; return overridenFetchAsync; } module.exports.captureFetchGlobal = captureFetchGlobal; module.exports.captureFetchModule = captureFetchModule; module.exports.enableCapture = enableCapture;