From 543697be24d647a590f6f357bf09c64b5a83f9ec Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Mon, 4 Apr 2022 16:09:18 -0500 Subject: [PATCH 1/4] feat: Function URLs --- datadog_lambda/tracing.py | 30 +++++++++++++++++++++++++ datadog_lambda/trigger.py | 47 ++++++++++++++++++++++++++++++++------- 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/datadog_lambda/tracing.py b/datadog_lambda/tracing.py index 68d32600..90a539b2 100644 --- a/datadog_lambda/tracing.py +++ b/datadog_lambda/tracing.py @@ -506,6 +506,9 @@ def create_inferred_span(event, context): ): logger.debug("API Gateway event detected. Inferring a span") return create_inferred_span_from_api_gateway_event(event, context) + elif event_source.equals(EventTypes.LAMBDA_FUNCTION_URL): + logger.debug("Function URL event detected. Inferring a span") + return create_inferred_span_from_lambda_function_url_event(event, context) elif event_source.equals( EventTypes.API_GATEWAY, subtype=EventSubtypes.HTTP_API ): @@ -545,6 +548,33 @@ def create_inferred_span(event, context): logger.debug("Unable to infer a span: unknown event type") return None +def create_inferred_span_from_lambda_function_url_event(event, context): + domain = event["requestContext"]["domainName"] + path = event["rawPath"] + tags = { + "operation_name": "aws.lambda.url", + "http.url": domain + path, + "endpoint": path, + "http.method": event["requestContext"]["http"]["method"], + "resource_names": domain + path, + "request_id": context.aws_request_id, + } + request_time_epoch = event["requestContext"]["timeEpoch"] + args = { + "service": "aws.lambda", + "resource": domain + path, + "span_type": "http", + } + tracer.set_tags( + {"_dd.origin": "lambda"} + ) # function urls don't count as lambda_inferred, + # because they're in the same service as the inferring lambda function + span = tracer.trace("aws.lambda.url", **args) + InferredSpanInfo.set_tags(tags, tag_source="self", synchronicity="sync") + if span: + span.set_tags(tags) + span.start = request_time_epoch / 1000 + return span def is_api_gateway_invocation_async(event): return ( diff --git a/datadog_lambda/trigger.py b/datadog_lambda/trigger.py index cd7909fe..93604970 100644 --- a/datadog_lambda/trigger.py +++ b/datadog_lambda/trigger.py @@ -35,6 +35,7 @@ class EventTypes(_stringTypedEnum): CLOUDFRONT = "cloudfront" DYNAMODB = "dynamodb" KINESIS = "kinesis" + LAMBDA_FUNCTION_URL = "lambda-function-url" S3 = "s3" SNS = "sns" SQS = "sqs" @@ -115,6 +116,10 @@ def parse_event_source(event: dict) -> _EventSource: request_context = event.get("requestContext") if request_context and request_context.get("stage"): + if "domainName" in request_context and detect_lambda_function_url_domain( + request_context.get("domainName") + ): + return _EventSource(EventTypes.LAMBDA_FUNCTION_URL) event_source = _EventSource(EventTypes.API_GATEWAY) if "httpMethod" in event: event_source.subtype = EventSubtypes.API_GATEWAY @@ -159,6 +164,12 @@ def parse_event_source(event: dict) -> _EventSource: return event_source +def detect_lambda_function_url_domain(domain: str) -> bool: + # e.g. "etsn5fibjr.lambda-url.eu-south-1.amazonaws.com" + domain_parts = domain.split(".") + if len(domain_parts) < 2: + return False + return domain_parts[1] == "lambda-url" def parse_event_source_arn(source: _EventSource, event: dict, context: Any) -> str: """ @@ -186,6 +197,18 @@ def parse_event_source_arn(source: _EventSource, event: dict, context: Any) -> s aws_arn, account_id, distribution_id ) + # e.g. arn:aws:lambda:::url:: + if source.equals(EventTypes.LAMBDA_FUNCTION_URL): + function_name = "" + if len(split_function_arn) >= 7: + function_name = split_function_arn[6] + function_arn = f"arn:aws:lambda:{region}:{account_id}:url:{function_name}" + function_qualifier = "" + if len(split_function_arn) >= 8: + function_qualifier = split_function_arn[7] + function_arn = function_arn + f":{function_qualifier}" + return function_arn + # e.g. arn:aws:apigateway:us-east-1::/restapis/xyz123/stages/default if source.event_type == EventTypes.API_GATEWAY: request_context = event.get("requestContext") @@ -275,7 +298,7 @@ def extract_trigger_tags(event: dict, context: Any) -> dict: if event_source_arn: trigger_tags["function_trigger.event_source_arn"] = event_source_arn - if event_source.event_type in [EventTypes.API_GATEWAY, EventTypes.ALB]: + if event_source.event_type in [EventTypes.API_GATEWAY, EventTypes.ALB, EventTypes.LAMBDA_FUNCTION_URL]: trigger_tags.update(extract_http_tags(event)) return trigger_tags @@ -283,15 +306,23 @@ def extract_trigger_tags(event: dict, context: Any) -> dict: def extract_http_status_code_tag(trigger_tags, response): """ - If the Lambda was triggered by API Gateway or ALB add the returned status code + If the Lambda was triggered by API Gateway, Lambda Function URL, or ALB add the returned status code as a tag to the function execution span. """ - is_http_trigger = trigger_tags and ( - trigger_tags.get("function_trigger.event_source") == "api-gateway" - or trigger_tags.get("function_trigger.event_source") - == "application-load-balancer" - ) - if not is_http_trigger: + if trigger_tags is None: + return + str_event_source = trigger_tags.get("function_trigger.event_source") + # it would be cleaner if each event type was a constant object that + # knew some properties about itself like this. + str_http_triggers = [ + et.value + for et in [ + EventTypes.API_GATEWAY, + EventTypes.LAMBDA_FUNCTION_URL, + EventTypes.ALB, + ] + ] + if str_event_source not in str_http_triggers: return status_code = "200" From 96517569dfcc3ee250b41bfe5ad97241e8f0cc90 Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Mon, 4 Apr 2022 16:53:44 -0500 Subject: [PATCH 2/4] feat: Use new standard for tagging service and resource names --- datadog_lambda/tracing.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/datadog_lambda/tracing.py b/datadog_lambda/tracing.py index 90a539b2..86935b12 100644 --- a/datadog_lambda/tracing.py +++ b/datadog_lambda/tracing.py @@ -549,8 +549,11 @@ def create_inferred_span(event, context): return None def create_inferred_span_from_lambda_function_url_event(event, context): - domain = event["requestContext"]["domainName"] - path = event["rawPath"] + request_context = event["requestContext"] + domain = request_context["domainName"] + method = event["httpMethod"] + path = event["path"] + resource = "{0} {1}".format(method, path) tags = { "operation_name": "aws.lambda.url", "http.url": domain + path, @@ -561,8 +564,8 @@ def create_inferred_span_from_lambda_function_url_event(event, context): } request_time_epoch = event["requestContext"]["timeEpoch"] args = { - "service": "aws.lambda", - "resource": domain + path, + "service": domain, + "resource": resource, "span_type": "http", } tracer.set_tags( From 58711c16766544c2d0f3c0e8071d23147fc2f693 Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Mon, 4 Apr 2022 17:45:20 -0500 Subject: [PATCH 3/4] feat: Use the right path --- datadog_lambda/tracing.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/datadog_lambda/tracing.py b/datadog_lambda/tracing.py index 86935b12..a9c5dd4f 100644 --- a/datadog_lambda/tracing.py +++ b/datadog_lambda/tracing.py @@ -551,18 +551,18 @@ def create_inferred_span(event, context): def create_inferred_span_from_lambda_function_url_event(event, context): request_context = event["requestContext"] domain = request_context["domainName"] - method = event["httpMethod"] - path = event["path"] + method = request_context["http"]["method"] + path = request_context["http"]["path"] resource = "{0} {1}".format(method, path) tags = { "operation_name": "aws.lambda.url", "http.url": domain + path, "endpoint": path, - "http.method": event["requestContext"]["http"]["method"], + "http.method": method, "resource_names": domain + path, "request_id": context.aws_request_id, } - request_time_epoch = event["requestContext"]["timeEpoch"] + request_time_epoch = request_context["timeEpoch"] args = { "service": domain, "resource": resource, From 30a0c87cb223969b6bc96f7de89671a8b63bf698 Mon Sep 17 00:00:00 2001 From: AJ Stuyvenberg Date: Mon, 4 Apr 2022 17:50:54 -0500 Subject: [PATCH 4/4] feat: black --- datadog_lambda/tracing.py | 2 ++ datadog_lambda/trigger.py | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/datadog_lambda/tracing.py b/datadog_lambda/tracing.py index a9c5dd4f..487321a6 100644 --- a/datadog_lambda/tracing.py +++ b/datadog_lambda/tracing.py @@ -548,6 +548,7 @@ def create_inferred_span(event, context): logger.debug("Unable to infer a span: unknown event type") return None + def create_inferred_span_from_lambda_function_url_event(event, context): request_context = event["requestContext"] domain = request_context["domainName"] @@ -579,6 +580,7 @@ def create_inferred_span_from_lambda_function_url_event(event, context): span.start = request_time_epoch / 1000 return span + def is_api_gateway_invocation_async(event): return ( "headers" in event diff --git a/datadog_lambda/trigger.py b/datadog_lambda/trigger.py index 93604970..a2d2792e 100644 --- a/datadog_lambda/trigger.py +++ b/datadog_lambda/trigger.py @@ -164,6 +164,7 @@ def parse_event_source(event: dict) -> _EventSource: return event_source + def detect_lambda_function_url_domain(domain: str) -> bool: # e.g. "etsn5fibjr.lambda-url.eu-south-1.amazonaws.com" domain_parts = domain.split(".") @@ -171,6 +172,7 @@ def detect_lambda_function_url_domain(domain: str) -> bool: return False return domain_parts[1] == "lambda-url" + def parse_event_source_arn(source: _EventSource, event: dict, context: Any) -> str: """ Parses the trigger event for an available ARN. If an ARN field is not provided @@ -298,7 +300,11 @@ def extract_trigger_tags(event: dict, context: Any) -> dict: if event_source_arn: trigger_tags["function_trigger.event_source_arn"] = event_source_arn - if event_source.event_type in [EventTypes.API_GATEWAY, EventTypes.ALB, EventTypes.LAMBDA_FUNCTION_URL]: + if event_source.event_type in [ + EventTypes.API_GATEWAY, + EventTypes.ALB, + EventTypes.LAMBDA_FUNCTION_URL, + ]: trigger_tags.update(extract_http_tags(event)) return trigger_tags