Skip to content

Commit 6986c3a

Browse files
fix: support ALB multiValueHeaders for inferred aws.alb span
Handle both ALB event subtypes via resolve_alb_request_headers, fix inbound trace propagation from multiValueHeaders, and expand unit/wrapper coverage for HTTPS URLs, peer.service, and dd_resource_key. FRSLES-851 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 66fc7da commit 6986c3a

5 files changed

Lines changed: 136 additions & 21 deletions

File tree

datadog_lambda/tracing.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
is_step_function_event,
4444
EventTypes,
4545
EventSubtypes,
46+
resolve_alb_request_headers,
4647
)
4748
from datadog_lambda.durable import extract_context_from_durable_execution
4849

@@ -197,6 +198,11 @@ def extract_context_from_http_event_or_context(
197198
return context
198199

199200
headers = event.get("headers")
201+
if not isinstance(headers, dict) or not headers:
202+
if isinstance(event.get("multiValueHeaders"), dict):
203+
headers = resolve_alb_request_headers(event)
204+
else:
205+
headers = {}
200206
context = propagator.extract(headers)
201207

202208
if not _is_context_complete(context):
@@ -658,7 +664,9 @@ def extract_dd_trace_context(
658664
context = extract_context_from_request_header_or_context(
659665
event, lambda_context, event_source
660666
)
661-
elif isinstance(event, (set, dict)) and "headers" in event:
667+
elif isinstance(event, (set, dict)) and (
668+
"headers" in event or "multiValueHeaders" in event
669+
):
662670
context = extract_context_from_http_event_or_context(
663671
event, lambda_context, event_source, decode_authorizer_context
664672
)
@@ -837,7 +845,7 @@ def create_inferred_span(
837845
elif event_source.equals(EventTypes.LAMBDA_FUNCTION_URL):
838846
logger.debug("Function URL event detected. Inferring a span")
839847
return create_inferred_span_from_lambda_function_url_event(event, context)
840-
elif event_source.equals(EventTypes.ALB, subtype=EventSubtypes.ALB):
848+
elif event_source.event_type == EventTypes.ALB:
841849
logger.debug("ALB event detected. Inferring a span")
842850
return create_inferred_span_from_alb_event(event, context)
843851
elif event_source.equals(
@@ -960,9 +968,7 @@ def create_inferred_span_from_alb_event(event, context):
960968
elb = request_context.get("elb") or {}
961969
target_group_arn = elb.get("targetGroupArn")
962970

963-
headers = event.get("headers")
964-
if not isinstance(headers, dict):
965-
headers = {}
971+
headers = resolve_alb_request_headers(event)
966972
host = headers.get("host")
967973
method = event.get("httpMethod")
968974
path = event.get("path")
@@ -972,7 +978,9 @@ def create_inferred_span_from_alb_event(event, context):
972978
# fall back to it when DD_TRACE_AWS_SERVICE_REPRESENTATION_ENABLED is on.
973979
service_name = determine_service_name(service_mapping, host, "lambda_alb", host)
974980

975-
http_url = f"{proto}://{host}{path}" if host and path is not None else None
981+
http_url = (
982+
"%s://%s%s" % (proto, host, path) if host and path is not None else None
983+
)
976984
if method and path is not None:
977985
resource = f"{method} {path}"
978986
else:
@@ -992,13 +1000,13 @@ def create_inferred_span_from_alb_event(event, context):
9921000
# Drop tags we couldn't derive so the span never carries malformed values.
9931001
tags = {key: value for key, value in tags.items() if value is not None}
9941002

995-
InferredSpanInfo.set_tags(tags, tag_source="self", synchronicity="sync")
9961003
tracer.set_tags(_dd_origin)
9971004
# ALB events carry no request timestamp (unlike API GW requestTimeEpoch /
9981005
# Function URL timeEpoch), so the span starts at handler time.
9991006
span = tracer.trace(
10001007
"aws.alb", service=service_name, resource=resource, span_type="http"
10011008
)
1009+
InferredSpanInfo.set_tags(tags, tag_source="self", synchronicity="sync")
10021010
if span:
10031011
span.set_tags(tags)
10041012
span.set_metric(InferredSpanInfo.METRIC, 1.0)

datadog_lambda/trigger.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,29 @@ def get_event_source_arn(source: _EventSource, event: dict, context: Any) -> str
293293
return event_source_arn
294294

295295

296+
def resolve_alb_request_headers(event):
297+
"""
298+
Resolve ALB request headers from single-value ``headers`` or
299+
``multiValueHeaders`` (first value per key, matching datadog-lambda-js).
300+
"""
301+
headers = event.get("headers")
302+
if isinstance(headers, dict) and headers:
303+
return headers
304+
305+
multi_value = event.get("multiValueHeaders")
306+
if not isinstance(multi_value, dict):
307+
return {}
308+
309+
resolved = {}
310+
for key, value in multi_value.items():
311+
if isinstance(value, list):
312+
if value:
313+
resolved[key] = value[0]
314+
elif isinstance(value, str):
315+
resolved[key] = value
316+
return resolved
317+
318+
296319
def extract_http_tags(event):
297320
"""
298321
Extracts HTTP facet tags from the triggering event
@@ -327,13 +350,11 @@ def extract_http_tags(event):
327350
elif request_context and request_context.get("elb"):
328351
# ALB events have no requestContext.stage; derive the URL from the
329352
# forwarded host/proto headers and the top-level path.
330-
alb_headers = event.get("headers")
331-
if not isinstance(alb_headers, dict):
332-
alb_headers = {}
353+
alb_headers = resolve_alb_request_headers(event)
333354
host = alb_headers.get("host")
334355
if host:
335356
proto = alb_headers.get("x-forwarded-proto", "http")
336-
http_tags["http.url"] = f"{proto}://{host}"
357+
http_tags["http.url"] = proto + "://" + host
337358

338359
user_agent = alb_headers.get("user-agent")
339360
if user_agent:

tests/test_tracing.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2023,7 +2023,8 @@ def test_create_inferred_span_from_alb_event(self):
20232023
self.assertEqual(span.get_tag("span.kind"), "server")
20242024
self.assertEqual(span.get_tag("http.method"), "GET")
20252025
self.assertEqual(
2026-
span.get_tag("http.url"), f"http://{self.ALB_HOST}/lambda"
2026+
span.get_tag("http.url"),
2027+
"http://%s/lambda" % self.ALB_HOST,
20272028
)
20282029
self.assertEqual(span.get_tag("http.useragent"), self.ALB_USER_AGENT)
20292030
self.assertEqual(span.get_tag("endpoint"), "/lambda")
@@ -2050,10 +2051,32 @@ def test_create_inferred_span_omits_tags_when_headers_missing(self):
20502051
self.assertNotIn("http.method", span.get_tags())
20512052
self.assertNotIn("http.useragent", span.get_tags())
20522053

2053-
def test_multivalue_headers_subtype_returns_none(self):
2054+
def test_multivalue_headers_subtype_emits_inferred_span(self):
20542055
event = self._load_event(self.ALB_MULTIVALUE)
20552056
span = create_inferred_span(event, get_mock_context())
2056-
self.assertIsNone(span)
2057+
self.assertIsNotNone(span)
2058+
self.assertEqual(span.name, "aws.alb")
2059+
self.assertEqual(span.get_tag("http.method"), "GET")
2060+
self.assertEqual(
2061+
span.get_tag("http.url"),
2062+
"http://%s/lambda" % self.ALB_HOST,
2063+
)
2064+
self.assertEqual(span.get_tag("http.useragent"), self.ALB_USER_AGENT)
2065+
2066+
@with_trace_propagation_style("datadog")
2067+
def test_inbound_datadog_context_from_multivalue_headers(self):
2068+
event = self._load_event(self.ALB_MULTIVALUE)
2069+
ctx = get_mock_context()
2070+
2071+
parent_ctx, source, _ = extract_dd_trace_context(event, ctx)
2072+
self.assertIsNotNone(parent_ctx)
2073+
self.assertEqual(parent_ctx.trace_id, 12345)
2074+
self.assertEqual(parent_ctx.span_id, 67890)
2075+
2076+
set_dd_trace_py_root(source, merge_xray_traces=False)
2077+
span = create_inferred_span(event, ctx)
2078+
self.assertEqual(span.trace_id, parent_ctx.trace_id)
2079+
self.assertEqual(span.parent_id, parent_ctx.span_id)
20572080

20582081
@with_trace_propagation_style("datadog")
20592082
def test_inbound_datadog_context_parents_inferred_span(self):
@@ -2084,6 +2107,28 @@ def test_inbound_w3c_context_extracted_from_alb_event(self):
20842107
self.assertEqual(ctx.trace_id, 0xABCD)
20852108
self.assertEqual(ctx.span_id, 0x4D)
20862109

2110+
def test_http_url_uses_https_when_forwarded_proto_is_https(self):
2111+
event = self._load_event(self.ALB_SAMPLE)
2112+
event["headers"]["x-forwarded-proto"] = "https"
2113+
2114+
span = create_inferred_span(event, get_mock_context())
2115+
2116+
self.assertEqual(
2117+
span.get_tag("http.url"),
2118+
"https://%s/lambda" % self.ALB_HOST,
2119+
)
2120+
2121+
def test_http_url_excludes_query_string(self):
2122+
event = self._load_event(self.ALB_SAMPLE)
2123+
2124+
span = create_inferred_span(event, get_mock_context())
2125+
2126+
self.assertEqual(
2127+
span.get_tag("http.url"),
2128+
"http://%s/lambda" % self.ALB_HOST,
2129+
)
2130+
self.assertNotIn("query=", span.get_tag("http.url") or "")
2131+
20872132

20882133
class _Span(object):
20892134
def __init__(self, service, start, span_type, parent_name=None, tags=None):

tests/test_trigger.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -445,9 +445,10 @@ def test_extract_trigger_tags_application_load_balancer_multivalue_headers(self)
445445
assert tags.get("function_trigger.event_source") == "application-load-balancer"
446446
assert tags.get("http.method") == "GET"
447447
assert tags.get("http.route") == "/lambda"
448-
# multi-value subtype has no single-value ``headers`` map
449-
assert "http.url" not in tags
450-
assert "http.useragent" not in tags
448+
assert tags.get("http.url") == (
449+
"http://lambda-alb-123578498.us-east-2.elb.amazonaws.com/lambda"
450+
)
451+
assert tags.get("http.useragent").startswith("Mozilla/5.0")
451452

452453
def test_extract_trigger_tags_cloudfront(self):
453454
event_sample_source = "cloudfront"

tests/test_wrapper.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,7 +1057,36 @@ def lambda_handler(event, context):
10571057
self.assertEqual(inferred.get_tag("http.status_code"), "200")
10581058
self.assertEqual(inferred.get_tag("http.route"), "/lambda")
10591059
self.assertEqual(execution.parent_id, inferred.span_id)
1060-
self.assertEqual(execution.get_tag("http.status_code"), "200")
1060+
1061+
@patch("datadog_lambda.config.Config.trace_enabled", True)
1062+
@patch("datadog_lambda.config.Config.make_inferred_span", True)
1063+
@patch("datadog_lambda.config.Config.service", "alb-demo-downstream")
1064+
def test_wrapper_sets_peer_service_and_dd_resource_key(self):
1065+
@wrapper.datadog_lambda_wrapper
1066+
def lambda_handler(event, context):
1067+
return self._alb_response(200)
1068+
1069+
lambda_handler(self.alb_event, get_mock_context())
1070+
1071+
inferred = lambda_handler.inferred_span
1072+
1073+
self.assertEqual(inferred.get_tag("peer.service"), "alb-demo-downstream")
1074+
self.assertEqual(
1075+
inferred.get_tag("dd_resource_key"),
1076+
"arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-xyz/123abc",
1077+
)
1078+
1079+
@patch("datadog_lambda.config.Config.trace_enabled", True)
1080+
@patch("datadog_lambda.config.Config.make_inferred_span", False)
1081+
def test_wrapper_skips_inferred_alb_span_when_disabled(self):
1082+
@wrapper.datadog_lambda_wrapper
1083+
def lambda_handler(event, context):
1084+
return self._alb_response(200)
1085+
1086+
lambda_handler(self.alb_event, get_mock_context())
1087+
1088+
self.assertIsNone(lambda_handler.inferred_span)
1089+
self.assertIsNotNone(lambda_handler.span)
10611090

10621091
@patch("datadog_lambda.config.Config.trace_enabled", True)
10631092
@patch("datadog_lambda.config.Config.make_inferred_span", True)
@@ -1093,7 +1122,7 @@ def lambda_handler(event, context):
10931122

10941123
@patch("datadog_lambda.config.Config.trace_enabled", True)
10951124
@patch("datadog_lambda.config.Config.make_inferred_span", True)
1096-
def test_wrapper_multivalue_alb_event_has_no_inferred_span(self):
1125+
def test_wrapper_emits_inferred_alb_span_for_multivalue_headers(self):
10971126
with open(
10981127
"tests/event_samples/application-load-balancer-multivalue-headers.json"
10991128
) as f:
@@ -1105,5 +1134,16 @@ def lambda_handler(event, context):
11051134

11061135
lambda_handler(event, get_mock_context())
11071136

1108-
self.assertIsNone(lambda_handler.inferred_span)
1109-
self.assertIsNotNone(lambda_handler.span)
1137+
inferred = lambda_handler.inferred_span
1138+
execution = lambda_handler.span
1139+
1140+
self.assertIsNotNone(inferred)
1141+
self.assertEqual(inferred.name, "aws.alb")
1142+
self.assertEqual(inferred.get_tag("http.method"), "GET")
1143+
self.assertEqual(
1144+
inferred.get_tag("http.url"),
1145+
"http://lambda-alb-123578498.us-east-2.elb.amazonaws.com/lambda",
1146+
)
1147+
self.assertEqual(inferred.get_tag("http.status_code"), "200")
1148+
self.assertEqual(inferred.get_tag("http.route"), "/lambda")
1149+
self.assertEqual(execution.parent_id, inferred.span_id)

0 commit comments

Comments
 (0)