55from sentry_sdk .consts import SPANDATA
66from sentry_sdk .integrations .boto3 ._services .base import _ServiceExtension
77from sentry_sdk .integrations .boto3 ._utils import (
8- _extract_attributes ,
98 _get_aws_arn_attributes ,
109 _get_propagation_headers ,
1110)
1211from sentry_sdk .integrations .boto3 .consts import CLOUD_PROVIDER
1312from sentry_sdk .utils import capture_internal_exceptions
1413
1514if TYPE_CHECKING :
16- from typing import Any , Dict , Optional , Sequence
15+ from typing import Any , Dict , Optional
1716
1817 from sentry_sdk ._types import Attributes
1918 from sentry_sdk .integrations .boto3 ._context import AwsCallContext
20- from sentry_sdk .integrations .boto3 ._utils import _AttributeSpec
2119 from sentry_sdk .traces import Span
2220
23- # set on both request and response of the event source mapping apis.
24- _RESOURCE_MAPPING_ATTRIBUTES : "Sequence[_AttributeSpec]" = (
25- ("UUID" , SPANDATA .AWS_LAMBDA_RESOURCE_MAPPING_ID ),
26- )
27-
2821
2922class _LambdaExtension (_ServiceExtension ):
3023 def get_request_attributes (self , ctx : "AwsCallContext" ) -> "Attributes" :
31- attributes : "Attributes" = _extract_attributes (
32- ctx .params , _RESOURCE_MAPPING_ATTRIBUTES
33- )
24+ attributes : "Attributes" = {}
25+
26+ # event source mapping apis.
27+ if "UUID" in ctx .params :
28+ attributes [SPANDATA .AWS_LAMBDA_RESOURCE_MAPPING_ID ] = ctx .params ["UUID" ]
3429 if "FunctionName" not in ctx .params :
3530 return attributes
3631
37- # a name, a partial arn (`<account>:function:<name>`) or an arn, each optionally
38- # followed by `:<version or alias>`.
32+ # a name, a partial arn (`<account>:function:<name>`) or an
33+ # arn (`arn:<partition>:lambda:<region>:<account>:function:<name>`)
34+ # each optionally followed by `:<version or alias>`.
3935 function_name = ctx .params ["FunctionName" ]
4036 name = function_name .split (":function:" )[- 1 ].split (":" , 1 )[0 ]
41- invoke = ctx .operation_name in (
37+
38+ is_invoke = ctx .operation_name in (
4239 "Invoke" ,
4340 "InvokeAsync" ,
4441 "InvokeWithResponseStream" ,
4542 )
46- if invoke :
43+ if is_invoke :
4744 attributes [SPANDATA .FAAS_INVOKED_NAME ] = name
4845 attributes [SPANDATA .FAAS_INVOKED_PROVIDER ] = CLOUD_PROVIDER
4946 if ctx .region_name is not None :
5047 attributes [SPANDATA .FAAS_INVOKED_REGION ] = ctx .region_name
5148 else :
5249 attributes [SPANDATA .AWS_LAMBDA_TARGET_FUNCTION_NAME ] = name
5350
54- # params aren't validated yet by botocore.
5551 with capture_internal_exceptions ():
5652 arn_attributes = _get_aws_arn_attributes ((function_name ,), ("lambda" ,))
5753 attributes .update (arn_attributes )
58- if invoke and arn_attributes :
54+ if is_invoke and arn_attributes :
5955 attributes [SPANDATA .FAAS_INVOKED_REGION ] = function_name .split (":" )[3 ]
6056
6157 return attributes
6258
59+ def get_response_attributes (
60+ self , ctx : "AwsCallContext" , response : "Any"
61+ ) -> "Attributes" :
62+ attributes : "Attributes" = {}
63+
64+ if "UUID" in response :
65+ attributes [SPANDATA .AWS_LAMBDA_RESOURCE_MAPPING_ID ] = response ["UUID" ]
66+
67+ # `Invoke` returns `200 OK` when the function fails; reading it would leave
68+ # the caller with an empty body.
69+ if "FunctionError" in response :
70+ attributes [SPANDATA .ERROR_TYPE ] = "lambda_function_error"
71+
72+ return attributes
73+
6374 def get_request_params (
6475 self , ctx : "AwsCallContext" , span : "Optional[Span]"
6576 ) -> "Optional[Dict[str, Any]]" :
@@ -72,6 +83,8 @@ def get_request_params(
7283 return None
7384
7485 headers = _get_propagation_headers (ctx , span )
86+
87+ # https://docs.aws.amazon.com/lambda/latest/dg/python-context.html
7588 client_context = (
7689 json .loads (base64 .b64decode (ctx .params ["ClientContext" ]))
7790 if "ClientContext" in ctx .params
@@ -86,24 +99,15 @@ def get_request_params(
8699 encoded = base64 .b64encode (
87100 json .dumps (client_context , separators = ("," , ":" )).encode ()
88101 ).decode ()
89- # it takes at most 3,583 bytes of base64.
102+
103+ # lambda rejects more than 3,583 bytes of base64; skip propagation rather
104+ # than fail the call.
90105 if len (encoded ) > 3583 :
91106 return None
107+
92108 return {** ctx .params , "ClientContext" : encoded }
93109
94110 def has_streaming_response (self , ctx : "AwsCallContext" ) -> bool :
95- # `Invoke` returns once the function finished; its `Payload` is the complete
96- # result, not a stream. ending with the call keeps the span of a caller that
97- # never reads it.
111+ # `Invoke`'s `Payload` is a `StreamingBody`, but span
112+ # shouldn't be kept open until the caller reads it.
98113 return ctx .operation_name != "Invoke"
99-
100- def get_response_attributes (
101- self , ctx : "AwsCallContext" , response : "Any"
102- ) -> "Attributes" :
103- attributes = _extract_attributes (response , _RESOURCE_MAPPING_ATTRIBUTES )
104- # `Invoke` returns `200 OK` when the function fails, with `FunctionError` set. the
105- # payload holds the error and is never read.
106- # https://docs.aws.amazon.com/lambda/latest/api/API_Invoke.html#lambda-Invoke-response-FunctionError
107- if "FunctionError" in response :
108- attributes [SPANDATA .ERROR_TYPE ] = "lambda_function_error"
109- return attributes
0 commit comments