Skip to content

Commit a9375ff

Browse files
authored
[Fix] Update AWS SDK Instrumentation to inject XRay trace context into HTTP Headers (#131)
*Issue #, if available:* Fixes the absence of broken X-Ray context propagation when the underlying HTTP instrumentation is suppressed or disabled. Similarly to Java and Python, AWS SDK Js instrumentation itself should be able to inject the X-Ray Context into the HTTP Headers: - [Java example](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/81c7713bb2f638c85006c3e152ad13d6e02f3259/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/TracingExecutionInterceptor.java#L308) - [Python example ](https://github.com/open-telemetry/opentelemetry-python-contrib/blob/main/instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py#L163-L165) - See Specification that clarifies that AWS SDK instrumentations should use X-Ray propagator specifically. - https://github.com/open-telemetry/opentelemetry-specification/blob/v1.40.0/supplementary-guidelines/compatibility/aws.md?plain=1#L9-L12 Note - If the underlying HTTP instrumentation is enabled, then the underlying HTTP Child Span of the AWS SDK Span will overwrite the Trace Context to propagate through headers. *Description of changes:* - Move patched/extended instrumentations to an `patches/extended-instrumentations/` directory - Created `AwsSdkInstrumentationExtended` class that extends upstream AwsInstrumentation to override its patching mechanism of the `send` method. The overridden method will additionally update the AWS SDK middleware stack to inject the `X-Amzn-Trace-Id` HTTP header. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 694e4f4 commit a9375ff

File tree

9 files changed

+176
-7
lines changed

9 files changed

+176
-7
lines changed
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
build
22
node_modules
33
.eslintrc.js
4-
version.ts
4+
version.ts
5+
src/third-party

aws-distro-opentelemetry-node-autoinstrumentation/package.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,18 @@
3131
"prepublishOnly": "npm run compile",
3232
"tdd": "yarn test -- --watch-extensions ts --watch",
3333
"test": "nyc ts-mocha --timeout 10000 -p tsconfig.json --require '@opentelemetry/contrib-test-utils' 'test/**/*.ts'",
34-
"test:coverage": "nyc --all --check-coverage --functions 95 --lines 95 ts-mocha --timeout 10000 -p tsconfig.json --require '@opentelemetry/contrib-test-utils' 'test/**/*.ts'",
34+
"test:coverage": "nyc --check-coverage --functions 95 --lines 95 ts-mocha --timeout 10000 -p tsconfig.json --require '@opentelemetry/contrib-test-utils' 'test/**/*.ts'",
3535
"watch": "tsc -w"
3636
},
37+
"nyc": {
38+
"all": true,
39+
"include": [
40+
"src/**/*.ts"
41+
],
42+
"exclude": [
43+
"src/third-party/**/*.ts"
44+
]
45+
},
3746
"bugs": {
3847
"url": "https://github.com/aws-observability/aws-otel-js-instrumentation/issues"
3948
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { AwsInstrumentation } from '@opentelemetry/instrumentation-aws-sdk';
5+
import { context as otelContext, defaultTextMapSetter } from '@opentelemetry/api';
6+
import { AWSXRayPropagator } from '@opentelemetry/propagator-aws-xray';
7+
import type { Command as AwsV3Command } from '@aws-sdk/types';
8+
9+
const awsXrayPropagator = new AWSXRayPropagator();
10+
const V3_CLIENT_CONFIG_KEY = Symbol('opentelemetry.instrumentation.aws-sdk.client.config');
11+
type V3PluginCommand = AwsV3Command<any, any, any, any, any> & {
12+
[V3_CLIENT_CONFIG_KEY]?: any;
13+
};
14+
15+
// This class extends the upstream AwsInstrumentation to override its patching mechanism of the `send` method.
16+
// The overriden method will additionally update the AWS SDK middleware stack to inject the `X-Amzn-Trace-Id` HTTP header.
17+
//
18+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
19+
// @ts-ignore
20+
export class AwsSdkInstrumentationExtended extends AwsInstrumentation {
21+
// Override the upstream private _getV3SmithyClientSendPatch method to add middleware to inject X-Ray Trace Context into HTTP Headers
22+
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/instrumentation-aws-sdk-v0.48.0/plugins/node/opentelemetry-instrumentation-aws-sdk/src/aws-sdk.ts#L373-L384
23+
override _getV3SmithyClientSendPatch(original: (...args: unknown[]) => Promise<any>) {
24+
return function send(this: any, command: V3PluginCommand, ...args: unknown[]): Promise<any> {
25+
this.middlewareStack?.add(
26+
(next: any, context: any) => async (middlewareArgs: any) => {
27+
awsXrayPropagator.inject(otelContext.active(), middlewareArgs.request.headers, defaultTextMapSetter);
28+
const result = await next(middlewareArgs);
29+
return result;
30+
},
31+
{
32+
step: 'build',
33+
name: '_adotInjectXrayContextMiddleware',
34+
override: true,
35+
}
36+
);
37+
38+
command[V3_CLIENT_CONFIG_KEY] = this.config;
39+
return original.apply(this, [command, ...args]);
40+
};
41+
}
42+
}

aws-distro-opentelemetry-node-autoinstrumentation/src/patches/instrumentation-patch.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ import {
2525
} from './aws/services/bedrock';
2626
import { KinesisServiceExtension } from './aws/services/kinesis';
2727
import { S3ServiceExtension } from './aws/services/s3';
28-
import { AwsLambdaInstrumentationPatch } from './aws/services/aws-lambda';
28+
import { AwsLambdaInstrumentationPatch } from './extended-instrumentations/aws-lambda';
29+
import { InstrumentationConfigMap } from '@opentelemetry/auto-instrumentations-node';
30+
import { AwsSdkInstrumentationExtended } from './extended-instrumentations/aws-sdk-instrumentation-extended';
2931

3032
export const traceContextEnvironmentKey = '_X_AMZN_TRACE_ID';
3133
const awsPropagator = new AWSXRayPropagator();
@@ -38,7 +40,10 @@ export const headerGetter: TextMapGetter<APIGatewayProxyEventHeaders> = {
3840
},
3941
};
4042

41-
export function applyInstrumentationPatches(instrumentations: Instrumentation[]): void {
43+
export function applyInstrumentationPatches(
44+
instrumentations: Instrumentation[],
45+
instrumentationConfigs?: InstrumentationConfigMap
46+
): void {
4247
/*
4348
Apply patches to upstream instrumentation libraries.
4449
@@ -50,10 +55,16 @@ export function applyInstrumentationPatches(instrumentations: Instrumentation[])
5055
*/
5156
instrumentations.forEach((instrumentation, index) => {
5257
if (instrumentation.instrumentationName === '@opentelemetry/instrumentation-aws-sdk') {
58+
diag.debug('Overriding aws sdk instrumentation');
59+
instrumentations[index] = new AwsSdkInstrumentationExtended(
60+
instrumentationConfigs ? instrumentationConfigs['@opentelemetry/instrumentation-aws-sdk'] : undefined
61+
);
62+
5363
// Access private property servicesExtensions of AwsInstrumentation
5464
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
5565
// @ts-ignore
56-
const services: Map<string, ServiceExtension> | undefined = (instrumentation as any).servicesExtensions?.services;
66+
const services: Map<string, ServiceExtension> | undefined = (instrumentations[index] as any).servicesExtensions
67+
?.services;
5768
if (services) {
5869
services.set('S3', new S3ServiceExtension());
5970
services.set('Kinesis', new KinesisServiceExtension());

aws-distro-opentelemetry-node-autoinstrumentation/src/register.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ const instrumentationConfigs: InstrumentationConfigMap = {
6262
const instrumentations: Instrumentation[] = getNodeAutoInstrumentations(instrumentationConfigs);
6363

6464
// Apply instrumentation patches
65-
applyInstrumentationPatches(instrumentations);
65+
applyInstrumentationPatches(instrumentations, instrumentationConfigs);
6666

6767
const configurator: AwsOpentelemetryConfigurator = new AwsOpentelemetryConfigurator(instrumentations, useXraySampler);
6868
const configuration: Partial<opentelemetry.NodeSDKConfiguration> = configurator.configure();

aws-distro-opentelemetry-node-autoinstrumentation/test/patches/aws/services/aws-lambda.test.ts renamed to aws-distro-opentelemetry-node-autoinstrumentation/test/patches/extended-instrumentations/aws-lambda.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as path from 'path';
66
import * as fs from 'fs';
77
import { diag } from '@opentelemetry/api';
88
import { InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
9-
import { AwsLambdaInstrumentationPatch } from '../../../../src/patches/aws/services/aws-lambda';
9+
import { AwsLambdaInstrumentationPatch } from '../../../src/patches/extended-instrumentations/aws-lambda';
1010

1111
describe('AwsLambdaInstrumentationPatch', () => {
1212
let instrumentation: AwsLambdaInstrumentationPatch;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import * as sinon from 'sinon';
4+
import { AwsSdkInstrumentationExtended } from '../../../src/patches/extended-instrumentations/aws-sdk-instrumentation-extended';
5+
import expect from 'expect';
6+
import { AWSXRayPropagator } from '@opentelemetry/propagator-aws-xray';
7+
import { Context, TextMapSetter } from '@opentelemetry/api';
8+
9+
describe('AwsSdkInstrumentationExtended', () => {
10+
let instrumentation: AwsSdkInstrumentationExtended;
11+
12+
beforeEach(() => {
13+
instrumentation = new AwsSdkInstrumentationExtended({});
14+
});
15+
16+
afterEach(() => {
17+
sinon.restore();
18+
});
19+
20+
it('overridden _getV3SmithyClientSendPatch updates MiddlewareStack', async () => {
21+
const mockedMiddlewareStackInternal: any = [];
22+
const mockedMiddlewareStack = {
23+
add: (arg1: any, arg2: any) => mockedMiddlewareStackInternal.push([arg1, arg2]),
24+
};
25+
const send = instrumentation
26+
._getV3SmithyClientSendPatch((...args: unknown[]) => Promise.resolve())
27+
.bind({ middlewareStack: mockedMiddlewareStack });
28+
sinon
29+
.stub(AWSXRayPropagator.prototype, 'inject')
30+
.callsFake((context: Context, carrier: unknown, setter: TextMapSetter) => {
31+
(carrier as any)['isCarrierModified'] = 'carrierIsModified';
32+
});
33+
34+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
35+
// @ts-ignore
36+
await send({}, null);
37+
38+
const middlewareArgs: any = {
39+
request: {
40+
headers: {},
41+
},
42+
};
43+
await mockedMiddlewareStackInternal[0][0]((arg: any) => Promise.resolve(), null)(middlewareArgs);
44+
45+
expect(middlewareArgs.request.headers['isCarrierModified']).toEqual('carrierIsModified');
46+
expect(mockedMiddlewareStackInternal[0][1].name).toEqual('_adotInjectXrayContextMiddleware');
47+
});
48+
});

aws-distro-opentelemetry-node-autoinstrumentation/test/patches/instrumentation-patch.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ import * as sinon from 'sinon';
2323
import { AWSXRAY_TRACE_ID_HEADER, AWSXRayPropagator } from '@opentelemetry/propagator-aws-xray';
2424
import { Context } from 'aws-lambda';
2525
import { SinonStub } from 'sinon';
26+
import { S3 } from '@aws-sdk/client-s3';
27+
import nock = require('nock');
28+
import { ReadableSpan } from '@opentelemetry/sdk-trace-base';
29+
import { getTestSpans, registerInstrumentationTesting } from '@opentelemetry/contrib-test-utils';
30+
import { AwsSdkInstrumentationExtended } from '../../src/patches/extended-instrumentations/aws-sdk-instrumentation-extended';
2631

2732
const _STREAM_NAME: string = 'streamName';
2833
const _BUCKET_NAME: string = 'bucketName';
@@ -45,6 +50,10 @@ const UNPATCHED_INSTRUMENTATIONS: Instrumentation[] = getNodeAutoInstrumentation
4550
const PATCHED_INSTRUMENTATIONS: Instrumentation[] = getNodeAutoInstrumentations();
4651
applyInstrumentationPatches(PATCHED_INSTRUMENTATIONS);
4752

53+
const extendedAwsSdkInstrumentation: AwsInstrumentation = new AwsInstrumentation();
54+
applyInstrumentationPatches([extendedAwsSdkInstrumentation]);
55+
registerInstrumentationTesting(extendedAwsSdkInstrumentation);
56+
4857
describe('InstrumentationPatchTest', () => {
4958
it('SanityTestUnpatchedAwsSdkInstrumentation', () => {
5059
const awsSdkInstrumentation: AwsInstrumentation = extractAwsSdkInstrumentation(UNPATCHED_INSTRUMENTATIONS);
@@ -89,6 +98,9 @@ describe('InstrumentationPatchTest', () => {
8998
expect(services.get('BedrockRuntime')).toBeTruthy();
9099
// Sanity check
91100
expect(services.has('InvalidService')).toBeFalsy();
101+
102+
// Check that the original AWS SDK Instrumentation is replaced with the extended version
103+
expect(awsSdkInstrumentation).toBeInstanceOf(AwsSdkInstrumentationExtended);
92104
});
93105

94106
it('S3 without patching', () => {
@@ -388,6 +400,52 @@ describe('InstrumentationPatchTest', () => {
388400
expect(filteredInstrumentations.length).toEqual(1);
389401
return filteredInstrumentations[0] as AwsLambdaInstrumentation;
390402
}
403+
404+
describe('AwsSdkInstrumentationPatchTest', () => {
405+
let s3: S3;
406+
const region = 'us-east-1';
407+
408+
it('injects trace context header into request via propagator', async () => {
409+
s3 = new S3({
410+
region: region,
411+
credentials: {
412+
accessKeyId: 'abcde',
413+
secretAccessKey: 'abcde',
414+
},
415+
});
416+
417+
const dummyBucketName: string = 'dummy-bucket-name';
418+
let reqHeaders: any = {};
419+
420+
nock(`https://${dummyBucketName}.s3.${region}.amazonaws.com`)
421+
.get('/')
422+
.reply(200, function (uri: any, requestBody: any) {
423+
reqHeaders = this.req.headers;
424+
return 'null';
425+
});
426+
427+
await s3
428+
.listObjects({
429+
Bucket: dummyBucketName,
430+
})
431+
.catch((err: any) => {});
432+
433+
const testSpans: ReadableSpan[] = getTestSpans();
434+
const listObjectsSpans: ReadableSpan[] = testSpans.filter((s: ReadableSpan) => {
435+
return s.name === 'S3.ListObjects';
436+
});
437+
438+
expect(listObjectsSpans.length).toBe(1);
439+
440+
const traceId = listObjectsSpans[0].spanContext().traceId;
441+
const spanId = listObjectsSpans[0].spanContext().spanId;
442+
expect(reqHeaders['x-amzn-trace-id'] as string).toEqual(
443+
`Root=1-${traceId.substring(0, 8)}-${listObjectsSpans[0]
444+
.spanContext()
445+
.traceId.substring(8, 32)};Parent=${spanId};Sampled=1`
446+
);
447+
});
448+
});
391449
});
392450

393451
describe('customExtractor', () => {

0 commit comments

Comments
 (0)