diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 5ff880ab73..ee606f6d2c 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -453,12 +453,6 @@ class SPANDATA: Example: "123456789012" """ - CLOUD_AVAILABILITY_ZONE = "cloud.availability_zone" - """ - Cloud regions often have multiple, isolated locations known as zones to increase availability - Example: "us-east-1c" - """ - CLOUD_PLATFORM = "cloud.platform" """ The cloud platform in use. diff --git a/sentry_sdk/integrations/aws_lambda/_handler.py b/sentry_sdk/integrations/aws_lambda/_handler.py index ecc8d43f76..89ba0089b7 100644 --- a/sentry_sdk/integrations/aws_lambda/_handler.py +++ b/sentry_sdk/integrations/aws_lambda/_handler.py @@ -1,10 +1,11 @@ import functools import sys +from os import environ from typing import TYPE_CHECKING from urllib.parse import urlencode import sentry_sdk -from sentry_sdk.consts import OP +from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.data_collection import _apply_key_value_collection_filtering from sentry_sdk.integrations._wsgi_common import _filter_headers from sentry_sdk.integrations.aws_lambda._request import ( @@ -12,13 +13,11 @@ _make_request_event_processor, ) from sentry_sdk.integrations.aws_lambda.consts import ( - MILLIS_TO_SECONDS, - ORIGIN, - TIMEOUT_WARNING_BUFFER, -) -from sentry_sdk.integrations.cloud_resource_context import ( CLOUD_PLATFORM, CLOUD_PROVIDER, + IDENTIFIER, + LATEST_FUNCTION_VERSION, + ORIGIN, ) from sentry_sdk.scope import Scope from sentry_sdk.traces import SegmentNameSource @@ -30,10 +29,15 @@ ) if TYPE_CHECKING: - from typing import Any, Callable, TypeVar + from typing import Any, Callable, Mapping, TypeVar + + from sentry_sdk._types import Attributes F = TypeVar("F", bound=Callable[..., Any]) +TIMEOUT_WARNING_BUFFER = 1500 +MILLIS_TO_SECONDS = 1000.0 + def _wrap_handler(handler: "F") -> "F": from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration @@ -72,8 +76,6 @@ def sentry_handler( request_data = {} configured_time = aws_context.get_remaining_time_in_millis() - aws_region = aws_context.invoked_function_arn.split(":")[3] - with sentry_sdk.isolation_scope() as scope: timeout_thread = None with capture_internal_exceptions(): @@ -111,14 +113,18 @@ def sentry_handler( headers = {} header_attributes: "dict[str, Any]" = {} - for header, header_value in _filter_headers(headers).items(): - header_attributes[f"http.request.header.{header.lower()}"] = [ - header_value - ] + filtered_headers: "Mapping[str, Any]" = {} + with capture_internal_exceptions(): + filtered_headers = _filter_headers(headers) + for header, header_value in filtered_headers.items(): + with capture_internal_exceptions(): + header_attributes[ + f"{SPANDATA.HTTP_REQUEST_HEADER}.{header.lower()}" + ] = [header_value] additional_attributes: "dict[str, Any]" = {} if "httpMethod" in request_data: - additional_attributes["http.request.method"] = request_data[ + additional_attributes[SPANDATA.HTTP_REQUEST_METHOD] = request_data[ "httpMethod" ] @@ -131,7 +137,9 @@ def sentry_handler( behaviour=data_collection["url_query_params"], ) if filtered_qs: - additional_attributes["url.query"] = urlencode(filtered_qs) + additional_attributes[SPANDATA.URL_QUERY] = urlencode( + filtered_qs + ) if not scope._user and data_collection["user_info"]: user_info = _get_user_from_event(request_data) @@ -145,29 +153,46 @@ def sentry_handler( function_name = aws_context.function_name + attributes: "Attributes" = { + SPANDATA.SENTRY_OP: OP.FUNCTION_AWS, + SPANDATA.SENTRY_ORIGIN: ORIGIN, + SPANDATA.SENTRY_KIND: "server", + SPANDATA.SENTRY_SEGMENT_NAME_SOURCE: SegmentNameSource.COMPONENT, + SPANDATA.CLOUD_PROVIDER: CLOUD_PROVIDER, + SPANDATA.CLOUD_PLATFORM: CLOUD_PLATFORM, + SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT: batch_size, + **header_attributes, + **additional_attributes, + } + + with capture_internal_exceptions(): + invoked_function_arn = aws_context.invoked_function_arn + function_version = aws_context.function_version + arn_parts = invoked_function_arn.split(":") + resource_id = ":".join(arn_parts[:7]) + if function_version != LATEST_FUNCTION_VERSION: + resource_id = f"{resource_id}:{function_version}" + + attributes.update( + { + SPANDATA.CLOUD_REGION: environ["AWS_REGION"], + SPANDATA.CLOUD_ACCOUNT_ID: arn_parts[4], + SPANDATA.CLOUD_RESOURCE_ID: resource_id, + SPANDATA.FAAS_NAME: function_name, + SPANDATA.FAAS_VERSION: function_version, + SPANDATA.FAAS_INVOCATION_ID: aws_context.aws_request_id, + SPANDATA.AWS_LAMBDA_INVOKED_ARN: invoked_function_arn, + SPANDATA.AWS_LOG_GROUP_NAMES: [aws_context.log_group_name], + SPANDATA.AWS_LOG_STREAM_NAMES: [aws_context.log_stream_name], + } + ) + sentry_sdk.continue_trace(headers) Scope.set_custom_sampling_context(sampling_context) with sentry_sdk.start_span( name=function_name, parent_span=None, - attributes={ - "sentry.op": OP.FUNCTION_AWS, - "sentry.origin": ORIGIN, - "sentry.segment.name.source": SegmentNameSource.COMPONENT, - "cloud.region": aws_region, - "cloud.resource_id": aws_context.invoked_function_arn, - "cloud.platform": CLOUD_PLATFORM.AWS_LAMBDA, - "cloud.provider": CLOUD_PROVIDER.AWS, - "faas.name": function_name, - "faas.invocation_id": aws_context.aws_request_id, - "faas.version": aws_context.function_version, - "aws.lambda.invoked_arn": aws_context.invoked_function_arn, - "aws.log.group.names": [aws_context.log_group_name], - "aws.log.stream.names": [aws_context.log_stream_name], - "messaging.batch.message_count": batch_size, - **header_attributes, - **additional_attributes, - }, + attributes=attributes, ): try: return handler(aws_event, aws_context, *args, **kwargs) @@ -175,7 +200,7 @@ def sentry_handler( exc_info = sys.exc_info() sentry_event, hint = event_from_exception( exc_info, - mechanism={"type": "aws_lambda", "handled": False}, + mechanism={"type": IDENTIFIER, "handled": False}, ) sentry_sdk.capture_event(sentry_event, hint=hint) reraise(*exc_info) diff --git a/sentry_sdk/integrations/aws_lambda/_request.py b/sentry_sdk/integrations/aws_lambda/_request.py index 320348b452..9b2e73d9f7 100644 --- a/sentry_sdk/integrations/aws_lambda/_request.py +++ b/sentry_sdk/integrations/aws_lambda/_request.py @@ -6,9 +6,7 @@ import sentry_sdk from sentry_sdk.data_collection import _apply_key_value_collection_filtering from sentry_sdk.integrations._wsgi_common import _filter_headers -from sentry_sdk.utils import ( - AnnotatedValue, -) +from sentry_sdk.utils import AnnotatedValue if TYPE_CHECKING: from typing import Any, Optional @@ -119,8 +117,8 @@ def _get_url(aws_event: "Any", aws_context: "Any") -> str: host = headers.get("Host", None) proto = headers.get("X-Forwarded-Proto", None) if proto and host and path: - return "{}://{}{}".format(proto, host, path) - return "awslambda:///{}".format(aws_context.function_name) + return f"{proto}://{host}{path}" + return f"awslambda:///{aws_context.function_name}" def _get_cloudwatch_logs_url(aws_context: "Any", start_time: "datetime") -> str: diff --git a/sentry_sdk/integrations/aws_lambda/_runtime.py b/sentry_sdk/integrations/aws_lambda/_runtime.py index 04bccb8ac8..e5af2c6d82 100644 --- a/sentry_sdk/integrations/aws_lambda/_runtime.py +++ b/sentry_sdk/integrations/aws_lambda/_runtime.py @@ -32,7 +32,7 @@ def sentry_init_error(*args: "Any", **kwargs: "Any") -> "Any": if exc_info and all(exc_info): sentry_event, hint = event_from_exception( exc_info, - mechanism={"type": "aws_lambda", "handled": False}, + mechanism={"type": IDENTIFIER, "handled": False}, ) sentry_sdk.capture_event(sentry_event, hint=hint) diff --git a/sentry_sdk/integrations/aws_lambda/consts.py b/sentry_sdk/integrations/aws_lambda/consts.py index 21d10bc026..783534f975 100644 --- a/sentry_sdk/integrations/aws_lambda/consts.py +++ b/sentry_sdk/integrations/aws_lambda/consts.py @@ -4,7 +4,3 @@ CLOUD_PROVIDER = "aws" CLOUD_PLATFORM = "aws_lambda" LATEST_FUNCTION_VERSION = "$LATEST" - -# buffer time time required to send timeout warning. -TIMEOUT_WARNING_BUFFER = 1500 -MILLIS_TO_SECONDS = 1000.0 diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreamingDataCollection/index.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreamingDataCollection/index.py index b24dec6e95..73ac2a5cc2 100644 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreamingDataCollection/index.py +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/BasicOkSpanStreamingDataCollection/index.py @@ -3,6 +3,11 @@ import sentry_sdk from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration +# the alias is only in the name to get a qualified invoked ARN. +os.environ["AWS_LAMBDA_FUNCTION_NAME"] = os.environ["AWS_LAMBDA_FUNCTION_NAME"].split( + ":" +)[0] + sentry_sdk.init( dsn=os.environ.get("SENTRY_DSN"), traces_sample_rate=1.0, diff --git a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorSpanStreaming/index.py b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorSpanStreaming/index.py index 9503f04966..f2a63f2749 100644 --- a/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorSpanStreaming/index.py +++ b/tests/integrations/aws_lambda/lambda_functions_with_embedded_sdk/RaiseErrorSpanStreaming/index.py @@ -3,6 +3,11 @@ import sentry_sdk from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration +# the alias is only in the name to get a qualified invoked ARN +os.environ["AWS_LAMBDA_FUNCTION_NAME"] = os.environ["AWS_LAMBDA_FUNCTION_NAME"].split( + ":" +)[0] + sentry_sdk.init( dsn=os.environ.get("SENTRY_DSN"), traces_sample_rate=1.0, diff --git a/tests/integrations/aws_lambda/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index dea8cd965d..2d7712b2d2 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -730,12 +730,19 @@ def test_no_error(lambda_client, test_environment): assert _get_span_attr(attrs, SPANDATA.SENTRY_OP) == OP.FUNCTION_AWS assert _get_span_attr(attrs, SPANDATA.SENTRY_ORIGIN) == ORIGIN + assert _get_span_attr(attrs, SPANDATA.SENTRY_KIND) == "server" assert ( _get_span_attr(attrs, SPANDATA.SENTRY_SEGMENT_NAME_SOURCE) == SegmentNameSource.COMPONENT ) assert _get_span_attr(attrs, SPANDATA.CLOUD_PROVIDER) == CLOUD_PROVIDER assert _get_span_attr(attrs, SPANDATA.CLOUD_PLATFORM) == CLOUD_PLATFORM + assert _get_span_attr(attrs, SPANDATA.CLOUD_ACCOUNT_ID) == "012345678912" + assert _get_span_attr(attrs, SPANDATA.CLOUD_REGION) == "us-east-1" + assert ( + _get_span_attr(attrs, SPANDATA.CLOUD_RESOURCE_ID) + == "arn:aws:lambda:us-east-1:012345678912:function:BasicOkSpanStreaming" + ) assert _get_span_attr(attrs, SPANDATA.FAAS_NAME) == "BasicOkSpanStreaming" assert _get_span_attr(attrs, SPANDATA.FAAS_VERSION) == LATEST_FUNCTION_VERSION assert SPANDATA.FAAS_INVOCATION_ID in attrs @@ -787,11 +794,15 @@ def test_error(lambda_client, test_environment): assert _get_span_attr(attrs, SPANDATA.CLOUD_PROVIDER) == CLOUD_PROVIDER assert _get_span_attr(attrs, SPANDATA.CLOUD_PLATFORM) == CLOUD_PLATFORM assert _get_span_attr(attrs, SPANDATA.FAAS_NAME) == "RaiseErrorSpanStreaming" - assert _get_span_attr(attrs, SPANDATA.FAAS_VERSION) == LATEST_FUNCTION_VERSION + assert ( + _get_span_attr(attrs, SPANDATA.CLOUD_RESOURCE_ID) + == "arn:aws:lambda:us-east-1:012345678912:function:RaiseErrorSpanStreaming:5" + ) + assert _get_span_attr(attrs, SPANDATA.FAAS_VERSION) == "5" assert SPANDATA.FAAS_INVOCATION_ID in attrs assert ( _get_span_attr(attrs, SPANDATA.AWS_LAMBDA_INVOKED_ARN) - == "arn:aws:lambda:us-east-1:012345678912:function:RaiseErrorSpanStreaming" + == "arn:aws:lambda:us-east-1:012345678912:function:RaiseErrorSpanStreaming:5" ) assert _get_span_attr(attrs, SPANDATA.AWS_LOG_GROUP_NAMES) == [ "aws/lambda/RaiseErrorSpanStreaming" @@ -840,7 +851,7 @@ def test_trace_continuation(lambda_client, test_environment): assert _get_span_attr(attrs, SPANDATA.CLOUD_PROVIDER) == CLOUD_PROVIDER assert _get_span_attr(attrs, SPANDATA.CLOUD_PLATFORM) == CLOUD_PLATFORM assert _get_span_attr(attrs, SPANDATA.FAAS_NAME) == "RaiseErrorSpanStreaming" - assert _get_span_attr(attrs, SPANDATA.FAAS_VERSION) == LATEST_FUNCTION_VERSION + assert _get_span_attr(attrs, SPANDATA.FAAS_VERSION) == "5" assert SPANDATA.FAAS_INVOCATION_ID in attrs @@ -883,7 +894,15 @@ def test_request_attributes(lambda_client, test_environment): ) assert _get_span_attr(attrs, SPANDATA.CLOUD_PROVIDER) == CLOUD_PROVIDER assert _get_span_attr(attrs, SPANDATA.CLOUD_PLATFORM) == CLOUD_PLATFORM - assert _get_span_attr(attrs, SPANDATA.FAAS_VERSION) == LATEST_FUNCTION_VERSION + assert ( + _get_span_attr(attrs, SPANDATA.AWS_LAMBDA_INVOKED_ARN) + == "arn:aws:lambda:us-east-1:012345678912:function:BasicOkSpanStreamingDataCollection:prod" + ) + assert ( + _get_span_attr(attrs, SPANDATA.CLOUD_RESOURCE_ID) + == "arn:aws:lambda:us-east-1:012345678912:function:BasicOkSpanStreamingDataCollection:5" + ) + assert _get_span_attr(attrs, SPANDATA.FAAS_VERSION) == "5" assert SPANDATA.FAAS_INVOCATION_ID in attrs assert _get_span_attr(attrs, SPANDATA.AWS_LOG_GROUP_NAMES) == [ "aws/lambda/BasicOkSpanStreamingDataCollection" @@ -894,6 +913,7 @@ def test_request_attributes(lambda_client, test_environment): def test_url_query_params_with_data_collection(lambda_client, test_environment): + server = test_environment["server"] payload = { "httpMethod": "GET", "queryStringParameters": { @@ -908,7 +928,7 @@ def test_url_query_params_with_data_collection(lambda_client, test_environment): FunctionName="BasicOkSpanStreamingDataCollection", Payload=json.dumps(payload), ) - span_items = test_environment["server"].span_items + span_items = server.span_items segment_spans = [s for s in span_items if s["is_segment"]] assert len(segment_spans) == 1 diff --git a/tests/integrations/aws_lambda/utils.py b/tests/integrations/aws_lambda/utils.py index 8933d856ed..386adce4aa 100644 --- a/tests/integrations/aws_lambda/utils.py +++ b/tests/integrations/aws_lambda/utils.py @@ -27,6 +27,13 @@ LAMBDA_FUNCTION_TIMEOUT = 10 SAM_PORT = 3001 +# functions invoked through a published version or an alias instead of `$LATEST`: +# function name -> (qualifier of the invoked ARN, version the qualifier resolves to) +QUALIFIED_FUNCTIONS = { + "RaiseErrorSpanStreaming": ("5", "5"), + "BasicOkSpanStreamingDataCollection": ("prod", "5"), +} + PYTHON_VERSION = f"python{sys.version_info.major}.{sys.version_info.minor}" # Match the host CPU architecture so local runs on ARM machines (e.g. macOS) @@ -145,12 +152,11 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: sdk_path = os.path.join( LAMBDA_FUNCTION_WITH_EMBEDDED_SDK_DIR, lambda_dir, "sentry_sdk" ) - if not os.path.exists(sdk_path): - # Find the Sentry SDK in the current environment - import sentry_sdk as sdk_module + # refresh the embedded SDK so repeated runs cannot use stale source files. + import sentry_sdk as sdk_module - sdk_source = os.path.dirname(sdk_module.__file__) - shutil.copytree(sdk_source, sdk_path) + sdk_source = os.path.dirname(sdk_module.__file__) + shutil.copytree(sdk_source, sdk_path, dirs_exist_ok=True) # Install the requirements of Sentry SDK into the function directory subprocess.check_call( @@ -166,6 +172,18 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: ] ) + environment = {"SENTRY_DSN": dsn} + if lambda_dir in QUALIFIED_FUNCTIONS: + qualifier, function_version = QUALIFIED_FUNCTIONS[lambda_dir] + # SAM builds the invoked ARN from `AWS_LAMBDA_FUNCTION_NAME`, + # so the qualifier is appended there; the function's `index.py` strips it again. + environment.update( + { + "AWS_LAMBDA_FUNCTION_NAME": f"{lambda_dir}:{qualifier}", + "AWS_LAMBDA_FUNCTION_VERSION": function_version, + } + ) + CfnResource( self, lambda_dir, @@ -179,9 +197,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: "Timeout": LAMBDA_FUNCTION_TIMEOUT, "Architectures": [ARCHITECTURE], "Environment": { - "Variables": { - "SENTRY_DSN": dsn, - } + "Variables": environment, }, }, )