Skip to content

fix: Fix AWS Lambda under Python 3.8 and refactor test setup code #766

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 1, 2020
Merged
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
51 changes: 30 additions & 21 deletions sentry_sdk/integrations/aws_lambda.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
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
Expand Down Expand Up @@ -42,19 +41,15 @@ def sentry_init_error(*args, **kwargs):
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():
with capture_internal_exceptions():
with hub.configure_scope() as scope:
scope.clear_breadcrumbs()
# 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()

exc_info = sys.exc_info()
if exc_info and all(exc_info):
event, hint = event_from_exception(
exc_info,
client_options=client.options,
Expand Down Expand Up @@ -140,25 +135,39 @@ def __init__(self, timeout_warning=False):
@staticmethod
def setup_once():
# type: () -> None
import __main__ as lambda_bootstrap # type: ignore

pre_37 = True # Python 3.6 or 2.7

if not hasattr(lambda_bootstrap, "handle_http_request"):
try:
import bootstrap as lambda_bootstrap # type: ignore

pre_37 = False # Python 3.7
except ImportError:
pass
# Python 2.7: Everything is in `__main__`.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the crucial bit, where we do not attempt to import but rather see what's imported

#
# 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__)
#
# On 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']
#
# Such a setup would then make all monkeypatches useless.
if "bootstrap" in sys.modules:
lambda_bootstrap = sys.modules["bootstrap"] # type: Any
elif "__main__" in sys.modules:
lambda_bootstrap = sys.modules["__main__"]
else:
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"
"AwsLambdaIntegration disabled (could not find handle_event_request)"
)
return

pre_37 = hasattr(lambda_bootstrap, "handle_http_request") # Python 3.6 or 2.7

if pre_37:
old_handle_event_request = lambda_bootstrap.handle_event_request

Expand Down
148 changes: 148 additions & 0 deletions tests/integrations/aws_lambda/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import sys
import os
import shutil
import tempfile
import subprocess
import boto3
import uuid
import base64


def get_boto_client():
return boto3.client(
"lambda",
aws_access_key_id=os.environ["SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID"],
aws_secret_access_key=os.environ["SENTRY_PYTHON_TEST_AWS_SECRET_ACCESS_KEY"],
region_name="us-east-1",
)


def run_lambda_function(
client,
runtime,
code,
payload,
add_finalizer,
syntax_check=True,
timeout=30,
subprocess_kwargs=(),
):
subprocess_kwargs = dict(subprocess_kwargs)

with tempfile.TemporaryDirectory() as tmpdir:
test_lambda_py = os.path.join(tmpdir, "test_lambda.py")
with open(test_lambda_py, "w") as f:
f.write(code)

if syntax_check:
# 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, test_lambda_py])

setup_cfg = os.path.join(tmpdir, "setup.cfg")
with open(setup_cfg, "w") as f:
f.write("[install]\nprefix=")

subprocess.check_call(
[sys.executable, "setup.py", "sdist", "-d", os.path.join(tmpdir, "..")],
**subprocess_kwargs
)

# https://docs.aws.amazon.com/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html
subprocess.check_call(
"pip install ../*.tar.gz -t .", cwd=tmpdir, shell=True, **subprocess_kwargs
)
shutil.make_archive(os.path.join(tmpdir, "ball"), "zip", tmpdir)

fn_name = "test_function_{}".format(uuid.uuid4())

with open(os.path.join(tmpdir, "ball.zip"), "rb") as zip:
client.create_function(
FunctionName=fn_name,
Runtime=runtime,
Timeout=timeout,
Role=os.environ["SENTRY_PYTHON_TEST_AWS_IAM_ROLE"],
Handler="test_lambda.test_handler",
Code={"ZipFile": zip.read()},
Description="Created as part of testsuite for getsentry/sentry-python",
)

@add_finalizer
def delete_function():
client.delete_function(FunctionName=fn_name)

response = client.invoke(
FunctionName=fn_name,
InvocationType="RequestResponse",
LogType="Tail",
Payload=payload,
)

assert 200 <= response["StatusCode"] < 300, response
return response


_REPL_CODE = """
import os

def test_handler(event, context):
line = {line!r}
if line.startswith(">>> "):
exec(line[4:])
elif line.startswith("$ "):
os.system(line[2:])
else:
print("Start a line with $ or >>>")

return b""
"""

try:
import click
except ImportError:
pass
else:

@click.command()
@click.option(
"--runtime", required=True, help="name of the runtime to use, eg python3.8"
)
@click.option("--verbose", is_flag=True, default=False)
def repl(runtime, verbose):
"""
Launch a "REPL" against AWS Lambda to inspect their runtime.
"""

cleanup = []
client = get_boto_client()

print("Start a line with `$ ` to run shell commands, or `>>> ` to run Python")

while True:
line = input()

response = run_lambda_function(
client,
runtime,
_REPL_CODE.format(line=line),
b"",
cleanup.append,
subprocess_kwargs={
"stdout": subprocess.DEVNULL,
"stderr": subprocess.DEVNULL,
}
if not verbose
else {},
)

for line in base64.b64decode(response["LogResult"]).splitlines():
print(line.decode("utf8"))

for f in cleanup:
f()

cleanup = []

if __name__ == "__main__":
repl()
Loading