Skip to content

Commit 4eb3e2d

Browse files
authored
fix(tracer): forward X-Amzn-Trace-Id header when instrumenting fetch (#3470)
1 parent e154e58 commit 4eb3e2d

File tree

5 files changed

+63
-13
lines changed

5 files changed

+63
-13
lines changed

packages/tracer/src/Tracer.ts

+5-10
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ import type {
1919
import type { Handler } from 'aws-lambda';
2020
import type { Segment, Subsegment } from 'aws-xray-sdk-core';
2121
import xraySdk from 'aws-xray-sdk-core';
22-
import { EnvironmentVariablesService } from './config/EnvironmentVariablesService.js';
22+
import {
23+
type EnvironmentVariablesService,
24+
environmentVariablesService,
25+
} from './config/EnvironmentVariablesService.js';
2326
import { ProviderService } from './provider/ProviderService.js';
2427
import type { ConfigServiceInterface } from './types/ConfigServiceInterface.js';
2528
import type { ProviderServiceInterface } from './types/ProviderService.js';
@@ -861,14 +864,6 @@ class Tracer extends Utility implements TracerInterface {
861864
: undefined;
862865
}
863866

864-
/**
865-
* Set and initialize `envVarsService`.
866-
* Used internally during initialization.
867-
*/
868-
private setEnvVarsService(): void {
869-
this.envVarsService = new EnvironmentVariablesService();
870-
}
871-
872867
/**
873868
* Method that reconciles the configuration passed with the environment variables.
874869
* Used internally during initialization.
@@ -879,7 +874,7 @@ class Tracer extends Utility implements TracerInterface {
879874
const { enabled, serviceName, captureHTTPsRequests, customConfigService } =
880875
options;
881876

882-
this.setEnvVarsService();
877+
this.envVarsService = environmentVariablesService;
883878
this.setCustomConfigService(customConfigService);
884879
this.setTracingEnabled(enabled);
885880
this.setCaptureResponse();

packages/tracer/src/config/EnvironmentVariablesService.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,6 @@ class EnvironmentVariablesService
6969
}
7070
}
7171

72-
export { EnvironmentVariablesService };
72+
const environmentVariablesService = new EnvironmentVariablesService();
73+
74+
export { EnvironmentVariablesService, environmentVariablesService };

packages/tracer/src/provider/ProviderService.ts

+8
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import http from 'node:http';
2626
import https from 'node:https';
2727
import { addUserAgentMiddleware } from '@aws-lambda-powertools/commons';
2828
import type { DiagnosticsChannel } from 'undici-types';
29+
import { environmentVariablesService } from '../config/EnvironmentVariablesService.js';
2930
import {
3031
findHeaderAndDecode,
3132
getRequestURL,
@@ -127,6 +128,13 @@ class ProviderService implements ProviderServiceInterface {
127128
);
128129
subsegment.addAttribute('namespace', 'remote');
129130

131+
// addHeader is not part of the type definition but it's available https://github.com/nodejs/undici/blob/main/docs/docs/api/DiagnosticsChannel.md#undicirequestcreate
132+
// @ts-expect-error
133+
request.addHeader(
134+
'X-Amzn-Trace-Id',
135+
`Root=${environmentVariablesService.getXrayTraceId()};Parent=${subsegment.id};Sampled=${subsegment.notTraced ? '0' : '1'}`
136+
);
137+
130138
(subsegment as HttpSubsegment).http = {
131139
request: {
132140
url: `${requestURL.protocol}//${requestURL.hostname}${requestURL.pathname}`,

packages/tracer/tests/helpers/mockRequests.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { channel } from 'node:diagnostics_channel';
22
import type { URL } from 'node:url';
3+
import { vi } from 'vitest';
34

45
type MockFetchOptions = {
56
origin?: string | URL;
@@ -31,7 +32,7 @@ const mockFetch = ({
3132
statusCode,
3233
headers,
3334
throwError,
34-
}: MockFetchOptions): void => {
35+
}: MockFetchOptions) => {
3536
const requestCreateChannel = channel('undici:request:create');
3637
const responseHeadersChannel = channel('undici:request:headers');
3738
const errorChannel = channel('undici:request:error');
@@ -40,6 +41,7 @@ const mockFetch = ({
4041
origin,
4142
method: method ?? 'GET',
4243
path,
44+
addHeader: vi.fn(),
4345
};
4446

4547
requestCreateChannel.publish({
@@ -70,6 +72,8 @@ const mockFetch = ({
7072
headers: encodedHeaders,
7173
},
7274
});
75+
76+
return request;
7377
};
7478

7579
export { mockFetch };

packages/tracer/tests/unit/ProviderService.test.ts

+42-1
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ describe('Class: ProviderService', () => {
364364

365365
// Act
366366
provider.instrumentFetch();
367-
mockFetch({
367+
const mockRequest = mockFetch({
368368
origin: 'https://aws.amazon.com',
369369
path: '/blogs',
370370
headers: {
@@ -387,6 +387,12 @@ describe('Class: ProviderService', () => {
387387
});
388388
expect(subsegment.close).toHaveBeenCalledTimes(1);
389389
expect(provider.setSegment).toHaveBeenLastCalledWith(segment);
390+
expect(mockRequest.addHeader).toHaveBeenLastCalledWith(
391+
'X-Amzn-Trace-Id',
392+
expect.stringMatching(
393+
/Root=1-abcdef12-3456abcdef123456abcdef12;Parent=\S{16};Sampled=1/
394+
)
395+
);
390396
});
391397

392398
it('excludes the content_length header when invalid or not found', async () => {
@@ -616,4 +622,39 @@ describe('Class: ProviderService', () => {
616622
expect(subsegment.close).toHaveBeenCalledTimes(1);
617623
expect(provider.setSegment).toHaveBeenLastCalledWith(segment);
618624
});
625+
626+
it('forwards the correct sampling decision in the request header', async () => {
627+
// Prepare
628+
const provider: ProviderService = new ProviderService();
629+
const segment = new Subsegment('## dummySegment');
630+
const subsegment = segment.addNewSubsegment('aws.amazon.com');
631+
subsegment.notTraced = true;
632+
vi.spyOn(segment, 'addNewSubsegment').mockImplementationOnce(
633+
() => subsegment
634+
);
635+
vi.spyOn(provider, 'getSegment')
636+
.mockImplementationOnce(() => segment)
637+
.mockImplementationOnce(() => subsegment)
638+
.mockImplementationOnce(() => subsegment);
639+
vi.spyOn(subsegment, 'close');
640+
vi.spyOn(provider, 'setSegment');
641+
642+
// Act
643+
provider.instrumentFetch();
644+
const mockRequest = mockFetch({
645+
origin: 'https://aws.amazon.com',
646+
path: '/blogs',
647+
headers: {
648+
'content-length': '100',
649+
},
650+
});
651+
652+
// Assess
653+
expect(mockRequest.addHeader).toHaveBeenLastCalledWith(
654+
'X-Amzn-Trace-Id',
655+
expect.stringMatching(
656+
/Root=1-abcdef12-3456abcdef123456abcdef12;Parent=\S{16};Sampled=0/
657+
)
658+
);
659+
});
619660
});

0 commit comments

Comments
 (0)