Skip to content

Commit ec245ce

Browse files
authored
Merge e9185ec into 3af0c75
2 parents 3af0c75 + e9185ec commit ec245ce

File tree

5 files changed

+163
-30
lines changed

5 files changed

+163
-30
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
"typescript": "^4.3.2"
4040
},
4141
"dependencies": {
42-
"bignumber.js": "^9.0.1",
4342
"dc-polyfill": "^0.1.3",
4443
"hot-shots": "8.5.0",
4544
"promise-retry": "^2.0.1",

src/trace/context/extractors/sqs.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,5 +126,53 @@ describe("SQSEventTraceExtractor", () => {
126126
const traceContext = extractor.extract(payload);
127127
expect(traceContext).toBeNull();
128128
});
129+
130+
it("extracts trace context from AWSTraceHeader with valid payload", () => {
131+
mockSpanContext = {
132+
toTraceId: () => "625397077193750208",
133+
toSpanId: () => "6538302989251745223",
134+
_sampling: {
135+
priority: "1",
136+
},
137+
};
138+
const tracerWrapper = new TracerWrapper();
139+
const payload: SQSEvent = {
140+
Records: [
141+
{
142+
body: "Hello world",
143+
attributes: {
144+
ApproximateReceiveCount: "1",
145+
SentTimestamp: "1605544528092",
146+
SenderId: "AROAYYB64AB3JHSRKO6XR:sqs-trace-dev-producer",
147+
ApproximateFirstReceiveTimestamp: "1605544528094",
148+
AWSTraceHeader: "Root=1-65f2f78c-0000000008addb5405b376c0;Parent=5abcb7ed643995c7;Sampled=1",
149+
},
150+
messageAttributes: {},
151+
eventSource: "aws:sqs",
152+
eventSourceARN: "arn:aws:sqs:eu-west-1:601427279990:metal-queue",
153+
awsRegion: "eu-west-1",
154+
messageId: "foo",
155+
md5OfBody: "x",
156+
receiptHandle: "x",
157+
},
158+
],
159+
};
160+
161+
const extractor = new SQSEventTraceExtractor(tracerWrapper);
162+
163+
const traceContext = extractor.extract(payload);
164+
expect(traceContext).not.toBeNull();
165+
166+
expect(spyTracerWrapper).toHaveBeenCalledWith({
167+
"x-datadog-parent-id": "6538302989251745223",
168+
"x-datadog-sampling-priority": "1",
169+
"x-datadog-trace-id": "625397077193750208",
170+
});
171+
172+
expect(traceContext?.toTraceId()).toBe("625397077193750208");
173+
expect(traceContext?.toSpanId()).toBe("6538302989251745223");
174+
expect(traceContext?.sampleMode()).toBe("1");
175+
expect(traceContext?.source).toBe("event");
176+
});
129177
});
130178
});

src/trace/context/extractors/sqs.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,29 @@ import { EventTraceExtractor } from "../extractor";
33
import { TracerWrapper } from "../../tracer-wrapper";
44
import { logDebug } from "../../../utils";
55
import { SpanContextWrapper } from "../../span-context-wrapper";
6+
import { XrayService } from "../../xray-service";
67

78
export class SQSEventTraceExtractor implements EventTraceExtractor {
89
constructor(private tracerWrapper: TracerWrapper) {}
910

1011
extract(event: SQSEvent): SpanContextWrapper | null {
11-
const headers = event?.Records?.[0]?.messageAttributes?._datadog?.stringValue;
12-
if (headers === undefined) return null;
13-
1412
try {
15-
const traceContext = this.tracerWrapper.extract(JSON.parse(headers));
16-
if (traceContext === null) return null;
13+
let preparedHeaders;
14+
const headers = event?.Records?.[0]?.messageAttributes?._datadog?.stringValue;
15+
if (headers !== undefined) {
16+
preparedHeaders = JSON.parse(headers);
17+
} else {
18+
if (event?.Records?.[0]?.attributes?.AWSTraceHeader) {
19+
preparedHeaders = XrayService.extraceDDContextFromAWSTraceHeader(event.Records[0].attributes.AWSTraceHeader);
20+
}
21+
}
1722

23+
if (!preparedHeaders) return null;
24+
const traceContext = this.tracerWrapper.extract(preparedHeaders);
25+
if (traceContext === null) {
26+
logDebug(`Failed to extract trace context from prepared headers: ${preparedHeaders}`);
27+
return null;
28+
}
1829
logDebug(`Extracted trace context from SQS event`, { traceContext, event });
1930
return traceContext;
2031
} catch (error) {

src/trace/xray-service.spec.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import {
2+
DATADOG_SAMPLING_PRIORITY_HEADER,
3+
DATADOG_TRACE_ID_HEADER,
4+
DATADOG_PARENT_ID_HEADER,
5+
} from "./context/extractor";
16
import { SampleMode } from "./trace-context-service";
27
import { XrayService } from "./xray-service";
38

@@ -340,4 +345,46 @@ describe("XrayService", () => {
340345
expect(traceId).toBeUndefined();
341346
});
342347
});
348+
349+
describe("parseAWSTraceHeader", () => {
350+
it("parses AWS trace header correctly", () => {
351+
const awsTraceHeader = "Root=1-5e272390-8c398be037738dc042009320;Parent=94ae789b969f1cc5;Sampled=1";
352+
const xrayHeaders = XrayService.parseAWSTraceHeader(awsTraceHeader);
353+
expect(xrayHeaders).toEqual({
354+
parentId: "94ae789b969f1cc5",
355+
sampled: "1",
356+
traceId: "1-5e272390-8c398be037738dc042009320",
357+
});
358+
});
359+
it.each(["Root=1-5e272390-8c398be037738dc042009320", "Root=1-65f2f78c-0000000008addb5405b376c0;Parent;Sampled"])(
360+
"returns undefined when AWS trace header is malformatted",
361+
(awsTraceHeader) => {
362+
const xrayHeaders = XrayService.parseAWSTraceHeader(awsTraceHeader);
363+
expect(xrayHeaders).toBeUndefined();
364+
},
365+
);
366+
});
367+
describe("extraceDDContextFromAWSTraceHeader", () => {
368+
it("extracts Datadog trace context from AWS trace header", () => {
369+
const awsTraceId = "Root=1-65f2f78c-0000000008addb5405b376c0;Parent=5abcb7ed643995c7;Sampled=1";
370+
const ddTraceContext = XrayService.extraceDDContextFromAWSTraceHeader(awsTraceId);
371+
372+
expect(ddTraceContext).toEqual({
373+
[DATADOG_TRACE_ID_HEADER]: "625397077193750208",
374+
[DATADOG_PARENT_ID_HEADER]: "6538302989251745223",
375+
[DATADOG_SAMPLING_PRIORITY_HEADER]: "1",
376+
});
377+
});
378+
379+
it("returns null when AWS trace header is NOT injected by dd-trace", () => {
380+
const awsTraceId = "Root=1-5e272390-8c398be037738dc042009320;Parent=94ae789b969f1cc5;Sampled=1";
381+
const ddTraceContext = XrayService.extraceDDContextFromAWSTraceHeader(awsTraceId);
382+
expect(ddTraceContext).toBeNull();
383+
});
384+
it("returns null when AWS trace header cannot be parsed", () => {
385+
const awsTraceId = "Root=1-5e272390-8c398be037738dc042009320;;";
386+
const ddTraceContext = XrayService.extraceDDContextFromAWSTraceHeader(awsTraceId);
387+
expect(ddTraceContext).toBeNull();
388+
});
389+
});
343390
});

src/trace/xray-service.ts

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import { randomBytes } from "crypto";
22
import { logDebug } from "../utils";
33
import { SampleMode, TraceContext, TraceSource } from "./trace-context-service";
4-
import BigNumber from "bignumber.js";
54
import { Socket, createSocket } from "dgram";
65
import { SpanContextWrapper } from "./span-context-wrapper";
76
import { StepFunctionContext } from "./step-function-service";
7+
import {
8+
DATADOG_TRACE_ID_HEADER,
9+
DATADOG_PARENT_ID_HEADER,
10+
DATADOG_SAMPLING_PRIORITY_HEADER,
11+
DatadogTraceHeaders,
12+
} from "./context/extractor";
813

914
const AMZN_TRACE_ID_ENV_VAR = "_X_AMZN_TRACE_ID";
1015
const AWS_XRAY_DAEMON_ADDRESS_ENV_VAR = "AWS_XRAY_DAEMON_ADDRESS";
@@ -70,16 +75,9 @@ export class XrayService {
7075
});
7176
}
7277

73-
private parseTraceContextHeader(): XrayTraceHeader | undefined {
74-
const header = process.env[AMZN_TRACE_ID_ENV_VAR];
75-
if (header === undefined) {
76-
logDebug("Couldn't read Xray trace header from env");
77-
return;
78-
}
79-
80-
// Example: Root=1-5e272390-8c398be037738dc042009320;Parent=94ae789b969f1cc5;Sampled=1
81-
logDebug(`Reading Xray trace context from env var ${header}`);
82-
const [root, parent, _sampled] = header.split(";");
78+
// Example: Root=1-5e272390-8c398be037738dc042009320;Parent=94ae789b969f1cc5;Sampled=1
79+
public static parseAWSTraceHeader(awsTraceHeader: string): XrayTraceHeader | undefined {
80+
const [root, parent, _sampled] = awsTraceHeader.split(";");
8381
if (parent === undefined || _sampled === undefined) return;
8482

8583
const [, traceId] = root.split("=");
@@ -94,6 +92,18 @@ export class XrayService {
9492
};
9593
}
9694

95+
private parseTraceContextHeader(): XrayTraceHeader | undefined {
96+
const header = process.env[AMZN_TRACE_ID_ENV_VAR];
97+
if (header === undefined) {
98+
logDebug("Couldn't read Xray trace header from env");
99+
return;
100+
}
101+
102+
// Example: Root=1-5e272390-8c398be037738dc042009320;Parent=94ae789b969f1cc5;Sampled=1
103+
logDebug(`Reading Xray trace context from env var ${header}`);
104+
return XrayService.parseAWSTraceHeader(header);
105+
}
106+
97107
private convertToSampleMode(xraySampled: number): SampleMode {
98108
return xraySampled === 1 ? SampleMode.USER_KEEP : SampleMode.USER_REJECT;
99109
}
@@ -172,11 +182,12 @@ export class XrayService {
172182

173183
private convertToParentId(xrayParentId: string): string | undefined {
174184
if (xrayParentId.length !== 16) return;
175-
176-
const hex = new BigNumber(xrayParentId, 16);
177-
if (hex.isNaN()) return;
178-
179-
return hex.toString(10);
185+
try {
186+
return BigInt("0x" + xrayParentId).toString(10);
187+
} catch (_) {
188+
logDebug(`Faied to convert xray parent id ${xrayParentId}`);
189+
return undefined;
190+
}
180191
}
181192

182193
private convertToTraceId(xrayTraceId: string): string | undefined {
@@ -187,13 +198,30 @@ export class XrayService {
187198
if (lastPart.length !== 24) return;
188199

189200
// We want to turn the last 63 bits into a decimal number in a string representation
190-
// Unfortunately, all numbers in javascript are represented by float64 bit numbers, which
191-
// means we can't parse 64 bit integers accurately.
192-
const hex = new BigNumber(lastPart, 16);
193-
if (hex.isNaN()) return;
194-
195-
// Toggle off the 64th bit
196-
const last63Bits = hex.mod(new BigNumber("8000000000000000", 16));
197-
return last63Bits.toString(10);
201+
try {
202+
return (BigInt("0x" + lastPart) % BigInt("0x8000000000000000")).toString(10); // mod by 2^63 will leave us with the last 63 bits
203+
} catch (_) {
204+
logDebug(`Faied to convert trace id ${lastPart}`);
205+
return undefined;
206+
}
207+
}
208+
209+
public static extraceDDContextFromAWSTraceHeader(amznTraceId: string): DatadogTraceHeaders | null {
210+
const awsContext = XrayService.parseAWSTraceHeader(amznTraceId);
211+
if (!awsContext) {
212+
return null;
213+
}
214+
const traceIdParts = awsContext.traceId.split("-");
215+
if (traceIdParts && traceIdParts.length > 2 && traceIdParts[2].startsWith("00000000")) {
216+
// This AWSTraceHeader contains Datadog injected trace context
217+
return {
218+
[DATADOG_TRACE_ID_HEADER]: hexStrToDecimalStr(traceIdParts[2].substring(8)),
219+
[DATADOG_PARENT_ID_HEADER]: hexStrToDecimalStr(awsContext.parentId),
220+
[DATADOG_SAMPLING_PRIORITY_HEADER]: awsContext.sampled,
221+
};
222+
}
223+
return null;
198224
}
199225
}
226+
227+
const hexStrToDecimalStr = (hexString: string): string => BigInt("0x" + hexString).toString(10);

0 commit comments

Comments
 (0)