From 62ab844bb9eb06910baea144662aa0abe18dae6f Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Tue, 29 Apr 2025 15:44:30 -0400 Subject: [PATCH 1/4] extract first value from `event.multiValueHeaders` when `event.headers` is missing --- src/trace/context/extractor.ts | 3 ++- src/trace/context/extractors/http.ts | 12 +++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/trace/context/extractor.ts b/src/trace/context/extractor.ts index b8a18253..ee3631d7 100644 --- a/src/trace/context/extractor.ts +++ b/src/trace/context/extractor.ts @@ -75,7 +75,8 @@ export class TraceContextExtractor { private getTraceEventExtractor(event: any): EventTraceExtractor | undefined { if (!event || typeof event !== "object") return; - if (event.headers !== null && typeof event.headers === "object") { + const headers = event.headers ?? event.multiValueHeaders + if (headers !== null && typeof headers === "object") { return new HTTPEventTraceExtractor(this.tracerWrapper, this.config.decodeAuthorizerContext); } diff --git a/src/trace/context/extractors/http.ts b/src/trace/context/extractors/http.ts index 96192f93..2e57a21d 100644 --- a/src/trace/context/extractors/http.ts +++ b/src/trace/context/extractors/http.ts @@ -41,11 +41,17 @@ export class HTTPEventTraceExtractor implements EventTraceExtractor { } } - const headers = event.headers; + const headers = event.headers ?? event.multiValueHeaders; const lowerCaseHeaders: { [key: string]: string } = {}; - for (const key of Object.keys(headers)) { - lowerCaseHeaders[key.toLowerCase()] = headers[key]; + for (const [key, val] of Object.entries(headers)) { + if (Array.isArray(val)) { + // MultiValueHeaders: take the first value + lowerCaseHeaders[key.toLowerCase()] = val[0] ?? ""; + } else if (typeof val === "string") { + // Single‐value header + lowerCaseHeaders[key.toLowerCase()] = val; + } } const traceContext = this.tracerWrapper.extract(lowerCaseHeaders); From f9469885df4559bc21d0d461455c3e90a2e13d52 Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Tue, 29 Apr 2025 15:45:23 -0400 Subject: [PATCH 2/4] unit test --- src/trace/context/extractors/http.spec.ts | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/trace/context/extractors/http.spec.ts b/src/trace/context/extractors/http.spec.ts index f4e926e6..7ed291b4 100644 --- a/src/trace/context/extractors/http.spec.ts +++ b/src/trace/context/extractors/http.spec.ts @@ -97,6 +97,36 @@ describe("HTTPEventTraceExtractor", () => { expect(traceContext?.source).toBe("event"); }); + it("extracts trace context from payload with multiValueHeaders", () => { + mockSpanContext = { + toTraceId: () => "123", + toSpanId: () => "456", + _sampling: { priority: "1" }, + }; + const tracerWrapper = new TracerWrapper(); + const payload = { + multiValueHeaders: { + "X-Datadog-Trace-Id": ["123", "789"], + "X-Datadog-Parent-Id": ["456"], + "X-Datadog-Sampling-Priority": ["1"], + }, + }; + const extractor = new HTTPEventTraceExtractor(tracerWrapper); + const traceContext = extractor.extract(payload); + + expect(traceContext).not.toBeNull(); + expect(spyTracerWrapper).toHaveBeenCalledWith({ + "x-datadog-trace-id": "123", + "x-datadog-parent-id": "456", + "x-datadog-sampling-priority": "1", + }); + + expect(traceContext?.toTraceId()).toBe("123"); + expect(traceContext?.toSpanId()).toBe("456"); + expect(traceContext?.sampleMode()).toBe("1"); + }); + + it("extracts trace context from payload with authorizer", () => { mockSpanContext = { toTraceId: () => "2389589954026090296", From 96300cdcb56173c838481458b66b4bd59c757302 Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Tue, 29 Apr 2025 15:48:52 -0400 Subject: [PATCH 3/4] lint --- src/trace/context/extractor.ts | 2 +- src/trace/context/extractors/http.spec.ts | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/trace/context/extractor.ts b/src/trace/context/extractor.ts index ee3631d7..fb690835 100644 --- a/src/trace/context/extractor.ts +++ b/src/trace/context/extractor.ts @@ -75,7 +75,7 @@ export class TraceContextExtractor { private getTraceEventExtractor(event: any): EventTraceExtractor | undefined { if (!event || typeof event !== "object") return; - const headers = event.headers ?? event.multiValueHeaders + const headers = event.headers ?? event.multiValueHeaders; if (headers !== null && typeof headers === "object") { return new HTTPEventTraceExtractor(this.tracerWrapper, this.config.decodeAuthorizerContext); } diff --git a/src/trace/context/extractors/http.spec.ts b/src/trace/context/extractors/http.spec.ts index 7ed291b4..384f709d 100644 --- a/src/trace/context/extractors/http.spec.ts +++ b/src/trace/context/extractors/http.spec.ts @@ -106,8 +106,8 @@ describe("HTTPEventTraceExtractor", () => { const tracerWrapper = new TracerWrapper(); const payload = { multiValueHeaders: { - "X-Datadog-Trace-Id": ["123", "789"], - "X-Datadog-Parent-Id": ["456"], + "X-Datadog-Trace-Id": ["123", "789"], + "X-Datadog-Parent-Id": ["456"], "X-Datadog-Sampling-Priority": ["1"], }, }; @@ -116,8 +116,8 @@ describe("HTTPEventTraceExtractor", () => { expect(traceContext).not.toBeNull(); expect(spyTracerWrapper).toHaveBeenCalledWith({ - "x-datadog-trace-id": "123", - "x-datadog-parent-id": "456", + "x-datadog-trace-id": "123", + "x-datadog-parent-id": "456", "x-datadog-sampling-priority": "1", }); @@ -126,7 +126,6 @@ describe("HTTPEventTraceExtractor", () => { expect(traceContext?.sampleMode()).toBe("1"); }); - it("extracts trace context from payload with authorizer", () => { mockSpanContext = { toTraceId: () => "2389589954026090296", From 032f3ca481451b17a2ae06f816a772c00b9ad07e Mon Sep 17 00:00:00 2001 From: Nicholas Hulston Date: Wed, 30 Apr 2025 15:13:11 -0400 Subject: [PATCH 4/4] test flattening multiValueHeaders with real ALB event --- ...tion-load-balancer-multivalue-headers.json | 65 +++++++++++++++++++ src/trace/context/extractors/http.spec.ts | 32 +++++++++ 2 files changed, 97 insertions(+) create mode 100644 event_samples/application-load-balancer-multivalue-headers.json diff --git a/event_samples/application-load-balancer-multivalue-headers.json b/event_samples/application-load-balancer-multivalue-headers.json new file mode 100644 index 00000000..1502bbce --- /dev/null +++ b/event_samples/application-load-balancer-multivalue-headers.json @@ -0,0 +1,65 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:1234567890:targetgroup/nhulston-alb-test/dcabb42f66a496e0" + } + }, + "httpMethod": "GET", + "path": "/", + "multiValueQueryStringParameters": {}, + "multiValueHeaders": { + "accept": [ + "*/*" + ], + "accept-encoding": [ + "gzip, deflate" + ], + "accept-language": [ + "*" + ], + "connection": [ + "keep-alive" + ], + "host": [ + "nhulston-test-0987654321.us-east-1.elb.amazonaws.com" + ], + "sec-fetch-mode": [ + "cors" + ], + "traceparent": [ + "00-68126c4300000000125a7f065cf9a530-1c6dcc8ab8a6e99d-01" + ], + "tracestate": [ + "dd=t.dm:-0;t.tid:68126c4300000000;s:1;p:1c6dcc8ab8a6e99d" + ], + "user-agent": [ + "node" + ], + "x-amzn-trace-id": [ + "Root=1-68126c45-01b175997ab51c4c47a2d643" + ], + "x-datadog-parent-id": [ + "1234567890" + ], + "x-datadog-sampling-priority": [ + "1" + ], + "x-datadog-tags": [ + "_dd.p.tid=68126c4300000000,_dd.p.dm=-0" + ], + "x-datadog-trace-id": [ + "0987654321" + ], + "x-forwarded-for": [ + "18.204.55.6" + ], + "x-forwarded-port": [ + "80" + ], + "x-forwarded-proto": [ + "http" + ] + }, + "body": "", + "isBase64Encoded": false +} diff --git a/src/trace/context/extractors/http.spec.ts b/src/trace/context/extractors/http.spec.ts index 384f709d..1cb7b3f4 100644 --- a/src/trace/context/extractors/http.spec.ts +++ b/src/trace/context/extractors/http.spec.ts @@ -1,5 +1,6 @@ import { TracerWrapper } from "../../tracer-wrapper"; import { HTTPEventSubType, HTTPEventTraceExtractor } from "./http"; +const albMultivalueHeadersEvent = require("../../../../event_samples/application-load-balancer-multivalue-headers.json"); let mockSpanContext: any = null; @@ -126,6 +127,37 @@ describe("HTTPEventTraceExtractor", () => { expect(traceContext?.sampleMode()).toBe("1"); }); + it("flattens a real ALB multiValueHeaders payload into a lowercase, single-value map", () => { + const tracerWrapper = new TracerWrapper(); + const extractor = new HTTPEventTraceExtractor(tracerWrapper); + + spyTracerWrapper.mockClear(); + extractor.extract(albMultivalueHeadersEvent); + expect(spyTracerWrapper).toHaveBeenCalled(); + + const captured = spyTracerWrapper.mock.calls[0][0] as Record; + + expect(captured).toEqual({ + accept: "*/*", + "accept-encoding": "gzip, deflate", + "accept-language": "*", + connection: "keep-alive", + host: "nhulston-test-0987654321.us-east-1.elb.amazonaws.com", + "sec-fetch-mode": "cors", + "user-agent": "node", + traceparent: "00-68126c4300000000125a7f065cf9a530-1c6dcc8ab8a6e99d-01", + tracestate: "dd=t.dm:-0;t.tid:68126c4300000000;s:1;p:1c6dcc8ab8a6e99d", + "x-amzn-trace-id": "Root=1-68126c45-01b175997ab51c4c47a2d643", + "x-datadog-tags": "_dd.p.tid=68126c4300000000,_dd.p.dm=-0", + "x-datadog-sampling-priority": "1", + "x-datadog-trace-id": "0987654321", + "x-datadog-parent-id": "1234567890", + "x-forwarded-for": "18.204.55.6", + "x-forwarded-port": "80", + "x-forwarded-proto": "http", + }); + }); + it("extracts trace context from payload with authorizer", () => { mockSpanContext = { toTraceId: () => "2389589954026090296",