Skip to content

Commit 191c5c7

Browse files
authored
Extract trace context from ALB multiValueHeaders (#647)
* extract first value from `event.multiValueHeaders` when `event.headers` is missing * unit test * lint * test flattening multiValueHeaders with real ALB event
1 parent e5d9ced commit 191c5c7

File tree

4 files changed

+137
-4
lines changed

4 files changed

+137
-4
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"requestContext": {
3+
"elb": {
4+
"targetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:1234567890:targetgroup/nhulston-alb-test/dcabb42f66a496e0"
5+
}
6+
},
7+
"httpMethod": "GET",
8+
"path": "/",
9+
"multiValueQueryStringParameters": {},
10+
"multiValueHeaders": {
11+
"accept": [
12+
"*/*"
13+
],
14+
"accept-encoding": [
15+
"gzip, deflate"
16+
],
17+
"accept-language": [
18+
"*"
19+
],
20+
"connection": [
21+
"keep-alive"
22+
],
23+
"host": [
24+
"nhulston-test-0987654321.us-east-1.elb.amazonaws.com"
25+
],
26+
"sec-fetch-mode": [
27+
"cors"
28+
],
29+
"traceparent": [
30+
"00-68126c4300000000125a7f065cf9a530-1c6dcc8ab8a6e99d-01"
31+
],
32+
"tracestate": [
33+
"dd=t.dm:-0;t.tid:68126c4300000000;s:1;p:1c6dcc8ab8a6e99d"
34+
],
35+
"user-agent": [
36+
"node"
37+
],
38+
"x-amzn-trace-id": [
39+
"Root=1-68126c45-01b175997ab51c4c47a2d643"
40+
],
41+
"x-datadog-parent-id": [
42+
"1234567890"
43+
],
44+
"x-datadog-sampling-priority": [
45+
"1"
46+
],
47+
"x-datadog-tags": [
48+
"_dd.p.tid=68126c4300000000,_dd.p.dm=-0"
49+
],
50+
"x-datadog-trace-id": [
51+
"0987654321"
52+
],
53+
"x-forwarded-for": [
54+
"18.204.55.6"
55+
],
56+
"x-forwarded-port": [
57+
"80"
58+
],
59+
"x-forwarded-proto": [
60+
"http"
61+
]
62+
},
63+
"body": "",
64+
"isBase64Encoded": false
65+
}

src/trace/context/extractor.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ export class TraceContextExtractor {
7575
private getTraceEventExtractor(event: any): EventTraceExtractor | undefined {
7676
if (!event || typeof event !== "object") return;
7777

78-
if (event.headers !== null && typeof event.headers === "object") {
78+
const headers = event.headers ?? event.multiValueHeaders;
79+
if (headers !== null && typeof headers === "object") {
7980
return new HTTPEventTraceExtractor(this.tracerWrapper, this.config.decodeAuthorizerContext);
8081
}
8182

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { TracerWrapper } from "../../tracer-wrapper";
22
import { HTTPEventSubType, HTTPEventTraceExtractor } from "./http";
3+
const albMultivalueHeadersEvent = require("../../../../event_samples/application-load-balancer-multivalue-headers.json");
34

45
let mockSpanContext: any = null;
56

@@ -97,6 +98,66 @@ describe("HTTPEventTraceExtractor", () => {
9798
expect(traceContext?.source).toBe("event");
9899
});
99100

101+
it("extracts trace context from payload with multiValueHeaders", () => {
102+
mockSpanContext = {
103+
toTraceId: () => "123",
104+
toSpanId: () => "456",
105+
_sampling: { priority: "1" },
106+
};
107+
const tracerWrapper = new TracerWrapper();
108+
const payload = {
109+
multiValueHeaders: {
110+
"X-Datadog-Trace-Id": ["123", "789"],
111+
"X-Datadog-Parent-Id": ["456"],
112+
"X-Datadog-Sampling-Priority": ["1"],
113+
},
114+
};
115+
const extractor = new HTTPEventTraceExtractor(tracerWrapper);
116+
const traceContext = extractor.extract(payload);
117+
118+
expect(traceContext).not.toBeNull();
119+
expect(spyTracerWrapper).toHaveBeenCalledWith({
120+
"x-datadog-trace-id": "123",
121+
"x-datadog-parent-id": "456",
122+
"x-datadog-sampling-priority": "1",
123+
});
124+
125+
expect(traceContext?.toTraceId()).toBe("123");
126+
expect(traceContext?.toSpanId()).toBe("456");
127+
expect(traceContext?.sampleMode()).toBe("1");
128+
});
129+
130+
it("flattens a real ALB multiValueHeaders payload into a lowercase, single-value map", () => {
131+
const tracerWrapper = new TracerWrapper();
132+
const extractor = new HTTPEventTraceExtractor(tracerWrapper);
133+
134+
spyTracerWrapper.mockClear();
135+
extractor.extract(albMultivalueHeadersEvent);
136+
expect(spyTracerWrapper).toHaveBeenCalled();
137+
138+
const captured = spyTracerWrapper.mock.calls[0][0] as Record<string, string>;
139+
140+
expect(captured).toEqual({
141+
accept: "*/*",
142+
"accept-encoding": "gzip, deflate",
143+
"accept-language": "*",
144+
connection: "keep-alive",
145+
host: "nhulston-test-0987654321.us-east-1.elb.amazonaws.com",
146+
"sec-fetch-mode": "cors",
147+
"user-agent": "node",
148+
traceparent: "00-68126c4300000000125a7f065cf9a530-1c6dcc8ab8a6e99d-01",
149+
tracestate: "dd=t.dm:-0;t.tid:68126c4300000000;s:1;p:1c6dcc8ab8a6e99d",
150+
"x-amzn-trace-id": "Root=1-68126c45-01b175997ab51c4c47a2d643",
151+
"x-datadog-tags": "_dd.p.tid=68126c4300000000,_dd.p.dm=-0",
152+
"x-datadog-sampling-priority": "1",
153+
"x-datadog-trace-id": "0987654321",
154+
"x-datadog-parent-id": "1234567890",
155+
"x-forwarded-for": "18.204.55.6",
156+
"x-forwarded-port": "80",
157+
"x-forwarded-proto": "http",
158+
});
159+
});
160+
100161
it("extracts trace context from payload with authorizer", () => {
101162
mockSpanContext = {
102163
toTraceId: () => "2389589954026090296",

src/trace/context/extractors/http.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,17 @@ export class HTTPEventTraceExtractor implements EventTraceExtractor {
4141
}
4242
}
4343

44-
const headers = event.headers;
44+
const headers = event.headers ?? event.multiValueHeaders;
4545
const lowerCaseHeaders: { [key: string]: string } = {};
4646

47-
for (const key of Object.keys(headers)) {
48-
lowerCaseHeaders[key.toLowerCase()] = headers[key];
47+
for (const [key, val] of Object.entries(headers)) {
48+
if (Array.isArray(val)) {
49+
// MultiValueHeaders: take the first value
50+
lowerCaseHeaders[key.toLowerCase()] = val[0] ?? "";
51+
} else if (typeof val === "string") {
52+
// Single‐value header
53+
lowerCaseHeaders[key.toLowerCase()] = val;
54+
}
4955
}
5056

5157
const traceContext = this.tracerWrapper.extract(lowerCaseHeaders);

0 commit comments

Comments
 (0)