From 3c5f311250c9fd42f038225562f29fab6389f0c4 Mon Sep 17 00:00:00 2001 From: Shantanu Dhiman Date: Wed, 8 Jul 2020 12:31:38 +0530 Subject: [PATCH 1/6] Changes: 1) Added a new wrapper decorator for post_init_error method to capture initialization error for AWS Lambda integration. 2) Modified _wrap_handler decorator to include code which runs a parallel thread to capture timeout error. 3) Modified _make_request_event_processor decorator to include execution duration as parameter. 4) Added TimeoutThread class in utils.py which is useful to capture timeout error. --- sentry_sdk/integrations/aws_lambda.py | 85 +++++++++++++++++++++++++-- sentry_sdk/utils.py | 29 +++++++++ 2 files changed, 110 insertions(+), 4 deletions(-) diff --git a/sentry_sdk/integrations/aws_lambda.py b/sentry_sdk/integrations/aws_lambda.py index 3a08d998db..007404cb65 100644 --- a/sentry_sdk/integrations/aws_lambda.py +++ b/sentry_sdk/integrations/aws_lambda.py @@ -1,6 +1,7 @@ from datetime import datetime, timedelta from os import environ import sys +import json from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk._compat import reraise @@ -9,6 +10,7 @@ capture_internal_exceptions, event_from_exception, logger, + TimeoutThread, ) from sentry_sdk.integrations import Integration from sentry_sdk.integrations._wsgi_common import _filter_headers @@ -25,6 +27,49 @@ F = TypeVar("F", bound=Callable[..., Any]) +# Constants +TIMEOUT_THRESHOLD_MILLIS = 1500 # Minimum time required to capture TimeoutError +SECONDS_CONVERSION_FACTOR = 1000.0 + + +def _wrap_init_error(init_error): + # type: (F) -> F + def sentry_init_error(*args, **kwargs): + # type: (*Any, **Any) -> Any + + # Fetch Initialization error details from arguments + error = json.loads(args[1]) + + hub = Hub.current + integration = hub.get_integration(AwsLambdaIntegration) + if integration is None: + return init_error(*args, **kwargs) + + # If an integration is there, a client has to be there. + client = hub.client # type: Any + + with hub.push_scope() as scope: + with capture_internal_exceptions(): + scope.clear_breadcrumbs() + try: + # Checking if there is any error/exception which is raised in the runtime + # environment from arguments and, re-raising it to capture it as an event. + if error.get("errorType"): + exc_info = sys.exc_info() + reraise(*exc_info) + except Exception: + exc_info = sys.exc_info() + event, hint = event_from_exception( + exc_info, + client_options=client.options, + mechanism={"type": "aws_lambda", "handled": False}, + ) + hub.capture_event(event, hint=hint) + + return init_error(*args, **kwargs) + + return sentry_init_error # type: ignore + def _wrap_handler(handler): # type: (F) -> F @@ -37,14 +82,31 @@ def sentry_handler(event, context, *args, **kwargs): # If an integration is there, a client has to be there. client = hub.client # type: Any + configured_time_in_millis = context.get_remaining_time_in_millis() with hub.push_scope() as scope: with capture_internal_exceptions(): scope.clear_breadcrumbs() scope.transaction = context.function_name - scope.add_event_processor(_make_request_event_processor(event, context)) + scope.add_event_processor(_make_request_event_processor(event, context, configured_time_in_millis)) try: + # Checking if parameter to check timeout is set True + if integration.get_check_timeout_error(): + # Starting the Timeout thread only if the configured time is greater than Timeout threshold value + if configured_time_in_millis > TIMEOUT_THRESHOLD_MILLIS: + remaining_time_in_sec = (configured_time_in_millis - TIMEOUT_THRESHOLD_MILLIS)/SECONDS_CONVERSION_FACTOR + + configured_time_in_sec = configured_time_in_millis / SECONDS_CONVERSION_FACTOR + configured_time = int(configured_time_in_sec) + + # Setting up the exact integer value of configured time(in seconds) + if configured_time < configured_time_in_sec: + configured_time = configured_time + 1 + + # Starting the thread to raise timeout warning exception + timeout_thread = TimeoutThread(remaining_time_in_sec, configured_time) + timeout_thread.start() return handler(event, context, *args, **kwargs) except Exception: exc_info = sys.exc_info() @@ -73,6 +135,14 @@ def _drain_queue(): class AwsLambdaIntegration(Integration): identifier = "aws_lambda" + def __init__(self, check_timeout_error=False): + # type: (bool) -> None + self.check_timeout_error = check_timeout_error + + def get_check_timeout_error(self): + # type: () -> bool + return self.check_timeout_error + @staticmethod def setup_once(): # type: () -> None @@ -126,6 +196,10 @@ def sentry_to_json(*args, **kwargs): lambda_bootstrap.to_json = sentry_to_json else: + 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 @@ -158,19 +232,22 @@ def inner(*args, **kwargs): ) -def _make_request_event_processor(aws_event, aws_context): - # type: (Any, Any) -> EventProcessor +def _make_request_event_processor(aws_event, aws_context, configured_timeout): + # type: (Any, Any, Any) -> EventProcessor start_time = datetime.now() def event_processor(event, hint, start_time=start_time): # type: (Event, Hint, datetime) -> Optional[Event] extra = event.setdefault("extra", {}) + remaining_time_in_milis = aws_context.get_remaining_time_in_millis() + exec_duration = configured_timeout - remaining_time_in_milis extra["lambda"] = { "function_name": aws_context.function_name, "function_version": aws_context.function_version, "invoked_function_arn": aws_context.invoked_function_arn, - "remaining_time_in_millis": aws_context.get_remaining_time_in_millis(), + "remaining_time_in_millis": remaining_time_in_milis, "aws_request_id": aws_context.aws_request_id, + "execution_duration": exec_duration, } extra["cloudwatch logs"] = { diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index 548796399c..f9c7d8519f 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -3,6 +3,8 @@ import logging import os import sys +import time +import threading from datetime import datetime @@ -870,3 +872,30 @@ def transaction_from_function(func): disable_capture_event = ContextVar("disable_capture_event") + + +class TimeoutThread(threading.Thread): + """Creates a Thread.""" + + def __init__(self, timeout_duration, configured_timeout): + # type: (float, int) -> None + threading.Thread.__init__(self) + self.timeout_duration = timeout_duration + self.configured_timeout = configured_timeout + + def get_timeout_duration(self): + # type: () -> float + return self.timeout_duration + + def get_configured_timeout(self): + # type: () -> int + return self.configured_timeout + + def run(self): + # type: () -> None + time.sleep(self.get_timeout_duration()) + # Raising Exception after timeout duration is reached + raise Exception( + "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds".format( + self.get_configured_timeout()) + ) From 1bf10870c80dca29319f2822cc66ee85c0cb998e Mon Sep 17 00:00:00 2001 From: Shantanu Dhiman Date: Wed, 8 Jul 2020 15:58:26 +0530 Subject: [PATCH 2/6] Changes: 1) As per review comments, moved the statement which fetches error details from args of sentry_init_error() method after the integration check. 2) As per review comments, removed the try-except block as execution info was available to us directly. --- sentry_sdk/integrations/aws_lambda.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/sentry_sdk/integrations/aws_lambda.py b/sentry_sdk/integrations/aws_lambda.py index 007404cb65..04c5df00e7 100644 --- a/sentry_sdk/integrations/aws_lambda.py +++ b/sentry_sdk/integrations/aws_lambda.py @@ -37,27 +37,23 @@ def _wrap_init_error(init_error): def sentry_init_error(*args, **kwargs): # type: (*Any, **Any) -> Any - # Fetch Initialization error details from arguments - error = json.loads(args[1]) - hub = Hub.current integration = hub.get_integration(AwsLambdaIntegration) if integration is None: return init_error(*args, **kwargs) + # Fetch Initialization error details from arguments + error = json.loads(args[1]) + # If an integration is there, a client has to be there. client = hub.client # type: Any with hub.push_scope() as scope: with capture_internal_exceptions(): scope.clear_breadcrumbs() - try: - # Checking if there is any error/exception which is raised in the runtime - # environment from arguments and, re-raising it to capture it as an event. - if error.get("errorType"): - exc_info = sys.exc_info() - reraise(*exc_info) - except Exception: + # Checking if there is any error/exception which is raised in the runtime + # environment from arguments and, re-raising it to capture it as an event. + if error.get("errorType"): exc_info = sys.exc_info() event, hint = event_from_exception( exc_info, From e53bde219f1ccc68e1f4c50fbeefd152c0e99937 Mon Sep 17 00:00:00 2001 From: Shantanu Dhiman Date: Wed, 15 Jul 2020 01:36:39 +0530 Subject: [PATCH 3/6] Changes: 1) Added automation unit tests for AWS Lambda integration. 2) The test cases are for timeout and initialization errors. --- tests/integrations/aws_lambda/test_aws.py | 54 ++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/tests/integrations/aws_lambda/test_aws.py b/tests/integrations/aws_lambda/test_aws.py index bc18d06b39..e634784399 100644 --- a/tests/integrations/aws_lambda/test_aws.py +++ b/tests/integrations/aws_lambda/test_aws.py @@ -67,7 +67,11 @@ def inner(code, payload): # Check file for valid syntax first, and that the integration does not # crash when not running in Lambda (but rather a local deployment tool # such as chalice's) - subprocess.check_call([sys.executable, str(tmp.join("test_lambda.py"))]) + try: + subprocess.check_call([sys.executable, str(tmp.join("test_lambda.py"))]) + except Exception as e: + # Exception caught in case of Initialization error + pass tmp.join("setup.cfg").write("[install]\nprefix=") subprocess.check_call([sys.executable, "setup.py", "sdist", "-d", str(tmpdir)]) @@ -85,6 +89,7 @@ def inner(code, payload): Handler="test_lambda.test_handler", Code={"ZipFile": tmpdir.join("ball.zip").read(mode="rb")}, Description="Created as part of testsuite for getsentry/sentry-python", + Timeout=4, ) @request.addfinalizer @@ -234,3 +239,50 @@ def test_handler(event, context): "query_string": {"bonkers": "true"}, "url": "https://iwsz2c7uwi.execute-api.us-east-1.amazonaws.com/asd", } + + +def test_init_error(run_lambda_function): + events, response = run_lambda_function( + LAMBDA_PRELUDE + + dedent( + """ + init_sdk() + func() + def test_handler(event, context): + return 0 + """ + ), + b'{"foo": "bar"}', + ) + log_result = (base64.b64decode(response["LogResult"])).decode("utf-8") + expected_text = "name 'func' is not defined" + assert expected_text in log_result + + +def test_timeout_error(run_lambda_function): + # Modifying LAMBDA_PRELUDE since capturing timeout error is kept optional. + modified_prelude = LAMBDA_PRELUDE.replace("[AwsLambdaIntegration()]", "[AwsLambdaIntegration(True)]") + events, response = run_lambda_function( + modified_prelude + + dedent( + """ + init_sdk() + def test_handler(event, context): + time.sleep(10) + return 0 + """ + ), + b'{"foo": "bar"}', + ) + expected_text = "WARNING : Function is expected to get timed out. Configured timeout duration = 4 seconds" + if not events: + # In case of Python 2.7 runtime environment + log_result = (base64.b64decode(response["LogResult"])).decode("utf-8") + assert expected_text in log_result + else: + # In case of Python 3.6, 3.7 & 3.8 runtime environments + (event,) = events + assert event["level"] == "error" + (exception,) = event["exception"]["values"] + assert exception["type"] == "Exception" + assert exception["value"] == expected_text From 9bacb3df6a86cca890835d50c54535462f919e51 Mon Sep 17 00:00:00 2001 From: Shantanu Dhiman Date: Wed, 22 Jul 2020 20:18:50 +0530 Subject: [PATCH 4/6] Changes: 1) Changed variable names as per review comments for check_timeout_error, TIMEOUT_THRESHOLD_MILLIS, SECONDS_CONVERSION_FACTOR. 2) Removed unnecessary getter methods. 3) Modified docstring for TimeoutThread class. 4) Added new context (new section) for execution data. 5) Moved logic to generate timeout warning inside capture_exception with context. 6) Parameterized subprocess.check_call() method for initialization error. 7) Created a new exception class ServerlessTimeoutWarning raised for case of timeouts. 8) Fixed other minor issues as per review comments. --- sentry_sdk/integrations/aws_lambda.py | 70 +++++++++++++---------- sentry_sdk/utils.py | 39 ++++++++----- tests/integrations/aws_lambda/test_aws.py | 36 +++++------- 3 files changed, 79 insertions(+), 66 deletions(-) diff --git a/sentry_sdk/integrations/aws_lambda.py b/sentry_sdk/integrations/aws_lambda.py index 04c5df00e7..bbf8395629 100644 --- a/sentry_sdk/integrations/aws_lambda.py +++ b/sentry_sdk/integrations/aws_lambda.py @@ -2,6 +2,7 @@ from os import environ import sys import json +import os from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk._compat import reraise @@ -28,8 +29,8 @@ F = TypeVar("F", bound=Callable[..., Any]) # Constants -TIMEOUT_THRESHOLD_MILLIS = 1500 # Minimum time required to capture TimeoutError -SECONDS_CONVERSION_FACTOR = 1000.0 +TIMEOUT_WARNING_BUFFER = 1500 # Buffer time required to send timeout warning to Sentry +MILLIS_TO_SECONDS = 1000.0 def _wrap_init_error(init_error): @@ -78,31 +79,33 @@ def sentry_handler(event, context, *args, **kwargs): # If an integration is there, a client has to be there. client = hub.client # type: Any - configured_time_in_millis = context.get_remaining_time_in_millis() + configured_time = context.get_remaining_time_in_millis() with hub.push_scope() as scope: with capture_internal_exceptions(): scope.clear_breadcrumbs() scope.transaction = context.function_name - scope.add_event_processor(_make_request_event_processor(event, context, configured_time_in_millis)) + scope.add_event_processor( + _make_request_event_processor(event, 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 + ) + + # Starting the thread to raise timeout warning exception + timeout_thread.start() try: - # Checking if parameter to check timeout is set True - if integration.get_check_timeout_error(): - # Starting the Timeout thread only if the configured time is greater than Timeout threshold value - if configured_time_in_millis > TIMEOUT_THRESHOLD_MILLIS: - remaining_time_in_sec = (configured_time_in_millis - TIMEOUT_THRESHOLD_MILLIS)/SECONDS_CONVERSION_FACTOR - - configured_time_in_sec = configured_time_in_millis / SECONDS_CONVERSION_FACTOR - configured_time = int(configured_time_in_sec) - - # Setting up the exact integer value of configured time(in seconds) - if configured_time < configured_time_in_sec: - configured_time = configured_time + 1 - - # Starting the thread to raise timeout warning exception - timeout_thread = TimeoutThread(remaining_time_in_sec, configured_time) - timeout_thread.start() return handler(event, context, *args, **kwargs) except Exception: exc_info = sys.exc_info() @@ -131,13 +134,9 @@ def _drain_queue(): class AwsLambdaIntegration(Integration): identifier = "aws_lambda" - def __init__(self, check_timeout_error=False): + def __init__(self, timeout_warning=False): # type: (bool) -> None - self.check_timeout_error = check_timeout_error - - def get_check_timeout_error(self): - # type: () -> bool - return self.check_timeout_error + self.timeout_warning = timeout_warning @staticmethod def setup_once(): @@ -234,16 +233,29 @@ def _make_request_event_processor(aws_event, aws_context, configured_timeout): def event_processor(event, hint, start_time=start_time): # type: (Event, Hint, datetime) -> Optional[Event] - extra = event.setdefault("extra", {}) + total_memory, used_memory, free_memory = map( + int, os.popen("free -t -m").readlines()[-1].split()[1:] + ) remaining_time_in_milis = aws_context.get_remaining_time_in_millis() exec_duration = configured_timeout - remaining_time_in_milis + + contexts = event.setdefault("contexts", {}) + if ( + isinstance(contexts, dict) + and "memory usage and execution duration" not in contexts + ): + contexts["memory usage and execution time"] = { + "Execution duration in millis": exec_duration, + "Memory usage in MB": used_memory, + "Remaining time in millis": remaining_time_in_milis, + } + + extra = 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, - "remaining_time_in_millis": remaining_time_in_milis, "aws_request_id": aws_context.aws_request_id, - "execution_duration": exec_duration, } extra["cloudwatch logs"] = { diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py index f9c7d8519f..d6bf44bb7e 100644 --- a/sentry_sdk/utils.py +++ b/sentry_sdk/utils.py @@ -874,28 +874,37 @@ def transaction_from_function(func): disable_capture_event = ContextVar("disable_capture_event") +class ServerlessTimeoutWarning(Exception): + """Raised when a serverless method is about to reach its timeout.""" + + pass + + class TimeoutThread(threading.Thread): - """Creates a Thread.""" + """Creates a Thread which runs (sleeps) for a time duration equal to + waiting_time and raises a custom ServerlessTimeout exception. + """ - def __init__(self, timeout_duration, configured_timeout): + def __init__(self, waiting_time, configured_timeout): # type: (float, int) -> None threading.Thread.__init__(self) - self.timeout_duration = timeout_duration + self.waiting_time = waiting_time self.configured_timeout = configured_timeout - def get_timeout_duration(self): - # type: () -> float - return self.timeout_duration - - def get_configured_timeout(self): - # type: () -> int - return self.configured_timeout - def run(self): # type: () -> None - time.sleep(self.get_timeout_duration()) + + time.sleep(self.waiting_time) + + integer_configured_timeout = int(self.configured_timeout) + + # Setting up the exact integer value of configured time(in seconds) + if integer_configured_timeout < self.configured_timeout: + integer_configured_timeout = integer_configured_timeout + 1 + # Raising Exception after timeout duration is reached - raise Exception( - "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds".format( - self.get_configured_timeout()) + raise ServerlessTimeoutWarning( + "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format( + integer_configured_timeout + ) ) diff --git a/tests/integrations/aws_lambda/test_aws.py b/tests/integrations/aws_lambda/test_aws.py index e634784399..ba4f346d17 100644 --- a/tests/integrations/aws_lambda/test_aws.py +++ b/tests/integrations/aws_lambda/test_aws.py @@ -31,11 +31,11 @@ def _send_event(self, event): time.sleep(1) print("\\nEVENT:", json.dumps(event)) -def init_sdk(**extra_init_args): +def init_sdk(timeout_warning=False, **extra_init_args): sentry_sdk.init( dsn="https://123abc@example.com/123", transport=TestTransport, - integrations=[AwsLambdaIntegration()], + integrations=[AwsLambdaIntegration(timeout_warning=timeout_warning)], shutdown_timeout=10, **extra_init_args ) @@ -57,7 +57,7 @@ def lambda_client(): @pytest.fixture(params=["python3.6", "python3.7", "python3.8", "python2.7"]) def run_lambda_function(tmpdir, lambda_client, request, relay_normalize): - def inner(code, payload): + def inner(code, payload, syntax_check=True): runtime = request.param tmpdir.ensure_dir("lambda_tmp").remove() tmp = tmpdir.ensure_dir("lambda_tmp") @@ -67,11 +67,8 @@ def inner(code, payload): # Check file for valid syntax first, and that the integration does not # crash when not running in Lambda (but rather a local deployment tool # such as chalice's) - try: + if syntax_check: subprocess.check_call([sys.executable, str(tmp.join("test_lambda.py"))]) - except Exception as e: - # Exception caught in case of Initialization error - pass tmp.join("setup.cfg").write("[install]\nprefix=") subprocess.check_call([sys.executable, "setup.py", "sdist", "-d", str(tmpdir)]) @@ -126,6 +123,8 @@ def test_basic(run_lambda_function): + dedent( """ init_sdk() + + def test_handler(event, context): raise Exception("something went wrong") """ @@ -248,11 +247,13 @@ def test_init_error(run_lambda_function): """ init_sdk() func() + def test_handler(event, context): return 0 """ ), b'{"foo": "bar"}', + syntax_check=False, ) log_result = (base64.b64decode(response["LogResult"])).decode("utf-8") expected_text = "name 'func' is not defined" @@ -260,13 +261,13 @@ def test_handler(event, context): def test_timeout_error(run_lambda_function): - # Modifying LAMBDA_PRELUDE since capturing timeout error is kept optional. - modified_prelude = LAMBDA_PRELUDE.replace("[AwsLambdaIntegration()]", "[AwsLambdaIntegration(True)]") events, response = run_lambda_function( - modified_prelude + LAMBDA_PRELUDE + dedent( """ - init_sdk() + init_sdk(timeout_warning=True) + + def test_handler(event, context): time.sleep(10) return 0 @@ -275,14 +276,5 @@ def test_handler(event, context): b'{"foo": "bar"}', ) expected_text = "WARNING : Function is expected to get timed out. Configured timeout duration = 4 seconds" - if not events: - # In case of Python 2.7 runtime environment - log_result = (base64.b64decode(response["LogResult"])).decode("utf-8") - assert expected_text in log_result - else: - # In case of Python 3.6, 3.7 & 3.8 runtime environments - (event,) = events - assert event["level"] == "error" - (exception,) = event["exception"]["values"] - assert exception["type"] == "Exception" - assert exception["value"] == expected_text + log_result = (base64.b64decode(response["LogResult"])).decode("utf-8") + assert expected_text in log_result From f65d9c93bae4b771bf94632e3a3cad1d23180398 Mon Sep 17 00:00:00 2001 From: Shantanu Dhiman Date: Wed, 29 Jul 2020 03:32:22 +0530 Subject: [PATCH 5/6] Changes: 1) Removed the memory usage data and reverted back the execution time and remaining time data in the 'lambda' key of 'addition data' context. 2) Paramterized the time.sleep() method in _send_transport() method of LAMBDA_PRELUDE to get the event data for timeout error test case. --- sentry_sdk/integrations/aws_lambda.py | 17 ++--------- tests/integrations/aws_lambda/test_aws.py | 35 ++++++++++++++++++++--- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/sentry_sdk/integrations/aws_lambda.py b/sentry_sdk/integrations/aws_lambda.py index bbf8395629..f5b16be1cf 100644 --- a/sentry_sdk/integrations/aws_lambda.py +++ b/sentry_sdk/integrations/aws_lambda.py @@ -2,7 +2,6 @@ from os import environ import sys import json -import os from sentry_sdk.hub import Hub, _should_send_default_pii from sentry_sdk._compat import reraise @@ -233,29 +232,17 @@ def _make_request_event_processor(aws_event, aws_context, configured_timeout): def event_processor(event, hint, start_time=start_time): # type: (Event, Hint, datetime) -> Optional[Event] - total_memory, used_memory, free_memory = map( - int, os.popen("free -t -m").readlines()[-1].split()[1:] - ) remaining_time_in_milis = aws_context.get_remaining_time_in_millis() exec_duration = configured_timeout - remaining_time_in_milis - contexts = event.setdefault("contexts", {}) - if ( - isinstance(contexts, dict) - and "memory usage and execution duration" not in contexts - ): - contexts["memory usage and execution time"] = { - "Execution duration in millis": exec_duration, - "Memory usage in MB": used_memory, - "Remaining time in millis": remaining_time_in_milis, - } - extra = 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"] = { diff --git a/tests/integrations/aws_lambda/test_aws.py b/tests/integrations/aws_lambda/test_aws.py index ba4f346d17..3398dfeaa0 100644 --- a/tests/integrations/aws_lambda/test_aws.py +++ b/tests/integrations/aws_lambda/test_aws.py @@ -22,13 +22,16 @@ import json from sentry_sdk.transport import HttpTransport +FLUSH_EVENT = True + class TestTransport(HttpTransport): def _send_event(self, event): # Delay event output like this to test proper shutdown # Note that AWS Lambda truncates the log output to 4kb, so you better # pray that your events are smaller than that or else tests start # failing. - time.sleep(1) + if FLUSH_EVENT: + time.sleep(1) print("\\nEVENT:", json.dumps(event)) def init_sdk(timeout_warning=False, **extra_init_args): @@ -255,6 +258,7 @@ def test_handler(event, context): b'{"foo": "bar"}', syntax_check=False, ) + log_result = (base64.b64decode(response["LogResult"])).decode("utf-8") expected_text = "name 'func' is not defined" assert expected_text in log_result @@ -266,6 +270,7 @@ def test_timeout_error(run_lambda_function): + dedent( """ init_sdk(timeout_warning=True) + FLUSH_EVENT=False def test_handler(event, context): @@ -275,6 +280,28 @@ def test_handler(event, context): ), b'{"foo": "bar"}', ) - expected_text = "WARNING : Function is expected to get timed out. Configured timeout duration = 4 seconds" - log_result = (base64.b64decode(response["LogResult"])).decode("utf-8") - assert expected_text in log_result + + (event,) = events + assert event["level"] == "error" + (exception,) = event["exception"]["values"] + assert exception["type"] == "ServerlessTimeoutWarning" + assert ( + exception["value"] + == "WARNING : Function is expected to get timed out. Configured timeout duration = 4 seconds." + ) + + assert exception["mechanism"] == {"type": "threading", "handled": False} + + assert event["extra"]["lambda"]["function_name"].startswith("test_function_") + + logs_url = event["extra"]["cloudwatch logs"]["url"] + assert logs_url.startswith("https://console.aws.amazon.com/cloudwatch/home?region=") + assert not re.search("(=;|=$)", logs_url) + assert event["extra"]["cloudwatch logs"]["log_group"].startswith( + "/aws/lambda/test_function_" + ) + + log_stream_re = "^[0-9]{4}/[0-9]{2}/[0-9]{2}/\\[[^\\]]+][a-f0-9]+$" + log_stream = event["extra"]["cloudwatch logs"]["log_stream"] + + assert re.match(log_stream_re, log_stream) From fcc06d5b5fe6bf05a6292ae0d3a805d76824e4ca Mon Sep 17 00:00:00 2001 From: Shantanu Dhiman Date: Wed, 29 Jul 2020 16:03:10 +0530 Subject: [PATCH 6/6] Fixed linting failure by adding syntax_check parameter in inner() method in test_aws.py --- tests/integrations/aws_lambda/test_aws.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/aws_lambda/test_aws.py b/tests/integrations/aws_lambda/test_aws.py index d3011bdbe2..b6af32f181 100644 --- a/tests/integrations/aws_lambda/test_aws.py +++ b/tests/integrations/aws_lambda/test_aws.py @@ -63,7 +63,7 @@ def run_lambda_function(tmpdir, lambda_client, request, relay_normalize): if request.param == "python3.8": pytest.xfail("Python 3.8 is currently broken") - def inner(code, payload): + def inner(code, payload, syntax_check=True): runtime = request.param tmpdir.ensure_dir("lambda_tmp").remove() tmp = tmpdir.ensure_dir("lambda_tmp")