Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fetch #590

Merged
merged 30 commits into from
Mar 9, 2024
Merged

Fetch #590

Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2f3efc8
Add support for including sql query in sql subsegment for MySQL
Jan 26, 2023
a8228e5
Update createSqlData to accept values and sql as arguments, and the c…
Jan 28, 2023
08ad919
Add function comments to mysqL_p.createSqlData
Feb 21, 2023
9c1758e
Merge remote-tracking branch 'upstream/master'
Apr 4, 2023
3f53f08
Merge remote-tracking branch 'upstream/master'
Apr 6, 2023
9855c14
Working Node 14 and 18 tests for fetch
Apr 6, 2023
ef5108a
Auto/manual fetch support
Apr 20, 2023
02248f3
Add typescript defs for fetch, insure node-fetch tests run in NodeJS 14
May 9, 2023
1b96361
Merge remote-tracking branch 'upstream/master' into fetch
May 29, 2023
8ecfd6f
Moved fetch patcher to sdk_contrib
May 30, 2023
2b42c8f
Added integration testing for captureFetch
May 30, 2023
925ea85
Revert tests to Javascript
May 30, 2023
9de92ee
Add chai-as-promised to base package.json dev dependencies, because y…
May 30, 2023
5d0c457
Try fixing package*.json
May 30, 2023
4097c51
Merge remote-tracking branch 'upstream/master' into fetch
Nov 23, 2023
8a1eb78
Change var to const
Dec 18, 2023
49951cb
Removed captureFetch per @jj22ee
Dec 18, 2023
675921f
Removed chai-as-promised and tsconfig.debug.json
Dec 19, 2023
92ea416
wip
Jan 23, 2024
941fc47
wip
Jan 23, 2024
002defc
remove docker files used for diag
Jan 23, 2024
dfbef99
Updates per jj22ee
Jan 23, 2024
6d3644a
Housekeeping
Jan 23, 2024
d2117b0
More housekeeping
Jan 23, 2024
b23bf44
Fix typescript type test
Jan 23, 2024
32651cb
Merge branch 'master' into fetch-pr
jj22ee Feb 26, 2024
9fb28d2
update version and package-lock, fix tests
jj22ee Feb 26, 2024
d7713c3
remove duplicate code
jj22ee Feb 26, 2024
f5f6fdc
Record fetch info in subsegment http property
Mar 8, 2024
dd7f457
Add type def file for subsegment addFetchRequestData method; if url o…
Mar 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29,771 changes: 13,837 additions & 15,934 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
}
},
"devDependencies": {
"@hapi/hapi": "^20.0.0",
"@smithy/config-resolver": "^2.0.15",
"@smithy/middleware-stack": "^2.0.6",
"@smithy/node-config-provider": "^2.1.3",
"@smithy/smithy-client": "^2.1.11",
"@hapi/hapi": "^20.0.0",
"@types/chai": "^4.2.12",
"@types/koa": "^2.11.3",
"@types/mocha": "^8.0.0",
Expand Down Expand Up @@ -45,7 +45,7 @@
"grunt-contrib-clean": "^1.0.0",
"grunt-jsdoc": "^2.4.0",
"koa": "^2.13.0",
"lerna": "^5.5.2",
"lerna": "^5.6.2",
"mocha": "^10.2.0",
"nock": "^13.2.9",
"nyc": "^15.1.0",
Expand All @@ -66,6 +66,7 @@
"aws-xray-sdk-core": "file:packages/core",
"aws-xray-sdk-express": "file:packages/express",
"aws-xray-sdk-fastify": "file:sdk_contrib/fastify",
"aws-xray-sdk-fetch": "file:sdk_contrib/fetch",
"aws-xray-sdk-hapi": "file:sdk_contrib/hapi",
"aws-xray-sdk-koa2": "file:sdk_contrib/koa",
"aws-xray-sdk-mysql": "file:packages/mysql",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,4 @@
],
"license": "Apache-2.0",
"repository": "https://github.com/aws/aws-xray-sdk-node/tree/master/packages/core"
}
}
1 change: 1 addition & 0 deletions sdk_contrib/fetch/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist
3 changes: 3 additions & 0 deletions sdk_contrib/fetch/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../../.eslintrc.json"
}
45 changes: 45 additions & 0 deletions sdk_contrib/fetch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# fetch-xray

A patcher for AWSXRay to support fetch, implemented via either the [node-fetch module](https://www.npmjs.com/package/node-fetch) or the built-in
global fetch support starting with [NodeJS 18](https://nodejs.org/en/blog/announcements/v18-release-announce).

## Usage

```js
const { captureFetchGlobal } = require('aws-xray-sdk-fetch');

// To use globally defined fetch (available in NodeJS 18+)
const fetch = captureFetchGloba();
const result = await fetch('https://foo.com');

// To use node-fetch module
const { captureFetchModule } = require('aws-xray-sdk-fetch');
const fetchModule = require('node-fetch');
const fetch = captureFetchModule(fetchModule); // Note, first parameter *must* be the node-fetch module
const result = await fetch('https://foo.com');
```

There are two optional parameters you can pass when calling `captureFetchGlobal` / `captureFetchModule`:

* **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 (default = False)
* **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

TypeScript bindings for the capture functions are included.

## Testing

Unit and integration tests can be run using `npm run test`. Typings file tess can be run using `npm run test-d`.

## Errata

1. This package CommonJS to conform with the rest of the AWSXRay codebase. As such, it is incompatible with node-fetch 3, which is ESM only. As such, it is written
to be compatible with [node-fetch version 2](https://www.npmjs.com/package/node-fetch#CommonJS), which should still receive critical fixes. If you are using global
fetch (available in NodeJS 18+) then this isn't an issue for you.

2. This package is designed working under the assumption that the NodeJS implementation of fetch is compatible with node-fetch, albeit with its own separate,
built-in typings. If NodeJS takes fetch in a different direction (breaks compatibility) then that would most likely break this package. There is no indication that
I could find that this will happen, but throwing it out there "just in case".

## Contributors

- [Jason Terando](https://github.com/jasonterando)
19 changes: 19 additions & 0 deletions sdk_contrib/fetch/lib/fetch_p.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import AWSXRay from 'aws-xray-sdk-core';
import * as fetchModule from 'node-fetch';

type FetchModuleType = typeof fetchModule;

type fetchModuleFetch = (url: URL | fetchModule.RequestInfo, init?: fetchModule.RequestInit | undefined) => Promise<fetchModule.Response>;

export function captureFetchGlobal(
downstreamXRayEnabled?: boolean,
subsegmentCallback?: (subsegment: AWSXRay.Subsegment, req: Request, res: Response | null, error: Error) => void):
typeof globalThis.fetch;

export function captureFetchModule(
fetch: FetchModuleType,
downstreamXRayEnabled?: boolean,
subsegmentCallback?: (subsegment: AWSXRay.Subsegment, req: fetchModule.Request, res: fetchModule.Response | null, error: Error) => void):
(url: URL | fetchModule.RequestInfo, init?: fetchModule.RequestInit | undefined) => Promise<fetchModule.Response>;

164 changes: 164 additions & 0 deletions sdk_contrib/fetch/lib/fetch_p.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/**
* @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;
}

const enableCapture = 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';

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._fetchEnableCapture = enableCapture;
37 changes: 37 additions & 0 deletions sdk_contrib/fetch/lib/subsegment_fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const {Subsegment} = require('aws-xray-sdk-core');

/**
* Appends remote request data to subsegment similar to what Subsegment.prototype.addRemoteRequestData
* does in core/lib/segments/attributes/subsegment.js
* @param {Subsegment} subsegment
* @param {Fetch Request} request
* @param {Fetch Request or null|undefined} response
* @param {boolean} downstreamXRayEnabled
*/
Subsegment.prototype.addFetchRequestData = function addFetchRequestData(request, response, downstreamXRayEnabled) {
this.http = {
request: {
url: request.url.toString(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
url: request.url.toString(),
url: (request.url) ? request.url.toString() : '',

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, done. I also put in a non-set check for method. .d.ts file is added for Subsegment.addFetchRequestData

method: request.method
}
};

if (downstreamXRayEnabled) {
this.traced = true;
}

if (response) {
this.http.response = {
status: response.status
};
if (response.headers) {
const clength = response.headers.get('content-length');
if (clength) {
const v = parseInt(clength);
if (! Number.isNaN(v)) {
this.http.response.content_length = v;
}
}
}
}
};
50 changes: 50 additions & 0 deletions sdk_contrib/fetch/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "aws-xray-sdk-fetch",
"version": "3.5.4",
"description": "AWS X-Ray plugin for node-fetch",
"author": "Amazon Web Services",
"contributors": [
"Jason Terando <[email protected]>"
],
"main": "lib/fetch_p.js",
"files": [
"lib"
],
"types": "lib/fetch_p.d.ts",
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"test": "mocha --recursive --exit ./test/ -R spec && tsd",
"test-d": "tsd"
},
"engines": {
"node": ">= 14.x"
},
"peerDependencies": {
"aws-xray-sdk-core": "^3.5.4"
},
"keywords": [
"amazon",
"api",
"aws",
"fetch",
"node-fetch",
"xray",
"x-ray",
"x ray"
],
"directories": {
"lib": "lib"
},
"license": "Apache-2.0",
"repository": "https://github.com/aws/aws-xray-sdk-node/tree/sdk_contrib/fetch",
"devDependencies": {
"@types/node-fetch": "^2.6.4",
"chai-as-promised": "^7.1.1",
"node-fetch": "^2.6.11",
"ts-expect": "^1.3.0"
},
"dependencies": {
"tsd": "^0.28.1"
}
}
28 changes: 28 additions & 0 deletions sdk_contrib/fetch/test-d/index.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import AWSXRay from 'aws-xray-sdk-core';
import * as fetchModule from 'node-fetch';
import { expectType } from 'ts-expect';
import { captureFetchGlobal, captureFetchModule } from '../lib/fetch_p';

type ModuleFetch = (url: URL | fetchModule.RequestInfo, init?: fetchModule.RequestInit | undefined) => Promise<fetchModule.Response>;

if (globalThis.fetch !== undefined) {
function fetchGlobalCallback(subsegment: AWSXRay.Subsegment, req: Request, res: Response | null, error: Error) {
console.log({ subsegment, req, res, error });
}

expectType<typeof globalThis.fetch>(captureFetchGlobal());
expectType<typeof globalThis.fetch>(captureFetchGlobal(true));
expectType<typeof globalThis.fetch>(captureFetchGlobal(false));
expectType<typeof globalThis.fetch>(captureFetchGlobal(true, fetchGlobalCallback));
expectType<typeof globalThis.fetch>(captureFetchGlobal(false, fetchGlobalCallback));
}

function fetchModuleCallback(subsegment: AWSXRay.Subsegment, req: fetchModule.Request, res: fetchModule.Response | null, error: Error) {
console.log({ subsegment, req, res, error });
}

expectType<ModuleFetch>(captureFetchModule(fetchModule));
expectType<ModuleFetch>(captureFetchModule(fetchModule, true));
expectType<ModuleFetch>(captureFetchModule(fetchModule, false));
expectType<ModuleFetch>(captureFetchModule(fetchModule, true, fetchModuleCallback));
expectType<ModuleFetch>(captureFetchModule(fetchModule, false, fetchModuleCallback));
Loading
Loading