Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions sentry_sdk/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
93 changes: 59 additions & 34 deletions sentry_sdk/integrations/aws_lambda/_handler.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
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 (
_get_user_from_event,
_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
Expand All @@ -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
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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"
]

Expand All @@ -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)
Expand All @@ -145,37 +153,54 @@ 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)
except Exception:
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)
Expand Down
8 changes: 3 additions & 5 deletions sentry_sdk/integrations/aws_lambda/_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion sentry_sdk/integrations/aws_lambda/_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
4 changes: 0 additions & 4 deletions sentry_sdk/integrations/aws_lambda/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
30 changes: 25 additions & 5 deletions tests/integrations/aws_lambda/test_aws_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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"
Expand All @@ -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": {
Expand All @@ -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
Expand Down
32 changes: 24 additions & 8 deletions tests/integrations/aws_lambda/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand All @@ -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,
Expand All @@ -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,
},
},
)
Expand Down
Loading