Skip to content

Commit 40839ab

Browse files
basepibeniwohli
andauthoredMay 9, 2023
Add lambda instrumentation + wrapper (#1796)
* Add lambda instrumentation + wrapper * Add new instrumentation to register * Add python path modification * Move elasticapm-run to elasticapm-run.py * Add new wrapper for lambda * execl the runtime * Move instrumentation to setup phase * Add explanation for import-time instrumentation * Move lambda wrapper stuff to contrib * Add NOTICE.md for lambda layer * Add make-distribution.sh (modified from Node-js version) * Pin the requirements * Add dependabot.yml * Rename elasticapm-lambda + add elasticapm-run to package * Add Dockerfile and put elasticapm-lambda in python/bin * Update elasticapm/contrib/serverless/aws_wrapper/elasticapm_handler.py Co-authored-by: Benjamin Wohlwend <[email protected]> --------- Co-authored-by: Benjamin Wohlwend <[email protected]>
1 parent dceda66 commit 40839ab

File tree

11 files changed

+436
-353
lines changed

11 files changed

+436
-353
lines changed
 

‎.github/dependabot.yml

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
version: 2
3+
updates:
4+
# Enable version updates for python
5+
- package-ecosystem: "pip"
6+
# Look for `requirements.txt` file in the `dev-utils` directory
7+
directory: "/dev-utils/"
8+
# Check for updates once a week
9+
schedule:
10+
interval: "weekly"
11+
reviewers:
12+
- "elastic/apm-agent-python"

‎Dockerfile

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Pin to Alpine 3.17.3
2+
FROM alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126
3+
ARG AGENT_DIR
4+
COPY ${AGENT_DIR} /opt/python

‎LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
BSD 3-Clause License
22

33
Copyright (c) 2009-2012, David Cramer and individual contributors
4-
Copyright (c) 2013-2018, Elasticsearch BV
4+
Copyright (c) 2013-2023, Elasticsearch BV
55
All rights reserved.
66

77
Redistribution and use in source and binary forms, with or without

‎NOTICE.md

+67-337
Large diffs are not rendered by default.

‎dev-utils/make-distribution.sh

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/bin/bash
2+
#
3+
# Make a Python APM agent distribution that is used as follows:
4+
# - "build/dist/elastic-apm-python-lambda-layer.zip" is published to AWS as a
5+
# Lambda layer (https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html)
6+
# - "build/dist/package/python/..." is used to build a Docker image of the APM agent
7+
#
8+
9+
if [ "$TRACE" != "" ]; then
10+
export PS4='${BASH_SOURCE}:${LINENO}: ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
11+
set -o xtrace
12+
fi
13+
set -o errexit
14+
set -o pipefail
15+
16+
# ---- support functions
17+
18+
function fatal {
19+
echo "$(basename $0): error: $*"
20+
exit 1
21+
}
22+
23+
# ---- mainline
24+
25+
TOP=$(cd $(dirname $0)/../ >/dev/null; pwd)
26+
BUILD_DIR="$TOP/build/dist"
27+
28+
if ! command -v pip >/dev/null 2>&1; then
29+
fatal "pip is unavailable"
30+
fi
31+
32+
rm -rf "$BUILD_DIR"
33+
mkdir -p "$BUILD_DIR"
34+
cd "$BUILD_DIR"
35+
36+
rm -f elastic-apm-python-lambda-layer.zip
37+
rm -f requirements.txt
38+
rm -rf package
39+
mkdir package
40+
cp "$TOP/dev-utils/requirements.txt" .
41+
echo "$TOP" >> ./requirements.txt
42+
pip install -r requirements.txt --target ./package/python
43+
cd package
44+
cp python/elasticapm/contrib/serverless/aws_wrapper/elasticapm-lambda.py ./python/bin/elasticapm-lambda
45+
chmod +x ./python/bin/elasticapm-lambda
46+
cp python/elasticapm/contrib/serverless/aws_wrapper/elasticapm_handler.py ./python/
47+
cp "$TOP/elasticapm/contrib/serverless/aws_wrapper/NOTICE.md" ./python/
48+
49+
echo ""
50+
zip -q -r ../elastic-apm-python-lambda-layer.zip .
51+
echo "Created build/dist/elastic-apm-python-lambda-layer.zip"
52+
53+
cd ..
54+
55+
56+
echo
57+
echo "The lambda layer can be published as follows for dev work:"
58+
echo " aws lambda --output json publish-layer-version --layer-name '$USER-dev-elastic-apm-python' --description '$USER dev Elastic APM Python agent lambda layer' --zip-file 'fileb://build/dist/elastic-apm-python-lambda-layer.zip'"

‎dev-utils/requirements.txt

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# These are the pinned requirements for the lambda layer/docker image
2+
certifi==2022.12.7
3+
urllib3==1.26.15
4+
wrapt==1.15.0

‎elasticapm/contrib/serverless/aws.py

+24-15
Original file line numberDiff line numberDiff line change
@@ -85,19 +85,7 @@ def handler(event, context):
8585

8686
name = kwargs.pop("name", None)
8787

88-
# Disable all background threads except for transport
89-
kwargs["metrics_interval"] = "0ms"
90-
kwargs["breakdown_metrics"] = False
91-
if "metrics_sets" not in kwargs and "ELASTIC_APM_METRICS_SETS" not in os.environ:
92-
# Allow users to override metrics sets
93-
kwargs["metrics_sets"] = []
94-
kwargs["central_config"] = False
95-
kwargs["cloud_provider"] = "none"
96-
kwargs["framework_name"] = "AWS Lambda"
97-
if "service_name" not in kwargs and "ELASTIC_APM_SERVICE_NAME" not in os.environ:
98-
kwargs["service_name"] = os.environ["AWS_LAMBDA_FUNCTION_NAME"]
99-
if "service_version" not in kwargs and "ELASTIC_APM_SERVICE_VERSION" not in os.environ:
100-
kwargs["service_version"] = os.environ.get("AWS_LAMBDA_FUNCTION_VERSION")
88+
kwargs = prep_kwargs(kwargs)
10189

10290
global INSTRUMENTED
10391
client = get_client()
@@ -109,9 +97,9 @@ def handler(event, context):
10997

11098
@functools.wraps(func)
11199
def decorated(*args, **kwds):
112-
if len(args) == 2:
100+
if len(args) >= 2:
113101
# Saving these for request context later
114-
event, context = args
102+
event, context = args[0:2]
115103
else:
116104
event, context = {}, {}
117105

@@ -125,6 +113,27 @@ def decorated(*args, **kwds):
125113
return decorated
126114

127115

116+
def prep_kwargs(kwargs=None):
117+
if kwargs is None:
118+
kwargs = {}
119+
120+
# Disable all background threads except for transport
121+
kwargs["metrics_interval"] = "0ms"
122+
kwargs["breakdown_metrics"] = False
123+
if "metrics_sets" not in kwargs and "ELASTIC_APM_METRICS_SETS" not in os.environ:
124+
# Allow users to override metrics sets
125+
kwargs["metrics_sets"] = []
126+
kwargs["central_config"] = False
127+
kwargs["cloud_provider"] = "none"
128+
kwargs["framework_name"] = "AWS Lambda"
129+
if "service_name" not in kwargs and "ELASTIC_APM_SERVICE_NAME" not in os.environ:
130+
kwargs["service_name"] = os.environ["AWS_LAMBDA_FUNCTION_NAME"]
131+
if "service_version" not in kwargs and "ELASTIC_APM_SERVICE_VERSION" not in os.environ:
132+
kwargs["service_version"] = os.environ.get("AWS_LAMBDA_FUNCTION_VERSION")
133+
134+
return kwargs
135+
136+
128137
class _lambda_transaction(object):
129138
"""
130139
Context manager for creating transactions around AWS Lambda functions.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
apm-agent-python Copyright 2013-2023 Elasticsearch BV
2+
3+
# Notice
4+
5+
This lambda layer contains several dependencies which have been vendored.
6+
7+
## urllib3
8+
9+
- **author:** Andrey Petrov
10+
- **project url:** https://github.com/urllib3/urllib3
11+
- **license:** MIT License, https://opensource.org/licenses/MIT
12+
13+
MIT License
14+
15+
Copyright (c) 2008-2019 Andrey Petrov and contributors (see CONTRIBUTORS.txt)
16+
17+
Permission is hereby granted, free of charge, to any person obtaining a copy
18+
of this software and associated documentation files (the "Software"), to deal
19+
in the Software without restriction, including without limitation the rights
20+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
21+
copies of the Software, and to permit persons to whom the Software is
22+
furnished to do so, subject to the following conditions:
23+
24+
The above copyright notice and this permission notice shall be included in all
25+
copies or substantial portions of the Software.
26+
27+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
33+
SOFTWARE.
34+
35+
## certifi
36+
37+
- **author:** Kenneth Reitz
38+
- **project url:** https://github.com/certifi/python-certifi
39+
- **license:** Mozilla Public License 2.0, https://opensource.org/licenses/MPL-2.0
40+
41+
This packge contains a modified version of ca-bundle.crt:
42+
43+
ca-bundle.crt -- Bundle of CA Root Certificates
44+
45+
Certificate data from Mozilla as of: Thu Nov 3 19:04:19 2011#
46+
This is a bundle of X.509 certificates of public Certificate Authorities
47+
(CA). These were automatically extracted from Mozilla's root certificates
48+
file (certdata.txt). This file can be found in the mozilla source tree:
49+
http://mxr.mozilla.org/mozilla/source/security/nss/lib/ckfw/builtins/certdata.txt?raw=1#
50+
It contains the certificates in PEM format and therefore
51+
can be directly used with curl / libcurl / php_curl, or with
52+
an Apache+mod_ssl webserver for SSL client authentication.
53+
Just configure this file as the SSLCACertificateFile.#
54+
55+
***** BEGIN LICENSE BLOCK *****
56+
This Source Code Form is subject to the terms of the Mozilla Public License,
57+
v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain
58+
one at http://mozilla.org/MPL/2.0/.
59+
60+
***** END LICENSE BLOCK *****
61+
@(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $
62+
63+
## wrapt
64+
65+
- **author:** Graham Dumpleton
66+
- **project url:** https://github.com/GrahamDumpleton/wrapt
67+
- **license:** BSD-2-Clause, http://opensource.org/licenses/BSD-2-Clause
68+
69+
Copyright (c) 2013, Graham Dumpleton
70+
All rights reserved.
71+
72+
Redistribution and use in source and binary forms, with or without
73+
modification, are permitted provided that the following conditions are met:
74+
75+
* Redistributions of source code must retain the above copyright notice, this
76+
list of conditions and the following disclaimer.
77+
78+
* Redistributions in binary form must reproduce the above copyright notice,
79+
this list of conditions and the following disclaimer in the documentation
80+
and/or other materials provided with the distribution.
81+
82+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
83+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
84+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
85+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
86+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
87+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
88+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
89+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
90+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
91+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
92+
POSSIBILITY OF SUCH DAMAGE.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# BSD 3-Clause License
2+
#
3+
# Copyright (c) 2023, Elasticsearch BV
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
#
9+
# * Redistributions of source code must retain the above copyright notice, this
10+
# list of conditions and the following disclaimer.
11+
#
12+
# * Redistributions in binary form must reproduce the above copyright notice,
13+
# this list of conditions and the following disclaimer in the documentation
14+
# and/or other materials provided with the distribution.
15+
#
16+
# * Neither the name of the copyright holder nor the names of its
17+
# contributors may be used to endorse or promote products derived from
18+
# this software without specific prior written permission.
19+
#
20+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
4+
# BSD 3-Clause License
5+
#
6+
# Copyright (c) 2023, Elasticsearch BV
7+
# All rights reserved.
8+
#
9+
# Redistribution and use in source and binary forms, with or without
10+
# modification, are permitted provided that the following conditions are met:
11+
#
12+
# * Redistributions of source code must retain the above copyright notice, this
13+
# list of conditions and the following disclaimer.
14+
#
15+
# * Redistributions in binary form must reproduce the above copyright notice,
16+
# this list of conditions and the following disclaimer in the documentation
17+
# and/or other materials provided with the distribution.
18+
#
19+
# * Neither the name of the copyright holder nor the names of its
20+
# contributors may be used to endorse or promote products derived from
21+
# this software without specific prior written permission.
22+
#
23+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
27+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
29+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33+
34+
import os
35+
import shutil
36+
import sys
37+
38+
39+
class LambdaError(Exception):
40+
pass
41+
42+
43+
if __name__ == "__main__":
44+
original_handler = os.environ.get("_HANDLER", None)
45+
if not original_handler:
46+
raise LambdaError("Cannot find original handler. _HANDLER is not set correctly.")
47+
48+
# AWS Lambda's `/var/runtime/bootstrap.py` uses `imp.load_module` to load
49+
# the handler from "_HANDLER". This means that the handler will be reloaded
50+
# (even if it's already been loaded), and any instrumentation that we do
51+
# will be lost. Thus, we can't use our normal wrapper script, and must
52+
# replace the handler altogether and wrap it manually.
53+
os.environ["ELASTICAPM_ORIGINAL_HANDLER"] = original_handler
54+
os.environ["_HANDLER"] = "elasticapm_handler.lambda_handler"
55+
56+
# Invoke the runtime
57+
args = sys.argv[1:]
58+
runtime = shutil.which(args[0])
59+
args = args[1:]
60+
os.execl(runtime, runtime, *args)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# BSD 3-Clause License
2+
#
3+
# Copyright (c) 2023, Elasticsearch BV
4+
# All rights reserved.
5+
#
6+
# Redistribution and use in source and binary forms, with or without
7+
# modification, are permitted provided that the following conditions are met:
8+
#
9+
# * Redistributions of source code must retain the above copyright notice, this
10+
# list of conditions and the following disclaimer.
11+
#
12+
# * Redistributions in binary form must reproduce the above copyright notice,
13+
# this list of conditions and the following disclaimer in the documentation
14+
# and/or other materials provided with the distribution.
15+
#
16+
# * Neither the name of the copyright holder nor the names of its
17+
# contributors may be used to endorse or promote products derived from
18+
# this software without specific prior written permission.
19+
#
20+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
31+
import os
32+
from importlib import import_module
33+
34+
import elasticapm
35+
from elasticapm import Client, get_client
36+
from elasticapm.contrib.serverless.aws import _lambda_transaction, prep_kwargs
37+
from elasticapm.utils.logging import get_logger
38+
39+
logger = get_logger("elasticapm.lambda")
40+
41+
# Prep client and instrument
42+
# For some reason, if we instrument as part of our handler, it adds 3+ seconds
43+
# to the cold start time. So we do it here. I still don't know why this slowdown
44+
# happens.
45+
client_kwargs = prep_kwargs()
46+
client = get_client()
47+
if not client:
48+
client = Client(**client_kwargs)
49+
client.activation_method = "wrapper"
50+
if not client.config.debug and client.config.instrument and client.config.enabled:
51+
elasticapm.instrument()
52+
53+
54+
class LambdaError(Exception):
55+
pass
56+
57+
58+
def lambda_handler(event, context):
59+
"""
60+
This handler is designed to replace the default handler in the lambda
61+
function, and then call the actual handler, which will be stored in
62+
ELASTICAPM_ORIGINAL_HANDLER.
63+
"""
64+
# Prep original handler
65+
original_handler = os.environ.get("ELASTICAPM_ORIGINAL_HANDLER", None)
66+
if not original_handler:
67+
raise LambdaError("Cannot find original handler. ELASTICAPM_ORIGINAL_HANDLER is not set correctly.")
68+
try:
69+
module, handler = original_handler.rsplit(".", 1)
70+
except ValueError:
71+
raise LambdaError(f"ELASTICAPM_ORIGINAL_HANDLER is not set correctly: {original_handler}")
72+
73+
# Import handler
74+
module = import_module(module.replace("/", "."))
75+
wrapped = getattr(module, handler)
76+
77+
client = get_client()
78+
79+
# Run the handler
80+
if not client.config.debug and client.config.instrument and client.config.enabled:
81+
with _lambda_transaction(wrapped, None, client, event, context) as sls:
82+
sls.response = wrapped(event, context)
83+
return sls.response
84+
else:
85+
return wrapped(event, context)

0 commit comments

Comments
 (0)
Please sign in to comment.