From e3dabcd0c795369a4bb4d33f472b1af82bdf1138 Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Fri, 9 Oct 2026 12:16:14 +0200 Subject: [PATCH 1/3] ref(aws-lambda): move to dir + split into modules --- sentry_sdk/integrations/aws_lambda.py | 516 ------------------ .../integrations/aws_lambda/__init__.py | 16 + .../integrations/aws_lambda/_handler.py | 184 +++++++ .../integrations/aws_lambda/_request.py | 154 ++++++ .../integrations/aws_lambda/_runtime.py | 197 +++++++ sentry_sdk/integrations/aws_lambda/consts.py | 6 + sentry_sdk/integrations/chalice.py | 2 +- 7 files changed, 558 insertions(+), 517 deletions(-) delete mode 100644 sentry_sdk/integrations/aws_lambda.py create mode 100644 sentry_sdk/integrations/aws_lambda/__init__.py create mode 100644 sentry_sdk/integrations/aws_lambda/_handler.py create mode 100644 sentry_sdk/integrations/aws_lambda/_request.py create mode 100644 sentry_sdk/integrations/aws_lambda/_runtime.py create mode 100644 sentry_sdk/integrations/aws_lambda/consts.py diff --git a/sentry_sdk/integrations/aws_lambda.py b/sentry_sdk/integrations/aws_lambda.py deleted file mode 100644 index 9bd174d01d..0000000000 --- a/sentry_sdk/integrations/aws_lambda.py +++ /dev/null @@ -1,516 +0,0 @@ -import functools -import json -import re -import sys -from copy import deepcopy -from datetime import datetime, timedelta, timezone -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.data_collection import _apply_key_value_collection_filtering -from sentry_sdk.integrations import Integration -from sentry_sdk.integrations._wsgi_common import _filter_headers -from sentry_sdk.integrations.cloud_resource_context import ( - CLOUD_PLATFORM, - CLOUD_PROVIDER, -) -from sentry_sdk.scope import Scope -from sentry_sdk.traces import SegmentNameSource -from sentry_sdk.utils import ( - AnnotatedValue, - TimeoutThread, - capture_internal_exceptions, - ensure_integration_enabled, - event_from_exception, - logger, - reraise, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Optional, TypeVar - - from sentry_sdk._types import Event, EventProcessor, Hint - - F = TypeVar("F", bound=Callable[..., Any]) - -# Constants -TIMEOUT_WARNING_BUFFER = 1500 # Buffer time required to send timeout warning to Sentry -MILLIS_TO_SECONDS = 1000.0 - - -def _get_user_from_event(aws_event: "dict[str, Any]") -> "dict[str, Any]": - if not isinstance(aws_event, dict): - return {} - - identity = aws_event.get("requestContext", {}).get("identity") - if identity is None: - return {} - - user_info: "dict[str, Any]" = {} - - user_arn = identity.get("userArn") - if user_arn is not None: - user_info["id"] = user_arn - - ip = identity.get("sourceIp") - if ip is not None: - user_info["ip_address"] = ip - - return user_info - - -def _wrap_init_error(init_error: "F") -> "F": - @ensure_integration_enabled(AwsLambdaIntegration, init_error) - def sentry_init_error(*args: "Any", **kwargs: "Any") -> "Any": - with capture_internal_exceptions(): - sentry_sdk.get_isolation_scope().clear_breadcrumbs() - - exc_info = sys.exc_info() - if exc_info and all(exc_info): - sentry_event, hint = event_from_exception( - exc_info, - mechanism={"type": "aws_lambda", "handled": False}, - ) - sentry_sdk.capture_event(sentry_event, hint=hint) - - else: - # Fall back to AWS lambdas JSON representation of the error - error_info = args[1] - if isinstance(error_info, str): - error_info = json.loads(error_info) - sentry_event = _event_from_error_json(error_info) - sentry_sdk.capture_event(sentry_event) - - return init_error(*args, **kwargs) - - return sentry_init_error # type: ignore - - -def _wrap_handler(handler: "F") -> "F": - @functools.wraps(handler) - def sentry_handler( - aws_event: "Any", aws_context: "Any", *args: "Any", **kwargs: "Any" - ) -> "Any": - # Per https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html, - # `event` here is *likely* a dictionary, but also might be a number of - # other types (str, int, float, None). - # - # In some cases, it is a list (if the user is batch-invoking their - # function, for example), in which case we'll use the first entry as a - # representative from which to try pulling request data. (Presumably it - # will be the same for all events in the list, since they're all hitting - # the lambda in the same request.) - - client = sentry_sdk.get_client() - integration = client.get_integration(AwsLambdaIntegration) - - if integration is None: - return handler(aws_event, aws_context, *args, **kwargs) - - if isinstance(aws_event, list) and len(aws_event) >= 1: - request_data = aws_event[0] - batch_size = len(aws_event) - else: - request_data = aws_event - batch_size = 1 - - if not isinstance(request_data, dict): - # If we're not dealing with a dictionary, we won't be able to get - # headers, path, http method, etc in any case, so it's fine that - # this is empty - 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(): - scope.clear_breadcrumbs() - scope.add_event_processor( - _make_request_event_processor( - request_data, aws_context, configured_time - ) - ) - - # Starting the Timeout thread only if the configured time is greater than Timeout warning - # buffer and timeout_warning parameter is set True. - if ( - integration.timeout_warning - and configured_time > TIMEOUT_WARNING_BUFFER - ): - waiting_time = ( - configured_time - TIMEOUT_WARNING_BUFFER - ) / MILLIS_TO_SECONDS - - timeout_thread = TimeoutThread( - waiting_time, - configured_time / MILLIS_TO_SECONDS, - isolation_scope=scope, - current_scope=sentry_sdk.get_current_scope(), - ) - - # Starting the thread to raise timeout warning exception - timeout_thread.start() - - headers = request_data.get("headers", {}) - # Some AWS Services (ie. EventBridge) set headers as a list - # or None, so we must ensure it is a dict - if not isinstance(headers, dict): - 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 - ] - - additional_attributes: "dict[str, Any]" = {} - if "httpMethod" in request_data: - additional_attributes["http.request.method"] = request_data[ - "httpMethod" - ] - - data_collection = client.options["data_collection"] - if "queryStringParameters" in request_data: - qs = request_data["queryStringParameters"] - if qs: - filtered_qs = _apply_key_value_collection_filtering( - items=qs, - behaviour=data_collection["url_query_params"], - ) - if filtered_qs: - additional_attributes["url.query"] = urlencode(filtered_qs) - - if not scope._user and data_collection["user_info"]: - user_info = _get_user_from_event(request_data) - if user_info: - scope.set_user(user_info) - - sampling_context = { - "aws_event": aws_event, - "aws_context": aws_context, - } - - function_name = aws_context.function_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": AwsLambdaIntegration.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, - }, - ): - 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}, - ) - sentry_sdk.capture_event(sentry_event, hint=hint) - reraise(*exc_info) - finally: - if timeout_thread: - timeout_thread.stop() - - return sentry_handler # type: ignore - - -def _drain_queue() -> None: - with capture_internal_exceptions(): - client = sentry_sdk.get_client() - integration = client.get_integration(AwsLambdaIntegration) - if integration is not None: - # Flush out the event queue before AWS kills the - # process. - client.flush() - - -class AwsLambdaIntegration(Integration): - identifier = "aws_lambda" - origin = f"auto.function.{identifier}" - - def __init__(self, timeout_warning: bool = False) -> None: - self.timeout_warning = timeout_warning - - @staticmethod - def setup_once() -> None: - lambda_bootstrap = get_lambda_bootstrap() - if not lambda_bootstrap: - logger.warning( - "Not running in AWS Lambda environment, " - "AwsLambdaIntegration disabled (could not find bootstrap module)" - ) - return - - if not hasattr(lambda_bootstrap, "handle_event_request"): - logger.warning( - "Not running in AWS Lambda environment, " - "AwsLambdaIntegration disabled (could not find handle_event_request)" - ) - return - - lambda_bootstrap.LambdaRuntimeClient.post_init_error = _wrap_init_error( - lambda_bootstrap.LambdaRuntimeClient.post_init_error - ) - - old_handle_event_request = lambda_bootstrap.handle_event_request - - def sentry_handle_event_request( # type: ignore - lambda_runtime_client, request_handler, *args, **kwargs - ): - request_handler = _wrap_handler(request_handler) - return old_handle_event_request( - lambda_runtime_client, request_handler, *args, **kwargs - ) - - lambda_bootstrap.handle_event_request = sentry_handle_event_request - - # Patch the runtime client to drain the queue. This should work - # even when the SDK is initialized inside of the handler - - def _wrap_post_function(f: "F") -> "F": - def inner(*args: "Any", **kwargs: "Any") -> "Any": - _drain_queue() - return f(*args, **kwargs) - - return inner # type: ignore - - lambda_bootstrap.LambdaRuntimeClient.post_invocation_result = ( - _wrap_post_function( - lambda_bootstrap.LambdaRuntimeClient.post_invocation_result - ) - ) - lambda_bootstrap.LambdaRuntimeClient.post_invocation_error = ( - _wrap_post_function( - lambda_bootstrap.LambdaRuntimeClient.post_invocation_error - ) - ) - - -def get_lambda_bootstrap() -> "Optional[Any]": - # Python 3.7: If the bootstrap module is *already imported*, it is the - # one we actually want to use (no idea what's in __main__) - # - # Python 3.8: bootstrap is also importable, but will be the same file - # as __main__ imported under a different name: - # - # sys.modules['__main__'].__file__ == sys.modules['bootstrap'].__file__ - # sys.modules['__main__'] is not sys.modules['bootstrap'] - # - # Python 3.9: bootstrap is in __main__.awslambdaricmain - # - # On container builds using the `aws-lambda-python-runtime-interface-client` - # (awslamdaric) module, bootstrap is located in sys.modules['__main__'].bootstrap - # - # Such a setup would then make all monkeypatches useless. - if "bootstrap" in sys.modules: - return sys.modules["bootstrap"] - elif "__main__" in sys.modules: - module = sys.modules["__main__"] - # python3.9 runtime - if hasattr(module, "awslambdaricmain") and hasattr( - module.awslambdaricmain, "bootstrap" - ): - return module.awslambdaricmain.bootstrap - elif hasattr(module, "bootstrap"): - # awslambdaric python module in container builds - return module.bootstrap - - # python3.8 runtime - return module - else: - return None - - -def _make_request_event_processor( - aws_event: "Any", aws_context: "Any", configured_timeout: "Any" -) -> "EventProcessor": - start_time = datetime.now(timezone.utc) - - def event_processor( - sentry_event: "Event", hint: "Hint", start_time: "datetime" = start_time - ) -> "Optional[Event]": - remaining_time_in_milis = aws_context.get_remaining_time_in_millis() - exec_duration = configured_timeout - remaining_time_in_milis - - extra = sentry_event.setdefault("extra", {}) - extra["lambda"] = { - "function_name": aws_context.function_name, - "function_version": aws_context.function_version, - "invoked_function_arn": aws_context.invoked_function_arn, - "aws_request_id": aws_context.aws_request_id, - "execution_duration_in_millis": exec_duration, - "remaining_time_in_millis": remaining_time_in_milis, - } - - extra["cloudwatch logs"] = { - "url": _get_cloudwatch_logs_url(aws_context, start_time), - "log_group": aws_context.log_group_name, - "log_stream": aws_context.log_stream_name, - } - - request = sentry_event.get("request", {}) - - if "httpMethod" in aws_event: - request["method"] = aws_event["httpMethod"] - - request["url"] = _get_url(aws_event, aws_context) - - client_options = sentry_sdk.get_client().options - data_collection = client_options["data_collection"] - - if "queryStringParameters" in aws_event: - query_string = aws_event["queryStringParameters"] - if query_string: - filtered_qs = _apply_key_value_collection_filtering( - items=query_string, - behaviour=data_collection["url_query_params"], - ) - if filtered_qs: - request["query_string"] = filtered_qs - - if "headers" in aws_event and isinstance(aws_event["headers"], dict): - request["headers"] = _filter_headers(aws_event["headers"]) - - if data_collection["user_info"]: - extracted_user = _get_user_from_event(aws_event) - if extracted_user: - user_info = sentry_event.setdefault("user", {}) - for key, value in extracted_user.items(): - user_info.setdefault(key, value) - - if "incoming_request" in data_collection["http_bodies"]: - if "body" in aws_event: - request["data"] = aws_event.get("body", "") - elif aws_event.get("body", None): - # Unfortunately couldn't find a way to get structured body from AWS - # event. Meaning every body is unstructured to us. - request["data"] = AnnotatedValue.removed_because_raw_data() - - sentry_event["request"] = deepcopy(request) - - return sentry_event - - return event_processor - - -def _get_url(aws_event: "Any", aws_context: "Any") -> str: - path = aws_event.get("path", None) - - headers = aws_event.get("headers") - if not isinstance(headers, dict): - headers = {} - - 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) - - -def _get_cloudwatch_logs_url(aws_context: "Any", start_time: "datetime") -> str: - """ - Generates a CloudWatchLogs console URL based on the context object - - Arguments: - aws_context {Any} -- context from lambda handler - - Returns: - str -- AWS Console URL to logs. - """ - formatstring = "%Y-%m-%dT%H:%M:%SZ" - region = environ.get("AWS_REGION", "") - - url = ( - "https://console.{domain}/cloudwatch/home?region={region}" - "#logEventViewer:group={log_group};stream={log_stream}" - ";start={start_time};end={end_time}" - ).format( - domain="amazonaws.cn" if region.startswith("cn-") else "aws.amazon.com", - region=region, - log_group=aws_context.log_group_name, - log_stream=aws_context.log_stream_name, - start_time=(start_time - timedelta(seconds=1)).strftime(formatstring), - end_time=(datetime.now(timezone.utc) + timedelta(seconds=2)).strftime( - formatstring - ), - ) - - return url - - -def _parse_formatted_traceback(formatted_tb: "list[str]") -> "list[dict[str, Any]]": - frames = [] - for frame in formatted_tb: - match = re.match(r'File "(.+)", line (\d+), in (.+)', frame.strip()) - if match: - file_name, line_number, func_name = match.groups() - line_number = int(line_number) - frames.append( - { - "filename": file_name, - "function": func_name, - "lineno": line_number, - "vars": None, - "pre_context": None, - "context_line": None, - "post_context": None, - } - ) - return frames - - -def _event_from_error_json(error_json: "dict[str, Any]") -> "Event": - """ - Converts the error JSON from AWS Lambda into a Sentry error event. - This is not a full fletched event, but better than nothing. - - This is an example of where AWS creates the error JSON: - https://github.com/aws/aws-lambda-python-runtime-interface-client/blob/2.2.1/awslambdaric/bootstrap.py#L479 - """ - event: "Event" = { - "level": "error", - "exception": { - "values": [ - { - "type": error_json.get("errorType"), - "value": error_json.get("errorMessage"), - "stacktrace": { - "frames": _parse_formatted_traceback( - error_json.get("stackTrace", []) - ), - }, - "mechanism": { - "type": "aws_lambda", - "handled": False, - }, - } - ], - }, - } - - return event diff --git a/sentry_sdk/integrations/aws_lambda/__init__.py b/sentry_sdk/integrations/aws_lambda/__init__.py new file mode 100644 index 0000000000..14339a74cc --- /dev/null +++ b/sentry_sdk/integrations/aws_lambda/__init__.py @@ -0,0 +1,16 @@ +from sentry_sdk.integrations import Integration +from sentry_sdk.integrations.aws_lambda.consts import IDENTIFIER, ORIGIN + + +class AwsLambdaIntegration(Integration): + identifier = IDENTIFIER + origin = ORIGIN + + def __init__(self, timeout_warning: bool = False) -> None: + self.timeout_warning = timeout_warning + + @staticmethod + def setup_once() -> None: + from sentry_sdk.integrations.aws_lambda._runtime import _setup_once + + _setup_once() diff --git a/sentry_sdk/integrations/aws_lambda/_handler.py b/sentry_sdk/integrations/aws_lambda/_handler.py new file mode 100644 index 0000000000..c302509a79 --- /dev/null +++ b/sentry_sdk/integrations/aws_lambda/_handler.py @@ -0,0 +1,184 @@ +import functools +import sys +from typing import TYPE_CHECKING +from urllib.parse import urlencode + +import sentry_sdk +from sentry_sdk.consts import OP +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 import AwsLambdaIntegration +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, + TIMEOUT_WARNING_BUFFER, +) +from sentry_sdk.integrations.cloud_resource_context import ( + CLOUD_PLATFORM, + CLOUD_PROVIDER, +) +from sentry_sdk.scope import Scope +from sentry_sdk.traces import SegmentNameSource +from sentry_sdk.utils import ( + TimeoutThread, + capture_internal_exceptions, + event_from_exception, + reraise, +) + +if TYPE_CHECKING: + from typing import Any, Callable, TypeVar + + F = TypeVar("F", bound=Callable[..., Any]) + + +def _wrap_handler(handler: "F") -> "F": + @functools.wraps(handler) + def sentry_handler( + aws_event: "Any", aws_context: "Any", *args: "Any", **kwargs: "Any" + ) -> "Any": + # Per https://docs.aws.amazon.com/lambda/latest/dg/python-handler.html, + # `event` here is *likely* a dictionary, but also might be a number of + # other types (str, int, float, None). + # + # In some cases, it is a list (if the user is batch-invoking their + # function, for example), in which case we'll use the first entry as a + # representative from which to try pulling request data. (Presumably it + # will be the same for all events in the list, since they're all hitting + # the lambda in the same request.) + + client = sentry_sdk.get_client() + integration = client.get_integration(AwsLambdaIntegration) + + if integration is None: + return handler(aws_event, aws_context, *args, **kwargs) + + if isinstance(aws_event, list) and len(aws_event) >= 1: + request_data = aws_event[0] + batch_size = len(aws_event) + else: + request_data = aws_event + batch_size = 1 + + if not isinstance(request_data, dict): + # If we're not dealing with a dictionary, we won't be able to get + # headers, path, http method, etc in any case, so it's fine that + # this is empty + 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(): + scope.clear_breadcrumbs() + scope.add_event_processor( + _make_request_event_processor( + request_data, aws_context, configured_time + ) + ) + + # Starting the Timeout thread only if the configured time is greater than Timeout warning + # buffer and timeout_warning parameter is set True. + if ( + integration.timeout_warning + and configured_time > TIMEOUT_WARNING_BUFFER + ): + waiting_time = ( + configured_time - TIMEOUT_WARNING_BUFFER + ) / MILLIS_TO_SECONDS + + timeout_thread = TimeoutThread( + waiting_time, + configured_time / MILLIS_TO_SECONDS, + isolation_scope=scope, + current_scope=sentry_sdk.get_current_scope(), + ) + + # Starting the thread to raise timeout warning exception + timeout_thread.start() + + headers = request_data.get("headers", {}) + # Some AWS Services (ie. EventBridge) set headers as a list + # or None, so we must ensure it is a dict + if not isinstance(headers, dict): + 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 + ] + + additional_attributes: "dict[str, Any]" = {} + if "httpMethod" in request_data: + additional_attributes["http.request.method"] = request_data[ + "httpMethod" + ] + + data_collection = client.options["data_collection"] + if "queryStringParameters" in request_data: + qs = request_data["queryStringParameters"] + if qs: + filtered_qs = _apply_key_value_collection_filtering( + items=qs, + behaviour=data_collection["url_query_params"], + ) + if filtered_qs: + additional_attributes["url.query"] = urlencode(filtered_qs) + + if not scope._user and data_collection["user_info"]: + user_info = _get_user_from_event(request_data) + if user_info: + scope.set_user(user_info) + + sampling_context = { + "aws_event": aws_event, + "aws_context": aws_context, + } + + function_name = aws_context.function_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": AwsLambdaIntegration.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, + }, + ): + 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}, + ) + sentry_sdk.capture_event(sentry_event, hint=hint) + reraise(*exc_info) + finally: + if timeout_thread: + timeout_thread.stop() + + return sentry_handler # type: ignore diff --git a/sentry_sdk/integrations/aws_lambda/_request.py b/sentry_sdk/integrations/aws_lambda/_request.py new file mode 100644 index 0000000000..320348b452 --- /dev/null +++ b/sentry_sdk/integrations/aws_lambda/_request.py @@ -0,0 +1,154 @@ +from copy import deepcopy +from datetime import datetime, timedelta, timezone +from os import environ +from typing import TYPE_CHECKING + +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, +) + +if TYPE_CHECKING: + from typing import Any, Optional + + from sentry_sdk._types import Event, EventProcessor, Hint + + +def _get_user_from_event(aws_event: "dict[str, Any]") -> "dict[str, Any]": + if not isinstance(aws_event, dict): + return {} + + identity = aws_event.get("requestContext", {}).get("identity") + if identity is None: + return {} + + user_info: "dict[str, Any]" = {} + + user_arn = identity.get("userArn") + if user_arn is not None: + user_info["id"] = user_arn + + ip = identity.get("sourceIp") + if ip is not None: + user_info["ip_address"] = ip + + return user_info + + +def _make_request_event_processor( + aws_event: "Any", aws_context: "Any", configured_timeout: "Any" +) -> "EventProcessor": + start_time = datetime.now(timezone.utc) + + def event_processor( + sentry_event: "Event", hint: "Hint", start_time: "datetime" = start_time + ) -> "Optional[Event]": + remaining_time_in_milis = aws_context.get_remaining_time_in_millis() + exec_duration = configured_timeout - remaining_time_in_milis + + extra = sentry_event.setdefault("extra", {}) + extra["lambda"] = { + "function_name": aws_context.function_name, + "function_version": aws_context.function_version, + "invoked_function_arn": aws_context.invoked_function_arn, + "aws_request_id": aws_context.aws_request_id, + "execution_duration_in_millis": exec_duration, + "remaining_time_in_millis": remaining_time_in_milis, + } + + extra["cloudwatch logs"] = { + "url": _get_cloudwatch_logs_url(aws_context, start_time), + "log_group": aws_context.log_group_name, + "log_stream": aws_context.log_stream_name, + } + + request = sentry_event.get("request", {}) + + if "httpMethod" in aws_event: + request["method"] = aws_event["httpMethod"] + + request["url"] = _get_url(aws_event, aws_context) + + client_options = sentry_sdk.get_client().options + data_collection = client_options["data_collection"] + + if "queryStringParameters" in aws_event: + query_string = aws_event["queryStringParameters"] + if query_string: + filtered_qs = _apply_key_value_collection_filtering( + items=query_string, + behaviour=data_collection["url_query_params"], + ) + if filtered_qs: + request["query_string"] = filtered_qs + + if "headers" in aws_event and isinstance(aws_event["headers"], dict): + request["headers"] = _filter_headers(aws_event["headers"]) + + if data_collection["user_info"]: + extracted_user = _get_user_from_event(aws_event) + if extracted_user: + user_info = sentry_event.setdefault("user", {}) + for key, value in extracted_user.items(): + user_info.setdefault(key, value) + + if "incoming_request" in data_collection["http_bodies"]: + if "body" in aws_event: + request["data"] = aws_event.get("body", "") + elif aws_event.get("body", None): + # Unfortunately couldn't find a way to get structured body from AWS + # event. Meaning every body is unstructured to us. + request["data"] = AnnotatedValue.removed_because_raw_data() + + sentry_event["request"] = deepcopy(request) + + return sentry_event + + return event_processor + + +def _get_url(aws_event: "Any", aws_context: "Any") -> str: + path = aws_event.get("path", None) + + headers = aws_event.get("headers") + if not isinstance(headers, dict): + headers = {} + + 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) + + +def _get_cloudwatch_logs_url(aws_context: "Any", start_time: "datetime") -> str: + """ + Generates a CloudWatchLogs console URL based on the context object + + Arguments: + aws_context {Any} -- context from lambda handler + + Returns: + str -- AWS Console URL to logs. + """ + formatstring = "%Y-%m-%dT%H:%M:%SZ" + region = environ.get("AWS_REGION", "") + + url = ( + "https://console.{domain}/cloudwatch/home?region={region}" + "#logEventViewer:group={log_group};stream={log_stream}" + ";start={start_time};end={end_time}" + ).format( + domain="amazonaws.cn" if region.startswith("cn-") else "aws.amazon.com", + region=region, + log_group=aws_context.log_group_name, + log_stream=aws_context.log_stream_name, + start_time=(start_time - timedelta(seconds=1)).strftime(formatstring), + end_time=(datetime.now(timezone.utc) + timedelta(seconds=2)).strftime( + formatstring + ), + ) + + return url diff --git a/sentry_sdk/integrations/aws_lambda/_runtime.py b/sentry_sdk/integrations/aws_lambda/_runtime.py new file mode 100644 index 0000000000..f331256b0e --- /dev/null +++ b/sentry_sdk/integrations/aws_lambda/_runtime.py @@ -0,0 +1,197 @@ +import json +import re +import sys +from typing import TYPE_CHECKING + +import sentry_sdk +from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration +from sentry_sdk.integrations.aws_lambda._handler import _wrap_handler +from sentry_sdk.utils import ( + capture_internal_exceptions, + ensure_integration_enabled, + event_from_exception, + logger, +) + +if TYPE_CHECKING: + from typing import Any, Callable, Optional, TypeVar + + from sentry_sdk._types import Event + + F = TypeVar("F", bound=Callable[..., Any]) + + +def _wrap_init_error(init_error: "F") -> "F": + @ensure_integration_enabled(AwsLambdaIntegration, init_error) + def sentry_init_error(*args: "Any", **kwargs: "Any") -> "Any": + + with capture_internal_exceptions(): + sentry_sdk.get_isolation_scope().clear_breadcrumbs() + + exc_info = sys.exc_info() + if exc_info and all(exc_info): + sentry_event, hint = event_from_exception( + exc_info, + mechanism={"type": "aws_lambda", "handled": False}, + ) + sentry_sdk.capture_event(sentry_event, hint=hint) + + else: + # Fall back to AWS lambdas JSON representation of the error + error_info = args[1] + if isinstance(error_info, str): + error_info = json.loads(error_info) + sentry_event = _event_from_error_json(error_info) + sentry_sdk.capture_event(sentry_event) + + return init_error(*args, **kwargs) + + return sentry_init_error # type: ignore + + +def _drain_queue() -> None: + with capture_internal_exceptions(): + client = sentry_sdk.get_client() + integration = client.get_integration(AwsLambdaIntegration) + if integration is not None: + # Flush out the event queue before AWS kills the + # process. + client.flush() + + +def _setup_once() -> None: + lambda_bootstrap = get_lambda_bootstrap() + if not lambda_bootstrap: + logger.warning( + "Not running in AWS Lambda environment, " + "AwsLambdaIntegration disabled (could not find bootstrap module)" + ) + return + + if not hasattr(lambda_bootstrap, "handle_event_request"): + logger.warning( + "Not running in AWS Lambda environment, " + "AwsLambdaIntegration disabled (could not find handle_event_request)" + ) + return + + lambda_bootstrap.LambdaRuntimeClient.post_init_error = _wrap_init_error( + lambda_bootstrap.LambdaRuntimeClient.post_init_error + ) + + old_handle_event_request = lambda_bootstrap.handle_event_request + + def sentry_handle_event_request( # type: ignore + lambda_runtime_client, request_handler, *args, **kwargs + ): + request_handler = _wrap_handler(request_handler) + return old_handle_event_request( + lambda_runtime_client, request_handler, *args, **kwargs + ) + + lambda_bootstrap.handle_event_request = sentry_handle_event_request + + # Patch the runtime client to drain the queue. This should work + # even when the SDK is initialized inside of the handler + + def _wrap_post_function(f: "F") -> "F": + def inner(*args: "Any", **kwargs: "Any") -> "Any": + _drain_queue() + return f(*args, **kwargs) + + return inner # type: ignore + + lambda_bootstrap.LambdaRuntimeClient.post_invocation_result = _wrap_post_function( + lambda_bootstrap.LambdaRuntimeClient.post_invocation_result + ) + lambda_bootstrap.LambdaRuntimeClient.post_invocation_error = _wrap_post_function( + lambda_bootstrap.LambdaRuntimeClient.post_invocation_error + ) + + +def get_lambda_bootstrap() -> "Optional[Any]": + # Python 3.7: If the bootstrap module is *already imported*, it is the + # one we actually want to use (no idea what's in __main__) + # + # Python 3.8: bootstrap is also importable, but will be the same file + # as __main__ imported under a different name: + # + # sys.modules['__main__'].__file__ == sys.modules['bootstrap'].__file__ + # sys.modules['__main__'] is not sys.modules['bootstrap'] + # + # Python 3.9: bootstrap is in __main__.awslambdaricmain + # + # On container builds using the `aws-lambda-python-runtime-interface-client` + # (awslamdaric) module, bootstrap is located in sys.modules['__main__'].bootstrap + # + # Such a setup would then make all monkeypatches useless. + if "bootstrap" in sys.modules: + return sys.modules["bootstrap"] + elif "__main__" in sys.modules: + module = sys.modules["__main__"] + # python3.9 runtime + if hasattr(module, "awslambdaricmain") and hasattr( + module.awslambdaricmain, "bootstrap" + ): + return module.awslambdaricmain.bootstrap + elif hasattr(module, "bootstrap"): + # awslambdaric python module in container builds + return module.bootstrap + + # python3.8 runtime + return module + else: + return None + + +def _parse_formatted_traceback(formatted_tb: "list[str]") -> "list[dict[str, Any]]": + frames = [] + for frame in formatted_tb: + match = re.match(r'File "(.+)", line (\d+), in (.+)', frame.strip()) + if match: + file_name, line_number, func_name = match.groups() + line_number = int(line_number) + frames.append( + { + "filename": file_name, + "function": func_name, + "lineno": line_number, + "vars": None, + "pre_context": None, + "context_line": None, + "post_context": None, + } + ) + return frames + + +def _event_from_error_json(error_json: "dict[str, Any]") -> "Event": + """ + Converts the error JSON from AWS Lambda into a Sentry error event. + This is not a full fletched event, but better than nothing. + + This is an example of where AWS creates the error JSON: + https://github.com/aws/aws-lambda-python-runtime-interface-client/blob/2.2.1/awslambdaric/bootstrap.py#L479 + """ + event: "Event" = { + "level": "error", + "exception": { + "values": [ + { + "type": error_json.get("errorType"), + "value": error_json.get("errorMessage"), + "stacktrace": { + "frames": _parse_formatted_traceback( + error_json.get("stackTrace", []) + ), + }, + "mechanism": { + "type": "aws_lambda", + "handled": False, + }, + } + ], + }, + } + + return event diff --git a/sentry_sdk/integrations/aws_lambda/consts.py b/sentry_sdk/integrations/aws_lambda/consts.py new file mode 100644 index 0000000000..5a447ca231 --- /dev/null +++ b/sentry_sdk/integrations/aws_lambda/consts.py @@ -0,0 +1,6 @@ +IDENTIFIER = "aws_lambda" +ORIGIN = f"auto.function.{IDENTIFIER}" + +# buffer time time required to send timeout warning. +TIMEOUT_WARNING_BUFFER = 1500 +MILLIS_TO_SECONDS = 1000.0 diff --git a/sentry_sdk/integrations/chalice.py b/sentry_sdk/integrations/chalice.py index 3916f182bf..7d2b072b57 100644 --- a/sentry_sdk/integrations/chalice.py +++ b/sentry_sdk/integrations/chalice.py @@ -4,7 +4,7 @@ import sentry_sdk from sentry_sdk.integrations import DidNotEnable, Integration, _check_minimum_version from sentry_sdk.integrations._wsgi_common import _filter_headers -from sentry_sdk.integrations.aws_lambda import _make_request_event_processor +from sentry_sdk.integrations.aws_lambda._request import _make_request_event_processor from sentry_sdk.traces import ( SegmentNameSource, Span, From a395170d5bed58a829d7f3127d23adf7df02ed5c Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Fri, 16 Oct 2026 14:25:00 +0200 Subject: [PATCH 2/3] inline `_event_from_error_json` --- .../integrations/aws_lambda/_runtime.py | 59 ++++++++----------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/sentry_sdk/integrations/aws_lambda/_runtime.py b/sentry_sdk/integrations/aws_lambda/_runtime.py index f331256b0e..0e3c0fbfd7 100644 --- a/sentry_sdk/integrations/aws_lambda/_runtime.py +++ b/sentry_sdk/integrations/aws_lambda/_runtime.py @@ -41,8 +41,31 @@ def sentry_init_error(*args: "Any", **kwargs: "Any") -> "Any": error_info = args[1] if isinstance(error_info, str): error_info = json.loads(error_info) - sentry_event = _event_from_error_json(error_info) - sentry_sdk.capture_event(sentry_event) + # convert error JSON into Sentry error event; not full fletched + # but better than nothing. Example of where AWS creates the error JSON: + # https://github.com/aws/aws-lambda-python-runtime-interface-client/blob/2.2.1/awslambdaric/bootstrap.py#L479 + sentry_error_event: "Event" = { + "level": "error", + "exception": { + "values": [ + { + "type": error_info.get("errorType"), + "value": error_info.get("errorMessage"), + "stacktrace": { + "frames": _parse_formatted_traceback( + error_info.get("stackTrace", []) + ), + }, + "mechanism": { + "type": "aws_lambda", + "handled": False, + }, + } + ], + }, + } + + sentry_sdk.capture_event(sentry_error_event) return init_error(*args, **kwargs) @@ -163,35 +186,3 @@ def _parse_formatted_traceback(formatted_tb: "list[str]") -> "list[dict[str, Any } ) return frames - - -def _event_from_error_json(error_json: "dict[str, Any]") -> "Event": - """ - Converts the error JSON from AWS Lambda into a Sentry error event. - This is not a full fletched event, but better than nothing. - - This is an example of where AWS creates the error JSON: - https://github.com/aws/aws-lambda-python-runtime-interface-client/blob/2.2.1/awslambdaric/bootstrap.py#L479 - """ - event: "Event" = { - "level": "error", - "exception": { - "values": [ - { - "type": error_json.get("errorType"), - "value": error_json.get("errorMessage"), - "stacktrace": { - "frames": _parse_formatted_traceback( - error_json.get("stackTrace", []) - ), - }, - "mechanism": { - "type": "aws_lambda", - "handled": False, - }, - } - ], - }, - } - - return event From 666405657dab175d75cd902ec7856d9b13b2650c Mon Sep 17 00:00:00 2001 From: Pablo Deputter Date: Fri, 16 Oct 2026 14:39:44 +0200 Subject: [PATCH 3/3] wip --- .../integrations/aws_lambda/__init__.py | 66 ++++++++++++++++++- .../integrations/aws_lambda/_handler.py | 6 +- .../integrations/aws_lambda/_runtime.py | 58 ++-------------- 3 files changed, 72 insertions(+), 58 deletions(-) diff --git a/sentry_sdk/integrations/aws_lambda/__init__.py b/sentry_sdk/integrations/aws_lambda/__init__.py index 14339a74cc..aa6e4b367e 100644 --- a/sentry_sdk/integrations/aws_lambda/__init__.py +++ b/sentry_sdk/integrations/aws_lambda/__init__.py @@ -1,5 +1,19 @@ +from typing import TYPE_CHECKING + from sentry_sdk.integrations import Integration +from sentry_sdk.integrations.aws_lambda._handler import _wrap_handler +from sentry_sdk.integrations.aws_lambda._runtime import ( + _drain_queue, + _wrap_init_error, + get_lambda_bootstrap, +) from sentry_sdk.integrations.aws_lambda.consts import IDENTIFIER, ORIGIN +from sentry_sdk.utils import logger + +if TYPE_CHECKING: + from typing import Any, Callable, TypeVar + + F = TypeVar("F", bound=Callable[..., Any]) class AwsLambdaIntegration(Integration): @@ -11,6 +25,54 @@ def __init__(self, timeout_warning: bool = False) -> None: @staticmethod def setup_once() -> None: - from sentry_sdk.integrations.aws_lambda._runtime import _setup_once + lambda_bootstrap = get_lambda_bootstrap() + if not lambda_bootstrap: + logger.warning( + "Not running in AWS Lambda environment, " + "AwsLambdaIntegration disabled (could not find bootstrap module)" + ) + return + + if not hasattr(lambda_bootstrap, "handle_event_request"): + logger.warning( + "Not running in AWS Lambda environment, " + "AwsLambdaIntegration disabled (could not find handle_event_request)" + ) + return + + lambda_bootstrap.LambdaRuntimeClient.post_init_error = _wrap_init_error( + lambda_bootstrap.LambdaRuntimeClient.post_init_error + ) + + old_handle_event_request = lambda_bootstrap.handle_event_request + + def sentry_handle_event_request( # type: ignore + lambda_runtime_client, request_handler, *args, **kwargs + ): + request_handler = _wrap_handler(request_handler) + return old_handle_event_request( + lambda_runtime_client, request_handler, *args, **kwargs + ) + + lambda_bootstrap.handle_event_request = sentry_handle_event_request + + # Patch the runtime client to drain the queue. This should work + # even when the SDK is initialized inside of the handler + + def _wrap_post_function(f: "F") -> "F": + def inner(*args: "Any", **kwargs: "Any") -> "Any": + _drain_queue() + return f(*args, **kwargs) + + return inner # type: ignore - _setup_once() + lambda_bootstrap.LambdaRuntimeClient.post_invocation_result = ( + _wrap_post_function( + lambda_bootstrap.LambdaRuntimeClient.post_invocation_result + ) + ) + lambda_bootstrap.LambdaRuntimeClient.post_invocation_error = ( + _wrap_post_function( + lambda_bootstrap.LambdaRuntimeClient.post_invocation_error + ) + ) diff --git a/sentry_sdk/integrations/aws_lambda/_handler.py b/sentry_sdk/integrations/aws_lambda/_handler.py index c302509a79..ecc8d43f76 100644 --- a/sentry_sdk/integrations/aws_lambda/_handler.py +++ b/sentry_sdk/integrations/aws_lambda/_handler.py @@ -7,13 +7,13 @@ from sentry_sdk.consts import OP 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 import AwsLambdaIntegration 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 ( @@ -36,6 +36,8 @@ def _wrap_handler(handler: "F") -> "F": + from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration + @functools.wraps(handler) def sentry_handler( aws_event: "Any", aws_context: "Any", *args: "Any", **kwargs: "Any" @@ -150,7 +152,7 @@ def sentry_handler( parent_span=None, attributes={ "sentry.op": OP.FUNCTION_AWS, - "sentry.origin": AwsLambdaIntegration.origin, + "sentry.origin": ORIGIN, "sentry.segment.name.source": SegmentNameSource.COMPONENT, "cloud.region": aws_region, "cloud.resource_id": aws_context.invoked_function_arn, diff --git a/sentry_sdk/integrations/aws_lambda/_runtime.py b/sentry_sdk/integrations/aws_lambda/_runtime.py index 0e3c0fbfd7..04bccb8ac8 100644 --- a/sentry_sdk/integrations/aws_lambda/_runtime.py +++ b/sentry_sdk/integrations/aws_lambda/_runtime.py @@ -4,13 +4,11 @@ from typing import TYPE_CHECKING import sentry_sdk -from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration -from sentry_sdk.integrations.aws_lambda._handler import _wrap_handler +from sentry_sdk.integrations.aws_lambda.consts import IDENTIFIER from sentry_sdk.utils import ( capture_internal_exceptions, ensure_integration_enabled, event_from_exception, - logger, ) if TYPE_CHECKING: @@ -22,6 +20,8 @@ def _wrap_init_error(init_error: "F") -> "F": + from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration + @ensure_integration_enabled(AwsLambdaIntegration, init_error) def sentry_init_error(*args: "Any", **kwargs: "Any") -> "Any": @@ -75,63 +75,13 @@ def sentry_init_error(*args: "Any", **kwargs: "Any") -> "Any": def _drain_queue() -> None: with capture_internal_exceptions(): client = sentry_sdk.get_client() - integration = client.get_integration(AwsLambdaIntegration) + integration = client.get_integration(IDENTIFIER) if integration is not None: # Flush out the event queue before AWS kills the # process. client.flush() -def _setup_once() -> None: - lambda_bootstrap = get_lambda_bootstrap() - if not lambda_bootstrap: - logger.warning( - "Not running in AWS Lambda environment, " - "AwsLambdaIntegration disabled (could not find bootstrap module)" - ) - return - - if not hasattr(lambda_bootstrap, "handle_event_request"): - logger.warning( - "Not running in AWS Lambda environment, " - "AwsLambdaIntegration disabled (could not find handle_event_request)" - ) - return - - lambda_bootstrap.LambdaRuntimeClient.post_init_error = _wrap_init_error( - lambda_bootstrap.LambdaRuntimeClient.post_init_error - ) - - old_handle_event_request = lambda_bootstrap.handle_event_request - - def sentry_handle_event_request( # type: ignore - lambda_runtime_client, request_handler, *args, **kwargs - ): - request_handler = _wrap_handler(request_handler) - return old_handle_event_request( - lambda_runtime_client, request_handler, *args, **kwargs - ) - - lambda_bootstrap.handle_event_request = sentry_handle_event_request - - # Patch the runtime client to drain the queue. This should work - # even when the SDK is initialized inside of the handler - - def _wrap_post_function(f: "F") -> "F": - def inner(*args: "Any", **kwargs: "Any") -> "Any": - _drain_queue() - return f(*args, **kwargs) - - return inner # type: ignore - - lambda_bootstrap.LambdaRuntimeClient.post_invocation_result = _wrap_post_function( - lambda_bootstrap.LambdaRuntimeClient.post_invocation_result - ) - lambda_bootstrap.LambdaRuntimeClient.post_invocation_error = _wrap_post_function( - lambda_bootstrap.LambdaRuntimeClient.post_invocation_error - ) - - def get_lambda_bootstrap() -> "Optional[Any]": # Python 3.7: If the bootstrap module is *already imported*, it is the # one we actually want to use (no idea what's in __main__)