Skip to content

Commit ab0cd2c

Browse files
authored
fix(aws-lambda): Fix bug for initial handler path (getsentry#1139)
* fix(aws-lambda): Fix bug for initial handler path Adds support for long initial handler paths in the format of `x.y.z` and dir paths in the format of `x/y.z`
1 parent b065890 commit ab0cd2c

File tree

4 files changed

+105
-36
lines changed

4 files changed

+105
-36
lines changed

scripts/init_serverless_sdk.py

+47-8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
'sentry_sdk.integrations.init_serverless_sdk.sentry_lambda_handler'
77
"""
88
import os
9+
import sys
10+
import re
911

1012
import sentry_sdk
1113
from sentry_sdk._types import MYPY
@@ -23,16 +25,53 @@
2325
)
2426

2527

28+
class AWSLambdaModuleLoader:
29+
DIR_PATH_REGEX = r"^(.+)\/([^\/]+)$"
30+
31+
def __init__(self, sentry_initial_handler):
32+
try:
33+
module_path, self.handler_name = sentry_initial_handler.rsplit(".", 1)
34+
except ValueError:
35+
raise ValueError("Incorrect AWS Handler path (Not a path)")
36+
37+
self.extract_and_load_lambda_function_module(module_path)
38+
39+
def extract_and_load_lambda_function_module(self, module_path):
40+
"""
41+
Method that extracts and loads lambda function module from module_path
42+
"""
43+
py_version = sys.version_info
44+
45+
if re.match(self.DIR_PATH_REGEX, module_path):
46+
# With a path like -> `scheduler/scheduler/event`
47+
# `module_name` is `event`, and `module_file_path` is `scheduler/scheduler/event.py`
48+
module_name = module_path.split(os.path.sep)[-1]
49+
module_file_path = module_path + ".py"
50+
51+
# Supported python versions are 2.7, 3.6, 3.7, 3.8
52+
if py_version >= (3, 5):
53+
import importlib.util
54+
spec = importlib.util.spec_from_file_location(module_name, module_file_path)
55+
self.lambda_function_module = importlib.util.module_from_spec(spec)
56+
spec.loader.exec_module(self.lambda_function_module)
57+
elif py_version[0] < 3:
58+
import imp
59+
self.lambda_function_module = imp.load_source(module_name, module_file_path)
60+
else:
61+
raise ValueError("Python version %s is not supported." % py_version)
62+
else:
63+
import importlib
64+
self.lambda_function_module = importlib.import_module(module_path)
65+
66+
def get_lambda_handler(self):
67+
return getattr(self.lambda_function_module, self.handler_name)
68+
69+
2670
def sentry_lambda_handler(event, context):
2771
# type: (Any, Any) -> None
2872
"""
2973
Handler function that invokes a lambda handler which path is defined in
30-
environment vairables as "SENTRY_INITIAL_HANDLER"
74+
environment variables as "SENTRY_INITIAL_HANDLER"
3175
"""
32-
try:
33-
module_name, handler_name = os.environ["SENTRY_INITIAL_HANDLER"].rsplit(".", 1)
34-
except ValueError:
35-
raise ValueError("Incorrect AWS Handler path (Not a path)")
36-
lambda_function = __import__(module_name)
37-
lambda_handler = getattr(lambda_function, handler_name)
38-
return lambda_handler(event, context)
76+
module_loader = AWSLambdaModuleLoader(os.environ["SENTRY_INITIAL_HANDLER"])
77+
return module_loader.get_lambda_handler()(event, context)

tests/integrations/aws_lambda/client.py

+24-4
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def get_boto_client():
1818

1919

2020
def build_no_code_serverless_function_and_layer(
21-
client, tmpdir, fn_name, runtime, timeout
21+
client, tmpdir, fn_name, runtime, timeout, initial_handler
2222
):
2323
"""
2424
Util function that auto instruments the no code implementation of the python
@@ -45,7 +45,7 @@ def build_no_code_serverless_function_and_layer(
4545
Timeout=timeout,
4646
Environment={
4747
"Variables": {
48-
"SENTRY_INITIAL_HANDLER": "test_lambda.test_handler",
48+
"SENTRY_INITIAL_HANDLER": initial_handler,
4949
"SENTRY_DSN": "https://123abc@example.com/123",
5050
"SENTRY_TRACES_SAMPLE_RATE": "1.0",
5151
}
@@ -67,12 +67,27 @@ def run_lambda_function(
6767
syntax_check=True,
6868
timeout=30,
6969
layer=None,
70+
initial_handler=None,
7071
subprocess_kwargs=(),
7172
):
7273
subprocess_kwargs = dict(subprocess_kwargs)
7374

7475
with tempfile.TemporaryDirectory() as tmpdir:
75-
test_lambda_py = os.path.join(tmpdir, "test_lambda.py")
76+
if initial_handler:
77+
# If Initial handler value is provided i.e. it is not the default
78+
# `test_lambda.test_handler`, then create another dir level so that our path is
79+
# test_dir.test_lambda.test_handler
80+
test_dir_path = os.path.join(tmpdir, "test_dir")
81+
python_init_file = os.path.join(test_dir_path, "__init__.py")
82+
os.makedirs(test_dir_path)
83+
with open(python_init_file, "w"):
84+
# Create __init__ file to make it a python package
85+
pass
86+
87+
test_lambda_py = os.path.join(tmpdir, "test_dir", "test_lambda.py")
88+
else:
89+
test_lambda_py = os.path.join(tmpdir, "test_lambda.py")
90+
7691
with open(test_lambda_py, "w") as f:
7792
f.write(code)
7893

@@ -127,8 +142,13 @@ def run_lambda_function(
127142
cwd=tmpdir,
128143
check=True,
129144
)
145+
146+
# Default initial handler
147+
if not initial_handler:
148+
initial_handler = "test_lambda.test_handler"
149+
130150
build_no_code_serverless_function_and_layer(
131-
client, tmpdir, fn_name, runtime, timeout
151+
client, tmpdir, fn_name, runtime, timeout, initial_handler
132152
)
133153

134154
@add_finalizer

tests/integrations/aws_lambda/test_aws.py

+33-23
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,9 @@ def lambda_runtime(request):
112112

113113
@pytest.fixture
114114
def run_lambda_function(request, lambda_client, lambda_runtime):
115-
def inner(code, payload, timeout=30, syntax_check=True, layer=None):
115+
def inner(
116+
code, payload, timeout=30, syntax_check=True, layer=None, initial_handler=None
117+
):
116118
from tests.integrations.aws_lambda.client import run_lambda_function
117119

118120
response = run_lambda_function(
@@ -124,6 +126,7 @@ def inner(code, payload, timeout=30, syntax_check=True, layer=None):
124126
timeout=timeout,
125127
syntax_check=syntax_check,
126128
layer=layer,
129+
initial_handler=initial_handler,
127130
)
128131

129132
# for better debugging
@@ -621,32 +624,39 @@ def test_serverless_no_code_instrumentation(run_lambda_function):
621624
python sdk, with no code changes sentry is able to capture errors
622625
"""
623626

624-
_, _, response = run_lambda_function(
625-
dedent(
626-
"""
627-
import sentry_sdk
627+
for initial_handler in [
628+
None,
629+
"test_dir/test_lambda.test_handler",
630+
"test_dir.test_lambda.test_handler",
631+
]:
632+
print("Testing Initial Handler ", initial_handler)
633+
_, _, response = run_lambda_function(
634+
dedent(
635+
"""
636+
import sentry_sdk
628637
629-
def test_handler(event, context):
630-
current_client = sentry_sdk.Hub.current.client
638+
def test_handler(event, context):
639+
current_client = sentry_sdk.Hub.current.client
631640
632-
assert current_client is not None
641+
assert current_client is not None
633642
634-
assert len(current_client.options['integrations']) == 1
635-
assert isinstance(current_client.options['integrations'][0],
636-
sentry_sdk.integrations.aws_lambda.AwsLambdaIntegration)
643+
assert len(current_client.options['integrations']) == 1
644+
assert isinstance(current_client.options['integrations'][0],
645+
sentry_sdk.integrations.aws_lambda.AwsLambdaIntegration)
637646
638-
raise Exception("something went wrong")
639-
"""
640-
),
641-
b'{"foo": "bar"}',
642-
layer=True,
643-
)
644-
assert response["FunctionError"] == "Unhandled"
645-
assert response["StatusCode"] == 200
647+
raise Exception("something went wrong")
648+
"""
649+
),
650+
b'{"foo": "bar"}',
651+
layer=True,
652+
initial_handler=initial_handler,
653+
)
654+
assert response["FunctionError"] == "Unhandled"
655+
assert response["StatusCode"] == 200
646656

647-
assert response["Payload"]["errorType"] != "AssertionError"
657+
assert response["Payload"]["errorType"] != "AssertionError"
648658

649-
assert response["Payload"]["errorType"] == "Exception"
650-
assert response["Payload"]["errorMessage"] == "something went wrong"
659+
assert response["Payload"]["errorType"] == "Exception"
660+
assert response["Payload"]["errorMessage"] == "something went wrong"
651661

652-
assert "sentry_handler" in response["LogResult"][3].decode("utf-8")
662+
assert "sentry_handler" in response["LogResult"][3].decode("utf-8")

tests/integrations/django/myapp/settings.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def middleware(request):
157157

158158
USE_L10N = True
159159

160-
USE_TZ = True
160+
USE_TZ = False
161161

162162
TEMPLATE_DEBUG = True
163163

0 commit comments

Comments
 (0)