diff --git a/scripts/build_aws_lambda_layer.py b/scripts/build_aws_lambda_layer.py index 9929044ff3..fbdb2f4a6f 100644 --- a/scripts/build_aws_lambda_layer.py +++ b/scripts/build_aws_lambda_layer.py @@ -113,9 +113,9 @@ def build_packaged_zip(base_dir=None, out_zip_filename=None): # Just for debugging dist_path = os.path.abspath(DIST_PATH) print("Created Lambda Layer package with this information:") - print(" - Base directory for generating package: {}".format(layer_builder.base_dir)) - print(" - Package zip filename: {}".format(layer_builder.out_zip_filename)) - print(" - Copied package zip to: {}".format(dist_path)) + print(f" - Base directory for generating package: {layer_builder.base_dir}") + print(f" - Package zip filename: {layer_builder.out_zip_filename}") + print(f" - Copied package zip to: {dist_path}") if __name__ == "__main__": diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 7ab271473e..5ff880ab73 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -363,6 +363,24 @@ class SPANDATA: Example: "wzHcyEWfmOGDIE5QOhTAqFDoDWP3y8IUvpNINCwL9N4TEHbUw0/gZJ+VZTmCNCWR7fezEN3eCiQ=" """ + AWS_LAMBDA_INVOKED_ARN = "aws.lambda.invoked_arn" + """ + The full ARN of the Lambda function that was invoked. + Example: "arn:aws:lambda:us-east-1:123456789012:function:my-function" + """ + + AWS_LOG_GROUP_NAMES = "aws.log.group.names" + """ + The name(s) of the AWS log group(s) an application is writing to. + Example: ["/aws/lambda/my-function", "opentelemetry-service"] + """ + + AWS_LOG_STREAM_NAMES = "aws.log.stream.names" + """ + The name(s) of the AWS log stream(s) an application is writing to. + Example: ["logs/main/10838bed-421f-43ef-870a-f43feacbbb5b"] + """ + AWS_REQUEST_ID = "aws.request_id" """ The AWS request ID as returned in the response headers. @@ -435,6 +453,18 @@ 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. + Example: "aws_lambda" + """ + CLOUD_PROVIDER = "cloud.provider" """ Name of the cloud provider. @@ -592,6 +622,23 @@ class SPANDATA: File size in bytes. Example: 1024 """ + FAAS_INVOCATION_ID = "faas.invocation_id" + """ + The invocation ID of the current function invocation. + Example: "af9d5aa4-a685-4c5f-a22b-444f80b3cc28" + """ + + FAAS_NAME = "faas.name" + """ + The name of the serverless function. + Example: "my_function" + """ + + FAAS_VERSION = "faas.version" + """ + The version of the function that was invoked. + Example: "$LATEST" + """ GEN_AI_AGENT_NAME = "gen_ai.agent.name" """ @@ -974,6 +1021,12 @@ class SPANDATA: Example: 418 """ + MESSAGING_BATCH_MESSAGE_COUNT = "messaging.batch.message_count" + """ + The number of messages sent, received, or processed in the scope of the batching operation. + Example: 10 + """ + MESSAGING_DESTINATION_NAME = "messaging.destination.name" """ The destination name where the message is being consumed from, @@ -1317,6 +1370,12 @@ class SPANDATA: Example: ["AtexitIntegration", "StdlibIntegration"] """ + SENTRY_SEGMENT_NAME_SOURCE = "sentry.segment.name.source" + """ + The source of the segment span name. Should only be set on segment spans. + Example: "route", "component", "view", "task", "custom", "url" + """ + USER_AGENT_ORIGINAL = "user_agent.original" """ Value of the HTTP User-Agent header sent by the client. diff --git a/sentry_sdk/integrations/aws_lambda/consts.py b/sentry_sdk/integrations/aws_lambda/consts.py index 5a447ca231..21d10bc026 100644 --- a/sentry_sdk/integrations/aws_lambda/consts.py +++ b/sentry_sdk/integrations/aws_lambda/consts.py @@ -1,6 +1,10 @@ IDENTIFIER = "aws_lambda" ORIGIN = f"auto.function.{IDENTIFIER}" +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/test_aws_lambda.py b/tests/integrations/aws_lambda/test_aws_lambda.py index 190d241313..dea8cd965d 100644 --- a/tests/integrations/aws_lambda/test_aws_lambda.py +++ b/tests/integrations/aws_lambda/test_aws_lambda.py @@ -9,6 +9,16 @@ import yaml from aws_cdk import App +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.aws_lambda.consts import ( + CLOUD_PLATFORM, + CLOUD_PROVIDER, + IDENTIFIER, + LATEST_FUNCTION_VERSION, + ORIGIN, +) +from sentry_sdk.traces import SegmentNameSource + from .utils import SAM_PORT, LocalLambdaStack, SentryServerForTesting DOCKER_NETWORK_NAME = "lambda-test-network" @@ -228,7 +238,7 @@ def test_non_dict_event( assert error_event["sdk"]["name"] == "sentry.python.aws_lambda" assert error_event["exception"]["values"][0]["type"] == "RuntimeError" assert error_event["exception"]["values"][0]["value"] == "Oh!" - assert error_event["exception"]["values"][0]["mechanism"]["type"] == "aws_lambda" + assert error_event["exception"]["values"][0]["mechanism"]["type"] == IDENTIFIER if has_request_data: request_data = { @@ -289,8 +299,8 @@ def test_request_data_with_default_data_collection(lambda_client, test_environme assert len(segment_spans) == 1 attrs = segment_spans[0]["attributes"] - assert _get_span_attr(attrs, "http.request.method") == "GET" - assert _get_span_attr(attrs, "url.query") == "bonkers=true" + assert _get_span_attr(attrs, SPANDATA.HTTP_REQUEST_METHOD) == "GET" + assert _get_span_attr(attrs, SPANDATA.URL_QUERY) == "bonkers=true" USER_INFO_PAYLOAD = b""" @@ -331,7 +341,7 @@ def test_user_info_with_data_collection_user_info_on(lambda_client, test_environ assert len(segment_spans) == 1 attrs = segment_spans[0]["attributes"] - assert _get_span_attr(attrs, "user.id") == "42" + assert _get_span_attr(attrs, SPANDATA.USER_ID) == "42" def test_request_data_with_data_collection_allowlist(lambda_client, test_environment): @@ -374,19 +384,29 @@ def test_request_data_with_data_collection_allowlist(lambda_client, test_environ assert len(segment_spans) == 1 attrs = segment_spans[0]["attributes"] - assert _get_span_attr(attrs, "http.request.method") == "GET" + assert _get_span_attr(attrs, SPANDATA.HTTP_REQUEST_METHOD) == "GET" # Allowlisted, non-sensitive headers pass through. - assert _get_span_attr(attrs, "http.request.header.user-agent") == ["custom"] - assert _get_span_attr(attrs, "http.request.header.x-allow-me") == ["yes"] + assert _get_span_attr(attrs, f"{SPANDATA.HTTP_REQUEST_HEADER}.user-agent") == [ + "custom" + ] + assert _get_span_attr(attrs, f"{SPANDATA.HTTP_REQUEST_HEADER}.x-allow-me") == [ + "yes" + ] # Not allowlisted -> filtered. - assert _get_span_attr(attrs, "http.request.header.host") == ["[Filtered]"] - assert _get_span_attr(attrs, "http.request.header.x-forwarded-proto") == [ + assert _get_span_attr(attrs, f"{SPANDATA.HTTP_REQUEST_HEADER}.host") == [ "[Filtered]" ] + assert _get_span_attr( + attrs, f"{SPANDATA.HTTP_REQUEST_HEADER}.x-forwarded-proto" + ) == ["[Filtered]"] # Allowlisted but sensitive -> still filtered. - assert _get_span_attr(attrs, "http.request.header.authorization") == ["[Filtered]"] + assert _get_span_attr(attrs, f"{SPANDATA.HTTP_REQUEST_HEADER}.authorization") == [ + "[Filtered]" + ] # Not allowlisted, and cookies are always filtered. - assert _get_span_attr(attrs, "http.request.header.cookie") == ["[Filtered]"] + assert _get_span_attr(attrs, f"{SPANDATA.HTTP_REQUEST_HEADER}.cookie") == [ + "[Filtered]" + ] def test_request_data_with_data_collection_denylist(lambda_client, test_environment): @@ -429,21 +449,29 @@ def test_request_data_with_data_collection_denylist(lambda_client, test_environm assert len(segment_spans) == 1 attrs = segment_spans[0]["attributes"] - assert _get_span_attr(attrs, "http.request.method") == "GET" + assert _get_span_attr(attrs, SPANDATA.HTTP_REQUEST_METHOD) == "GET" # Not denied by any term -> pass through. - assert _get_span_attr(attrs, "http.request.header.host") == [ + assert _get_span_attr(attrs, f"{SPANDATA.HTTP_REQUEST_HEADER}.host") == [ "iwsz2c7uwi.execute-api.us-east-1.amazonaws.com" ] - assert _get_span_attr(attrs, "http.request.header.x-custom") == ["keep-me"] + assert _get_span_attr(attrs, f"{SPANDATA.HTTP_REQUEST_HEADER}.x-custom") == [ + "keep-me" + ] # Denied by custom terms. - assert _get_span_attr(attrs, "http.request.header.user-agent") == ["[Filtered]"] - assert _get_span_attr(attrs, "http.request.header.x-forwarded-proto") == [ + assert _get_span_attr(attrs, f"{SPANDATA.HTTP_REQUEST_HEADER}.user-agent") == [ "[Filtered]" ] + assert _get_span_attr( + attrs, f"{SPANDATA.HTTP_REQUEST_HEADER}.x-forwarded-proto" + ) == ["[Filtered]"] # Denied by the built-in sensitive denylist. - assert _get_span_attr(attrs, "http.request.header.authorization") == ["[Filtered]"] + assert _get_span_attr(attrs, f"{SPANDATA.HTTP_REQUEST_HEADER}.authorization") == [ + "[Filtered]" + ] # Cookies are always filtered. - assert _get_span_attr(attrs, "http.request.header.cookie") == ["[Filtered]"] + assert _get_span_attr(attrs, f"{SPANDATA.HTTP_REQUEST_HEADER}.cookie") == [ + "[Filtered]" + ] def test_request_data_with_data_collection_off(lambda_client, test_environment): @@ -485,9 +513,11 @@ def test_request_data_with_data_collection_off(lambda_client, test_environment): assert len(segment_spans) == 1 attrs = segment_spans[0]["attributes"] - assert _get_span_attr(attrs, "http.request.method") == "GET" + assert _get_span_attr(attrs, SPANDATA.HTTP_REQUEST_METHOD) == "GET" # With request headers collection turned off, no header attributes are collected. - header_attrs = [k for k in attrs if k.startswith("http.request.header.")] + header_attrs = [ + k for k in attrs if k.startswith(f"{SPANDATA.HTTP_REQUEST_HEADER}.") + ] assert header_attrs == [] @@ -535,7 +565,7 @@ def test_url_query_params_with_data_collection_allowlist( # Not allowlisted -> substituted. # Allowlisted but sensitive -> still filtered. assert ( - _get_span_attr(attrs, "url.query") + _get_span_attr(attrs, SPANDATA.URL_QUERY) == "page=2&tracking=%5BFiltered%5D&token=%5BFiltered%5D" ) @@ -578,7 +608,7 @@ def test_url_query_params_with_data_collection_off(lambda_client, test_environme attrs = segment_spans[0]["attributes"] # With url_query_params collection turned off, no query string is collected. - assert "url.query" not in attrs + assert SPANDATA.URL_QUERY not in attrs @pytest.mark.parametrize( @@ -622,8 +652,7 @@ def test_span_origin(lambda_client, test_environment): segment_spans = [s for s in span_items if s.get("is_segment")] assert len(segment_spans) == 1 assert ( - _get_span_attr(segment_spans[0]["attributes"], "sentry.origin") - == "auto.function.aws_lambda" + _get_span_attr(segment_spans[0]["attributes"], SPANDATA.SENTRY_ORIGIN) == ORIGIN ) @@ -699,28 +728,28 @@ def test_no_error(lambda_client, test_environment): attrs = segment_span["attributes"] - assert _get_span_attr(attrs, "sentry.op") == "function.aws" - assert _get_span_attr(attrs, "sentry.origin") == "auto.function.aws_lambda" - assert _get_span_attr(attrs, "sentry.segment.name.source") == "component" - assert _get_span_attr(attrs, "cloud.provider") == "aws" - assert _get_span_attr(attrs, "cloud.platform") == "aws_lambda" + 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, "cloud.resource_id") - == "arn:aws:lambda:us-east-1:012345678912:function:BasicOkSpanStreaming" - ) - assert _get_span_attr(attrs, "cloud.region") == "us-east-1" - assert _get_span_attr(attrs, "faas.name") == "BasicOkSpanStreaming" - assert _get_span_attr(attrs, "faas.version") == "$LATEST" - assert "faas.invocation_id" in attrs + _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.FAAS_NAME) == "BasicOkSpanStreaming" + assert _get_span_attr(attrs, SPANDATA.FAAS_VERSION) == LATEST_FUNCTION_VERSION + assert SPANDATA.FAAS_INVOCATION_ID in attrs assert ( - _get_span_attr(attrs, "aws.lambda.invoked_arn") + _get_span_attr(attrs, SPANDATA.AWS_LAMBDA_INVOKED_ARN) == "arn:aws:lambda:us-east-1:012345678912:function:BasicOkSpanStreaming" ) - assert _get_span_attr(attrs, "aws.log.group.names") == [ + assert _get_span_attr(attrs, SPANDATA.AWS_LOG_GROUP_NAMES) == [ "aws/lambda/BasicOkSpanStreaming" ] - assert _get_span_attr(attrs, "aws.log.stream.names") == ["$LATEST"] - assert _get_span_attr(attrs, "messaging.batch.message_count") == 1 + assert _get_span_attr(attrs, SPANDATA.AWS_LOG_STREAM_NAMES) == [ + LATEST_FUNCTION_VERSION + ] + assert _get_span_attr(attrs, SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT) == 1 def test_error(lambda_client, test_environment): @@ -737,7 +766,7 @@ def test_error(lambda_client, test_environment): (exception,) = error_event["exception"]["values"] assert exception["type"] == "Exception" assert exception["value"] == "Oh!" - assert exception["mechanism"]["type"] == "aws_lambda" + assert exception["mechanism"]["type"] == IDENTIFIER assert not exception["mechanism"]["handled"] segment_spans = [s for s in span_items if s["is_segment"]] @@ -749,35 +778,35 @@ def test_error(lambda_client, test_environment): attrs = segment_span["attributes"] - assert _get_span_attr(attrs, "sentry.op") == "function.aws" - assert _get_span_attr(attrs, "sentry.origin") == "auto.function.aws_lambda" - assert _get_span_attr(attrs, "sentry.segment.name.source") == "component" - assert _get_span_attr(attrs, "cloud.provider") == "aws" - assert _get_span_attr(attrs, "cloud.platform") == "aws_lambda" + 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, "cloud.resource_id") - == "arn:aws:lambda:us-east-1:012345678912:function:RaiseErrorSpanStreaming" - ) - assert _get_span_attr(attrs, "cloud.region") == "us-east-1" - assert _get_span_attr(attrs, "faas.name") == "RaiseErrorSpanStreaming" - assert _get_span_attr(attrs, "faas.version") == "$LATEST" - assert "faas.invocation_id" in attrs + _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.FAAS_NAME) == "RaiseErrorSpanStreaming" + assert _get_span_attr(attrs, SPANDATA.FAAS_VERSION) == LATEST_FUNCTION_VERSION + assert SPANDATA.FAAS_INVOCATION_ID in attrs assert ( - _get_span_attr(attrs, "aws.lambda.invoked_arn") + _get_span_attr(attrs, SPANDATA.AWS_LAMBDA_INVOKED_ARN) == "arn:aws:lambda:us-east-1:012345678912:function:RaiseErrorSpanStreaming" ) - assert _get_span_attr(attrs, "aws.log.group.names") == [ + assert _get_span_attr(attrs, SPANDATA.AWS_LOG_GROUP_NAMES) == [ "aws/lambda/RaiseErrorSpanStreaming" ] - assert _get_span_attr(attrs, "aws.log.stream.names") == ["$LATEST"] - assert _get_span_attr(attrs, "messaging.batch.message_count") == 1 + assert _get_span_attr(attrs, SPANDATA.AWS_LOG_STREAM_NAMES) == [ + LATEST_FUNCTION_VERSION + ] + assert _get_span_attr(attrs, SPANDATA.MESSAGING_BATCH_MESSAGE_COUNT) == 1 def test_trace_continuation(lambda_client, test_environment): trace_id = "471a43a4192642f0b136d5159a501701" parent_span_id = "6e8f22c393e68f19" parent_sampled = 1 - sentry_trace_header = "{}-{}-{}".format(trace_id, parent_span_id, parent_sampled) + sentry_trace_header = f"{trace_id}-{parent_span_id}-{parent_sampled}" payload = { "headers": { @@ -802,15 +831,17 @@ def test_trace_continuation(lambda_client, test_environment): assert segment_span["trace_id"] == trace_id assert segment_span["name"] == "RaiseErrorSpanStreaming" attrs = segment_span["attributes"] - assert _get_span_attr(attrs, "sentry.op") == "function.aws" - assert _get_span_attr(attrs, "sentry.origin") == "auto.function.aws_lambda" - assert _get_span_attr(attrs, "sentry.segment.name.source") == "component" - assert _get_span_attr(attrs, "cloud.provider") == "aws" - assert _get_span_attr(attrs, "cloud.platform") == "aws_lambda" - assert _get_span_attr(attrs, "cloud.region") == "us-east-1" - assert _get_span_attr(attrs, "faas.name") == "RaiseErrorSpanStreaming" - assert _get_span_attr(attrs, "faas.version") == "$LATEST" - assert "faas.invocation_id" in attrs + 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_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.FAAS_NAME) == "RaiseErrorSpanStreaming" + assert _get_span_attr(attrs, SPANDATA.FAAS_VERSION) == LATEST_FUNCTION_VERSION + assert SPANDATA.FAAS_INVOCATION_ID in attrs def test_request_attributes(lambda_client, test_environment): @@ -835,25 +866,31 @@ def test_request_attributes(lambda_client, test_environment): segment_span = segment_spans[0] attrs = segment_span["attributes"] - assert _get_span_attr(attrs, "http.request.method") == "POST" + assert _get_span_attr(attrs, SPANDATA.HTTP_REQUEST_METHOD) == "POST" assert ( - _get_span_attr(attrs, "url.query") + _get_span_attr(attrs, SPANDATA.URL_QUERY) == "foo=bar&a-complicated-value=a%3Db%26c%3Dd" ) - assert _get_span_attr(attrs, "http.request.header.content-type") == [ + assert _get_span_attr(attrs, f"{SPANDATA.HTTP_REQUEST_HEADER}.content-type") == [ "application/json" ] - assert _get_span_attr(attrs, "http.request.header.accept") == ["text/html"] - assert _get_span_attr(attrs, "faas.name") == "BasicOkSpanStreamingDataCollection" - assert _get_span_attr(attrs, "cloud.provider") == "aws" - assert _get_span_attr(attrs, "cloud.platform") == "aws_lambda" - assert _get_span_attr(attrs, "cloud.region") == "us-east-1" - assert _get_span_attr(attrs, "faas.version") == "$LATEST" - assert "faas.invocation_id" in attrs - assert _get_span_attr(attrs, "aws.log.group.names") == [ + assert _get_span_attr(attrs, f"{SPANDATA.HTTP_REQUEST_HEADER}.accept") == [ + "text/html" + ] + assert ( + _get_span_attr(attrs, SPANDATA.FAAS_NAME) + == "BasicOkSpanStreamingDataCollection" + ) + 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 SPANDATA.FAAS_INVOCATION_ID in attrs + assert _get_span_attr(attrs, SPANDATA.AWS_LOG_GROUP_NAMES) == [ "aws/lambda/BasicOkSpanStreamingDataCollection" ] - assert _get_span_attr(attrs, "aws.log.stream.names") == ["$LATEST"] + assert _get_span_attr(attrs, SPANDATA.AWS_LOG_STREAM_NAMES) == [ + LATEST_FUNCTION_VERSION + ] def test_url_query_params_with_data_collection(lambda_client, test_environment): @@ -881,7 +918,7 @@ def test_url_query_params_with_data_collection(lambda_client, test_environment): # "page" passes through; "tracking" is denied by a custom term and "token" # by the built-in sensitive denylist. assert ( - _get_span_attr(attrs, "url.query") + _get_span_attr(attrs, SPANDATA.URL_QUERY) == "page=2&tracking=%5BFiltered%5D&token=%5BFiltered%5D" ) @@ -897,7 +934,7 @@ def test_user_info_with_data_collection_user_info_off(lambda_client, test_enviro assert len(segment_spans) == 1 attrs = segment_spans[0]["attributes"] - assert "user.id" not in attrs + assert SPANDATA.USER_ID not in attrs @pytest.mark.parametrize( @@ -910,7 +947,7 @@ def test_error_has_existing_trace_context( trace_id = "471a43a4192642f0b136d5159a501701" parent_span_id = "6e8f22c393e68f19" parent_sampled = 1 - sentry_trace_header = "{}-{}-{}".format(trace_id, parent_span_id, parent_sampled) + sentry_trace_header = f"{trace_id}-{parent_span_id}-{parent_sampled}" # We simulate here AWS Api Gateway's behavior of passing HTTP headers # as the `headers` dict in the event passed to the Lambda function.