Skip to content

Commit fc3f747

Browse files
authored
fix: Fix AWS Lambda under Python 3.8 and refactor test setup code (#766)
Fix #764
1 parent 90e2509 commit fc3f747

File tree

3 files changed

+244
-104
lines changed

3 files changed

+244
-104
lines changed

sentry_sdk/integrations/aws_lambda.py

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from datetime import datetime, timedelta
22
from os import environ
33
import sys
4-
import json
54

65
from sentry_sdk.hub import Hub, _should_send_default_pii
76
from sentry_sdk._compat import reraise
@@ -42,19 +41,15 @@ def sentry_init_error(*args, **kwargs):
4241
if integration is None:
4342
return init_error(*args, **kwargs)
4443

45-
# Fetch Initialization error details from arguments
46-
error = json.loads(args[1])
47-
4844
# If an integration is there, a client has to be there.
4945
client = hub.client # type: Any
5046

51-
with hub.push_scope() as scope:
52-
with capture_internal_exceptions():
47+
with capture_internal_exceptions():
48+
with hub.configure_scope() as scope:
5349
scope.clear_breadcrumbs()
54-
# Checking if there is any error/exception which is raised in the runtime
55-
# environment from arguments and, re-raising it to capture it as an event.
56-
if error.get("errorType"):
57-
exc_info = sys.exc_info()
50+
51+
exc_info = sys.exc_info()
52+
if exc_info and all(exc_info):
5853
event, hint = event_from_exception(
5954
exc_info,
6055
client_options=client.options,
@@ -140,25 +135,39 @@ def __init__(self, timeout_warning=False):
140135
@staticmethod
141136
def setup_once():
142137
# type: () -> None
143-
import __main__ as lambda_bootstrap # type: ignore
144-
145-
pre_37 = True # Python 3.6 or 2.7
146-
147-
if not hasattr(lambda_bootstrap, "handle_http_request"):
148-
try:
149-
import bootstrap as lambda_bootstrap # type: ignore
150138

151-
pre_37 = False # Python 3.7
152-
except ImportError:
153-
pass
139+
# Python 2.7: Everything is in `__main__`.
140+
#
141+
# Python 3.7: If the bootstrap module is *already imported*, it is the
142+
# one we actually want to use (no idea what's in __main__)
143+
#
144+
# On Python 3.8 bootstrap is also importable, but will be the same file
145+
# as __main__ imported under a different name:
146+
#
147+
# sys.modules['__main__'].__file__ == sys.modules['bootstrap'].__file__
148+
# sys.modules['__main__'] is not sys.modules['bootstrap']
149+
#
150+
# Such a setup would then make all monkeypatches useless.
151+
if "bootstrap" in sys.modules:
152+
lambda_bootstrap = sys.modules["bootstrap"] # type: Any
153+
elif "__main__" in sys.modules:
154+
lambda_bootstrap = sys.modules["__main__"]
155+
else:
156+
logger.warning(
157+
"Not running in AWS Lambda environment, "
158+
"AwsLambdaIntegration disabled (could not find bootstrap module)"
159+
)
160+
return
154161

155162
if not hasattr(lambda_bootstrap, "handle_event_request"):
156163
logger.warning(
157164
"Not running in AWS Lambda environment, "
158-
"AwsLambdaIntegration disabled"
165+
"AwsLambdaIntegration disabled (could not find handle_event_request)"
159166
)
160167
return
161168

169+
pre_37 = hasattr(lambda_bootstrap, "handle_http_request") # Python 3.6 or 2.7
170+
162171
if pre_37:
163172
old_handle_event_request = lambda_bootstrap.handle_event_request
164173

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import sys
2+
import os
3+
import shutil
4+
import tempfile
5+
import subprocess
6+
import boto3
7+
import uuid
8+
import base64
9+
10+
11+
def get_boto_client():
12+
return boto3.client(
13+
"lambda",
14+
aws_access_key_id=os.environ["SENTRY_PYTHON_TEST_AWS_ACCESS_KEY_ID"],
15+
aws_secret_access_key=os.environ["SENTRY_PYTHON_TEST_AWS_SECRET_ACCESS_KEY"],
16+
region_name="us-east-1",
17+
)
18+
19+
20+
def run_lambda_function(
21+
client,
22+
runtime,
23+
code,
24+
payload,
25+
add_finalizer,
26+
syntax_check=True,
27+
timeout=30,
28+
subprocess_kwargs=(),
29+
):
30+
subprocess_kwargs = dict(subprocess_kwargs)
31+
32+
with tempfile.TemporaryDirectory() as tmpdir:
33+
test_lambda_py = os.path.join(tmpdir, "test_lambda.py")
34+
with open(test_lambda_py, "w") as f:
35+
f.write(code)
36+
37+
if syntax_check:
38+
# Check file for valid syntax first, and that the integration does not
39+
# crash when not running in Lambda (but rather a local deployment tool
40+
# such as chalice's)
41+
subprocess.check_call([sys.executable, test_lambda_py])
42+
43+
setup_cfg = os.path.join(tmpdir, "setup.cfg")
44+
with open(setup_cfg, "w") as f:
45+
f.write("[install]\nprefix=")
46+
47+
subprocess.check_call(
48+
[sys.executable, "setup.py", "sdist", "-d", os.path.join(tmpdir, "..")],
49+
**subprocess_kwargs
50+
)
51+
52+
# https://docs.aws.amazon.com/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html
53+
subprocess.check_call(
54+
"pip install ../*.tar.gz -t .", cwd=tmpdir, shell=True, **subprocess_kwargs
55+
)
56+
shutil.make_archive(os.path.join(tmpdir, "ball"), "zip", tmpdir)
57+
58+
fn_name = "test_function_{}".format(uuid.uuid4())
59+
60+
with open(os.path.join(tmpdir, "ball.zip"), "rb") as zip:
61+
client.create_function(
62+
FunctionName=fn_name,
63+
Runtime=runtime,
64+
Timeout=timeout,
65+
Role=os.environ["SENTRY_PYTHON_TEST_AWS_IAM_ROLE"],
66+
Handler="test_lambda.test_handler",
67+
Code={"ZipFile": zip.read()},
68+
Description="Created as part of testsuite for getsentry/sentry-python",
69+
)
70+
71+
@add_finalizer
72+
def delete_function():
73+
client.delete_function(FunctionName=fn_name)
74+
75+
response = client.invoke(
76+
FunctionName=fn_name,
77+
InvocationType="RequestResponse",
78+
LogType="Tail",
79+
Payload=payload,
80+
)
81+
82+
assert 200 <= response["StatusCode"] < 300, response
83+
return response
84+
85+
86+
_REPL_CODE = """
87+
import os
88+
89+
def test_handler(event, context):
90+
line = {line!r}
91+
if line.startswith(">>> "):
92+
exec(line[4:])
93+
elif line.startswith("$ "):
94+
os.system(line[2:])
95+
else:
96+
print("Start a line with $ or >>>")
97+
98+
return b""
99+
"""
100+
101+
try:
102+
import click
103+
except ImportError:
104+
pass
105+
else:
106+
107+
@click.command()
108+
@click.option(
109+
"--runtime", required=True, help="name of the runtime to use, eg python3.8"
110+
)
111+
@click.option("--verbose", is_flag=True, default=False)
112+
def repl(runtime, verbose):
113+
"""
114+
Launch a "REPL" against AWS Lambda to inspect their runtime.
115+
"""
116+
117+
cleanup = []
118+
client = get_boto_client()
119+
120+
print("Start a line with `$ ` to run shell commands, or `>>> ` to run Python")
121+
122+
while True:
123+
line = input()
124+
125+
response = run_lambda_function(
126+
client,
127+
runtime,
128+
_REPL_CODE.format(line=line),
129+
b"",
130+
cleanup.append,
131+
subprocess_kwargs={
132+
"stdout": subprocess.DEVNULL,
133+
"stderr": subprocess.DEVNULL,
134+
}
135+
if not verbose
136+
else {},
137+
)
138+
139+
for line in base64.b64decode(response["LogResult"]).splitlines():
140+
print(line.decode("utf8"))
141+
142+
for f in cleanup:
143+
f()
144+
145+
cleanup = []
146+
147+
if __name__ == "__main__":
148+
repl()

0 commit comments

Comments
 (0)