Skip to content

Commit 3faee2d

Browse files
authored
Merge branch 'main' into zarir/dd-service-inferred-spans
2 parents e1cfa5e + c0b98d6 commit 3faee2d

11 files changed

Lines changed: 506 additions & 92 deletions

File tree

.github/workflows/update-snapshots.yml

Lines changed: 0 additions & 76 deletions
This file was deleted.

ci/datasources/regions.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ regions:
55
- code: "us-west-2"
66
- code: "af-south-1"
77
- code: "ap-east-1"
8+
- code: "ap-east-2"
89
- code: "ap-south-1"
910
- code: "ap-south-2"
1011
- code: "ap-southeast-1"
1112
- code: "ap-southeast-2"
1213
- code: "ap-southeast-3"
1314
- code: "ap-southeast-4"
1415
- code: "ap-southeast-5"
16+
- code: "ap-southeast-6"
1517
- code: "ap-southeast-7"
1618
- code: "ap-northeast-1"
1719
- code: "ap-northeast-2"

ci/publish_pypi.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@ if [ -d "dist" ]; then
2828
fi
2929

3030
# Publish to pypi
31+
source venv/bin/activate
3132
poetry publish --build --username __token__ --password $PYPI_TOKEN

datadog_lambda/tracing.py

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

@@ -196,7 +197,7 @@ def extract_context_from_http_event_or_context(
196197
if _is_context_complete(context):
197198
return context
198199

199-
headers = event.get("headers")
200+
headers = resolve_multivalue_headers(event)
200201
context = propagator.extract(headers)
201202

202203
if not _is_context_complete(context):
@@ -658,7 +659,9 @@ def extract_dd_trace_context(
658659
context = extract_context_from_request_header_or_context(
659660
event, lambda_context, event_source
660661
)
661-
elif isinstance(event, (set, dict)) and "headers" in event:
662+
elif isinstance(event, (set, dict)) and (
663+
"headers" in event or "multiValueHeaders" in event
664+
):
662665
context = extract_context_from_http_event_or_context(
663666
event, lambda_context, event_source, decode_authorizer_context
664667
)
@@ -837,6 +840,9 @@ def create_inferred_span(
837840
elif event_source.equals(EventTypes.LAMBDA_FUNCTION_URL):
838841
logger.debug("Function URL event detected. Inferring a span")
839842
return create_inferred_span_from_lambda_function_url_event(event, context)
843+
elif event_source.event_type == EventTypes.ALB:
844+
logger.debug("ALB event detected. Inferring a span")
845+
return create_inferred_span_from_alb_event(event, context)
840846
elif event_source.equals(
841847
EventTypes.API_GATEWAY, subtype=EventSubtypes.HTTP_API
842848
):
@@ -955,6 +961,54 @@ def create_inferred_span_from_lambda_function_url_event(event, context):
955961
return span
956962

957963

964+
def create_inferred_span_from_alb_event(event, context):
965+
request_context = event.get("requestContext") or {}
966+
elb = request_context.get("elb") or {}
967+
target_group_arn = elb.get("targetGroupArn")
968+
969+
headers = resolve_multivalue_headers(event)
970+
host = headers.get("host")
971+
method = event.get("httpMethod")
972+
path = event.get("path")
973+
proto = headers.get("x-forwarded-proto", "http")
974+
975+
# ALB has no api id; key the service mapping off the load-balancer host and
976+
# fall back to it when DD_TRACE_AWS_SERVICE_REPRESENTATION_ENABLED is on.
977+
service_name = determine_service_name(service_mapping, host, "lambda_alb", host)
978+
979+
http_url = "%s://%s%s" % (proto, host, path) if host and path is not None else None
980+
if method and path is not None:
981+
resource = f"{method} {path}"
982+
else:
983+
resource = method or path
984+
985+
tags = {
986+
"operation_name": "aws.alb",
987+
"span.kind": "server",
988+
"http.method": method,
989+
"http.url": http_url,
990+
"http.useragent": headers.get("user-agent"),
991+
"endpoint": path,
992+
"resource_names": resource,
993+
"request_id": context.aws_request_id,
994+
"target_group_arn": target_group_arn,
995+
}
996+
# Drop tags we couldn't derive so the span never carries malformed values.
997+
tags = {key: value for key, value in tags.items() if value is not None}
998+
999+
tracer.set_tags(_dd_origin)
1000+
# ALB events carry no request timestamp (unlike API GW requestTimeEpoch /
1001+
# Function URL timeEpoch), so the span starts at handler time.
1002+
span = tracer.trace(
1003+
"aws.alb", service=service_name, resource=resource, span_type="http"
1004+
)
1005+
InferredSpanInfo.set_tags(tags, tag_source="self", synchronicity="sync")
1006+
if span:
1007+
span.set_tags(tags)
1008+
span.set_metric(InferredSpanInfo.METRIC, 1.0)
1009+
return span
1010+
1011+
9581012
def is_api_gateway_invocation_async(event):
9591013
hdrs = event.get("headers")
9601014
if not hdrs:

datadog_lambda/trigger.py

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

295295

296+
def resolve_multivalue_headers(event):
297+
"""
298+
Resolve request headers from single-value ``headers`` or ``multiValueHeaders``
299+
(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+
319+
def _get_header_case_insensitive(headers, name):
320+
if not isinstance(headers, dict):
321+
return None
322+
name_lower = name.lower()
323+
for key, value in headers.items():
324+
if isinstance(key, str) and key.lower() == name_lower and value:
325+
return value
326+
return None
327+
328+
296329
def extract_http_tags(event):
297330
"""
298331
Extracts HTTP facet tags from the triggering event
@@ -324,19 +357,32 @@ def extract_http_tags(event):
324357
path = apigateway_v2_http.get("path")
325358
method = apigateway_v2_http.get("method")
326359

360+
elif request_context and request_context.get("elb"):
361+
# ALB events have no requestContext.stage; derive the URL from the
362+
# forwarded host/proto headers and the top-level path.
363+
alb_headers = resolve_multivalue_headers(event)
364+
host = alb_headers.get("host")
365+
if host:
366+
proto = alb_headers.get("x-forwarded-proto", "http")
367+
http_tags["http.url"] = proto + "://" + host
368+
369+
user_agent = alb_headers.get("user-agent")
370+
if user_agent:
371+
http_tags["http.useragent"] = user_agent
372+
373+
# ALB carries no route template, so use the request path as the route.
374+
if path:
375+
http_tags["http.route"] = path
376+
327377
if path:
328378
if http_tags.get("http.url"):
329379
http_tags["http.url"] += path
330380
if method:
331381
http_tags["http.method"] = method
332382

333-
# Safely get headers
334-
headers = event.get("headers", {})
335-
if not isinstance(headers, dict):
336-
headers = {}
337-
338-
if headers and headers.get("Referer"):
339-
http_tags["http.referer"] = headers.get("Referer")
383+
referer = _get_header_case_insensitive(resolve_multivalue_headers(event), "referer")
384+
if referer:
385+
http_tags["http.referer"] = referer
340386

341387
# Try to get `routeKey` from API GW v2; otherwise try to get `resource` from API GW v1
342388
route = event.get("routeKey") or event.get("resource")
@@ -415,6 +461,8 @@ def is_step_function_event(event):
415461
The actual event must contain "Execution", "StateMachine", and "State" fields.
416462
"""
417463
event = event.get("Payload", event)
464+
if not isinstance(event, dict):
465+
return False
418466

419467
# JSONPath style
420468
if "Execution" in event and "StateMachine" in event and "State" in event:

datadog_lambda/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "8.126.0.dev0"
1+
__version__ = "8.128.0.dev0"

datadog_lambda/wrapper.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from importlib import import_module
1010
from time import time_ns
1111

12-
from ddtrace.internal._exceptions import BlockingException
1312
from datadog_lambda.extension import should_use_extension, flush_extension
1413
from datadog_lambda.cold_start import (
1514
set_cold_start,
@@ -191,8 +190,6 @@ def __call__(self, event, context, **kwargs):
191190
if self.blocking_response:
192191
return self.blocking_response
193192
self.response = self.func(event, context, **kwargs)
194-
except BlockingException:
195-
self.blocking_response = get_asm_blocked_response(self.event_source)
196193
except Exception:
197194
from datadog_lambda.metric import submit_errors_metric
198195

@@ -201,6 +198,9 @@ def __call__(self, event, context, **kwargs):
201198
if self.span:
202199
self.span.set_traceback()
203200
raise
201+
except BaseException as e:
202+
if "BlockingException" in type(e).__name__:
203+
self.blocking_response = get_asm_blocked_response(self.event_source)
204204
finally:
205205
self._after(event, context)
206206
if self.blocking_response:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "datadog_lambda"
3-
version = "8.126.0.dev0"
3+
version = "8.128.0.dev0"
44
description = "The Datadog AWS Lambda Library"
55
authors = ["Datadog, Inc. <dev@datadoghq.com>"]
66
license = "Apache-2.0"

0 commit comments

Comments
 (0)