Skip to content

Commit e6aa74c

Browse files
feat: emit inferred aws.alb span for ALB-triggered Lambdas
When a Lambda is invoked behind an Application Load Balancer, create an inferred aws.alb span with HTTP request tags and populate ALB HTTP facet tags for http.url, http.route, and http.useragent. FRSLES-851 / APMSVLS-542 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 897fb0d commit e6aa74c

3 files changed

Lines changed: 75 additions & 0 deletions

File tree

datadog_lambda/tracing.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,9 @@ def create_inferred_span(
837837
elif event_source.equals(EventTypes.LAMBDA_FUNCTION_URL):
838838
logger.debug("Function URL event detected. Inferring a span")
839839
return create_inferred_span_from_lambda_function_url_event(event, context)
840+
elif event_source.equals(EventTypes.ALB, subtype=EventSubtypes.ALB):
841+
logger.debug("ALB event detected. Inferring a span")
842+
return create_inferred_span_from_alb_event(event, context)
840843
elif event_source.equals(
841844
EventTypes.API_GATEWAY, subtype=EventSubtypes.HTTP_API
842845
):
@@ -952,6 +955,56 @@ def create_inferred_span_from_lambda_function_url_event(event, context):
952955
return span
953956

954957

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

datadog_lambda/trigger.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,25 @@ def extract_http_tags(event):
324324
path = apigateway_v2_http.get("path")
325325
method = apigateway_v2_http.get("method")
326326

327+
elif request_context and request_context.get("elb"):
328+
# ALB events have no requestContext.stage; derive the URL from the
329+
# 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 = {}
333+
host = alb_headers.get("host")
334+
if host:
335+
proto = alb_headers.get("x-forwarded-proto", "http")
336+
http_tags["http.url"] = f"{proto}://{host}"
337+
338+
user_agent = alb_headers.get("user-agent")
339+
if user_agent:
340+
http_tags["http.useragent"] = user_agent
341+
342+
# ALB carries no route template, so use the request path as the route.
343+
if path:
344+
http_tags["http.route"] = path
345+
327346
if path:
328347
if http_tags.get("http.url"):
329348
http_tags["http.url"] += path

tests/test_trigger.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,9 @@ def test_extract_trigger_tags_application_load_balancer(self):
427427
"function_trigger.event_source": "application-load-balancer",
428428
"function_trigger.event_source_arn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-xyz/123abc",
429429
"http.method": "GET",
430+
"http.url": "http://lambda-alb-123578498.us-east-2.elb.amazonaws.com/lambda",
431+
"http.route": "/lambda",
432+
"http.useragent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36",
430433
"span.kind": "server",
431434
},
432435
)

0 commit comments

Comments
 (0)