Skip to content

Commit 25125b5

Browse files
ahmedetefysentry-bot
and
sentry-bot
authored
feat(serverless): Python Serverless nocode instrumentation (getsentry#1004)
* Moved logic from aws_lambda.py to aws_lambda.__init__ * Added init function that revokes original handler * Added documentation * fix: Formatting * Added test definition for serverless no code instrumentation * TODO comments * Refactored AWSLambda Layer script and fixed missing dir bug * Removed redunant line * Organized import * Moved build-aws-layer script to integrations/aws_lambda * Added check if path fails * Renamed script to have underscore rather than dashes * Fixed naming change for calling script * Tests to ensure lambda check does not fail existing tests * Added dest abs path as an arg * Testing init script * Modifying tests to accomodate addtion of layer * Added test that ensures serverless auto instrumentation works as expected * Removed redundant test arg from sentry_sdk init in serverless init * Removed redundant todo statement * Refactored layer and function creation into its own function * Linting fixes * Linting fixes * Moved scripts from within sdk to scripts dir * Updated documentation * Pinned dependency to fix CI issue Co-authored-by: sentry-bot <markus+ghbot@sentry.io>
1 parent b48c285 commit 25125b5

File tree

7 files changed

+276
-107
lines changed

7 files changed

+276
-107
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,5 @@ apidocs-hotfix: apidocs
6363
aws-lambda-layer-build: dist
6464
$(VENV_PATH)/bin/pip install urllib3
6565
$(VENV_PATH)/bin/pip install certifi
66-
$(VENV_PATH)/bin/python -m scripts.build-awslambda-layer
66+
$(VENV_PATH)/bin/python -m scripts.build_awslambda_layer
6767
.PHONY: aws-lambda-layer-build

scripts/build-awslambda-layer.py

Lines changed: 0 additions & 77 deletions
This file was deleted.

scripts/build_awslambda_layer.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import os
2+
import subprocess
3+
import tempfile
4+
import shutil
5+
6+
from sentry_sdk.consts import VERSION as SDK_VERSION
7+
from sentry_sdk._types import MYPY
8+
9+
if MYPY:
10+
from typing import Union
11+
12+
13+
class PackageBuilder:
14+
def __init__(
15+
self,
16+
base_dir, # type: str
17+
pkg_parent_dir, # type: str
18+
dist_rel_path, # type: str
19+
):
20+
# type: (...) -> None
21+
self.base_dir = base_dir
22+
self.pkg_parent_dir = pkg_parent_dir
23+
self.dist_rel_path = dist_rel_path
24+
self.packages_dir = self.get_relative_path_of(pkg_parent_dir)
25+
26+
def make_directories(self):
27+
# type: (...) -> None
28+
os.makedirs(self.packages_dir)
29+
30+
def install_python_binaries(self):
31+
# type: (...) -> None
32+
wheels_filepath = os.path.join(
33+
self.dist_rel_path, f"sentry_sdk-{SDK_VERSION}-py2.py3-none-any.whl"
34+
)
35+
subprocess.run(
36+
[
37+
"pip",
38+
"install",
39+
"--no-cache-dir", # Disables the cache -> always accesses PyPI
40+
"-q", # Quiet
41+
wheels_filepath, # Copied to the target directory before installation
42+
"-t", # Target directory flag
43+
self.packages_dir,
44+
],
45+
check=True,
46+
)
47+
48+
def create_init_serverless_sdk_package(self):
49+
# type: (...) -> None
50+
"""
51+
Method that creates the init_serverless_sdk pkg in the
52+
sentry-python-serverless zip
53+
"""
54+
serverless_sdk_path = f'{self.packages_dir}/sentry_sdk/' \
55+
f'integrations/init_serverless_sdk'
56+
if not os.path.exists(serverless_sdk_path):
57+
os.makedirs(serverless_sdk_path)
58+
shutil.copy('scripts/init_serverless_sdk.py',
59+
f'{serverless_sdk_path}/__init__.py')
60+
61+
def zip(
62+
self, filename # type: str
63+
):
64+
# type: (...) -> None
65+
subprocess.run(
66+
[
67+
"zip",
68+
"-q", # Quiet
69+
"-x", # Exclude files
70+
"**/__pycache__/*", # Files to be excluded
71+
"-r", # Recurse paths
72+
filename, # Output filename
73+
self.pkg_parent_dir, # Files to be zipped
74+
],
75+
cwd=self.base_dir,
76+
check=True, # Raises CalledProcessError if exit status is non-zero
77+
)
78+
79+
def get_relative_path_of(
80+
self, subfile # type: str
81+
):
82+
# type: (...) -> str
83+
return os.path.join(self.base_dir, subfile)
84+
85+
86+
# Ref to `pkg_parent_dir` Top directory in the ZIP file.
87+
# Placing the Sentry package in `/python` avoids
88+
# creating a directory for a specific version. For more information, see
89+
# https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html#configuration-layers-path
90+
def build_packaged_zip(
91+
dist_rel_path="dist", # type: str
92+
dest_zip_filename=f"sentry-python-serverless-{SDK_VERSION}.zip", # type: str
93+
pkg_parent_dir="python", # type: str
94+
dest_abs_path=None, # type: Union[str, None]
95+
):
96+
# type: (...) -> None
97+
if dest_abs_path is None:
98+
dest_abs_path = os.path.abspath(
99+
os.path.join(os.path.dirname(__file__), "..", dist_rel_path)
100+
)
101+
with tempfile.TemporaryDirectory() as tmp_dir:
102+
package_builder = PackageBuilder(tmp_dir, pkg_parent_dir, dist_rel_path)
103+
package_builder.make_directories()
104+
package_builder.install_python_binaries()
105+
package_builder.create_init_serverless_sdk_package()
106+
package_builder.zip(dest_zip_filename)
107+
if not os.path.exists(dist_rel_path):
108+
os.makedirs(dist_rel_path)
109+
shutil.copy(
110+
package_builder.get_relative_path_of(dest_zip_filename), dest_abs_path
111+
)
112+
113+
114+
if __name__ == "__main__":
115+
build_packaged_zip()

scripts/init_serverless_sdk.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""
2+
For manual instrumentation,
3+
The Handler function string of an aws lambda function should be added as an
4+
environment variable with a key of 'INITIAL_HANDLER' along with the 'DSN'
5+
Then the Handler function sstring should be replaced with
6+
'sentry_sdk.integrations.init_serverless_sdk.sentry_lambda_handler'
7+
"""
8+
import os
9+
10+
import sentry_sdk
11+
from sentry_sdk._types import MYPY
12+
from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
13+
14+
if MYPY:
15+
from typing import Any
16+
17+
18+
# Configure Sentry SDK
19+
sentry_sdk.init(
20+
dsn=os.environ["DSN"],
21+
integrations=[AwsLambdaIntegration(timeout_warning=True)],
22+
)
23+
24+
25+
def sentry_lambda_handler(event, context):
26+
# type: (Any, Any) -> None
27+
"""
28+
Handler function that invokes a lambda handler which path is defined in
29+
environment vairables as "INITIAL_HANDLER"
30+
"""
31+
try:
32+
module_name, handler_name = os.environ["INITIAL_HANDLER"].rsplit(".", 1)
33+
except ValueError:
34+
raise ValueError("Incorrect AWS Handler path (Not a path)")
35+
lambda_function = __import__(module_name)
36+
lambda_handler = getattr(lambda_function, handler_name)
37+
lambda_handler(event, context)

tests/integrations/aws_lambda/client.py

Lines changed: 83 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,46 @@ def get_boto_client():
1717
)
1818

1919

20+
def build_no_code_serverless_function_and_layer(
21+
client, tmpdir, fn_name, runtime, timeout
22+
):
23+
"""
24+
Util function that auto instruments the no code implementation of the python
25+
sdk by creating a layer containing the Python-sdk, and then creating a func
26+
that uses that layer
27+
"""
28+
from scripts.build_awslambda_layer import (
29+
build_packaged_zip,
30+
)
31+
32+
build_packaged_zip(dest_abs_path=tmpdir, dest_zip_filename="serverless-ball.zip")
33+
34+
with open(os.path.join(tmpdir, "serverless-ball.zip"), "rb") as serverless_zip:
35+
response = client.publish_layer_version(
36+
LayerName="python-serverless-sdk-test",
37+
Description="Created as part of testsuite for getsentry/sentry-python",
38+
Content={"ZipFile": serverless_zip.read()},
39+
)
40+
41+
with open(os.path.join(tmpdir, "ball.zip"), "rb") as zip:
42+
client.create_function(
43+
FunctionName=fn_name,
44+
Runtime=runtime,
45+
Timeout=timeout,
46+
Environment={
47+
"Variables": {
48+
"INITIAL_HANDLER": "test_lambda.test_handler",
49+
"DSN": "https://123abc@example.com/123",
50+
}
51+
},
52+
Role=os.environ["SENTRY_PYTHON_TEST_AWS_IAM_ROLE"],
53+
Handler="sentry_sdk.integrations.init_serverless_sdk.sentry_lambda_handler",
54+
Layers=[response["LayerVersionArn"]],
55+
Code={"ZipFile": zip.read()},
56+
Description="Created as part of testsuite for getsentry/sentry-python",
57+
)
58+
59+
2060
def run_lambda_function(
2161
client,
2262
runtime,
@@ -25,6 +65,7 @@ def run_lambda_function(
2565
add_finalizer,
2666
syntax_check=True,
2767
timeout=30,
68+
layer=None,
2869
subprocess_kwargs=(),
2970
):
3071
subprocess_kwargs = dict(subprocess_kwargs)
@@ -40,39 +81,53 @@ def run_lambda_function(
4081
# such as chalice's)
4182
subprocess.check_call([sys.executable, test_lambda_py])
4283

43-
setup_cfg = os.path.join(tmpdir, "setup.cfg")
44-
with open(setup_cfg, "w") as f:
45-
f.write("[install]\nprefix=")
84+
fn_name = "test_function_{}".format(uuid.uuid4())
4685

47-
subprocess.check_call(
48-
[sys.executable, "setup.py", "sdist", "-d", os.path.join(tmpdir, "..")],
49-
**subprocess_kwargs
50-
)
86+
if layer is None:
87+
setup_cfg = os.path.join(tmpdir, "setup.cfg")
88+
with open(setup_cfg, "w") as f:
89+
f.write("[install]\nprefix=")
5190

52-
subprocess.check_call(
53-
"pip install mock==3.0.0 funcsigs -t .",
54-
cwd=tmpdir,
55-
shell=True,
56-
**subprocess_kwargs
57-
)
91+
subprocess.check_call(
92+
[sys.executable, "setup.py", "sdist", "-d", os.path.join(tmpdir, "..")],
93+
**subprocess_kwargs
94+
)
5895

59-
# https://docs.aws.amazon.com/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html
60-
subprocess.check_call(
61-
"pip install ../*.tar.gz -t .", cwd=tmpdir, shell=True, **subprocess_kwargs
62-
)
63-
shutil.make_archive(os.path.join(tmpdir, "ball"), "zip", tmpdir)
96+
subprocess.check_call(
97+
"pip install mock==3.0.0 funcsigs -t .",
98+
cwd=tmpdir,
99+
shell=True,
100+
**subprocess_kwargs
101+
)
64102

65-
fn_name = "test_function_{}".format(uuid.uuid4())
103+
# https://docs.aws.amazon.com/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html
104+
subprocess.check_call(
105+
"pip install ../*.tar.gz -t .",
106+
cwd=tmpdir,
107+
shell=True,
108+
**subprocess_kwargs
109+
)
66110

67-
with open(os.path.join(tmpdir, "ball.zip"), "rb") as zip:
68-
client.create_function(
69-
FunctionName=fn_name,
70-
Runtime=runtime,
71-
Timeout=timeout,
72-
Role=os.environ["SENTRY_PYTHON_TEST_AWS_IAM_ROLE"],
73-
Handler="test_lambda.test_handler",
74-
Code={"ZipFile": zip.read()},
75-
Description="Created as part of testsuite for getsentry/sentry-python",
111+
shutil.make_archive(os.path.join(tmpdir, "ball"), "zip", tmpdir)
112+
113+
with open(os.path.join(tmpdir, "ball.zip"), "rb") as zip:
114+
client.create_function(
115+
FunctionName=fn_name,
116+
Runtime=runtime,
117+
Timeout=timeout,
118+
Role=os.environ["SENTRY_PYTHON_TEST_AWS_IAM_ROLE"],
119+
Handler="test_lambda.test_handler",
120+
Code={"ZipFile": zip.read()},
121+
Description="Created as part of testsuite for getsentry/sentry-python",
122+
)
123+
else:
124+
subprocess.run(
125+
["zip", "-q", "-x", "**/__pycache__/*", "-r", "ball.zip", "./"],
126+
cwd=tmpdir,
127+
check=True,
128+
)
129+
build_no_code_serverless_function_and_layer(
130+
client, tmpdir, fn_name, runtime, timeout
76131
)
77132

78133
@add_finalizer

0 commit comments

Comments
 (0)