Skip to content

Commit f2b642e

Browse files
authored
Support aws opentelemetry distro in Lambda Python Runtimes (#241)
*Issue #, if available:* *Description of changes:* Support aws opentelemetry distro in AWS Lambda Python Runtimes. The PR contains: * Build aws opentelemetry distro Lambda python layer Run `build-lambda-layer.sh` in `aws-otel-python-instrumentation/lambda-layer/src` * Unit test for aws opentelemetry distro Lambda python layer Run `tox` in `aws-otel-python-instrumentation/lambda-layer/src` * Deploy the aws opentelemetry distro Lambda python layer with an Application Signals enabled Sample App in personal AWS account. Run `build.sh` in `aws-otel-python-instrumentation/lambda-layer` By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 90f7fa0 commit f2b642e

File tree

19 files changed

+813
-1
lines changed

19 files changed

+813
-1
lines changed

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/aws_opentelemetry_configurator.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,8 @@ def _customize_exporter(span_exporter: SpanExporter, resource: Resource) -> Span
250250
if not _is_application_signals_enabled():
251251
return span_exporter
252252
if _is_lambda_environment():
253-
return AwsMetricAttributesSpanExporterBuilder(OTLPUdpSpanExporter(), resource).build()
253+
traces_endpoint = os.environ.get(AWS_XRAY_DAEMON_ADDRESS_CONFIG, "127.0.0.1:2000")
254+
return AwsMetricAttributesSpanExporterBuilder(OTLPUdpSpanExporter(endpoint=traces_endpoint), resource).build()
254255
return AwsMetricAttributesSpanExporterBuilder(span_exporter, resource).build()
255256

256257

lambda-layer/build.sh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/bin/bash
2+
set -e
3+
4+
pushd src || exit
5+
rm -rf build
6+
./build-lambda-layer.sh
7+
popd || exit
8+
9+
pushd sample-apps || exit
10+
rm -rf build
11+
./package-lambda-function.sh
12+
popd || exit
13+
14+
pushd terraform/lambda || exit
15+
terraform init
16+
terraform apply -auto-approve
17+
popd || exit
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import json
2+
import os
3+
4+
import boto3
5+
import requests
6+
7+
client = boto3.client("s3")
8+
9+
10+
# lambda function
11+
def lambda_handler(event, context):
12+
13+
requests.get("https://aws.amazon.com/")
14+
15+
client.list_buckets()
16+
17+
return {"statusCode": 200, "body": json.dumps(os.environ.get("_X_AMZN_TRACE_ID"))}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
requests
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/sh
2+
set -e
3+
4+
mkdir -p build/python
5+
python3 -m pip install -r function/requirements.txt -t build/python
6+
cp function/lambda_function.py build/python
7+
cd build/python
8+
zip -r ../function.zip ./*

lambda-layer/src/Dockerfile

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
ARG runtime=python3.12
2+
3+
FROM public.ecr.aws/sam/build-${runtime}
4+
5+
ADD . /workspace
6+
7+
WORKDIR /workspace
8+
9+
RUN mkdir -p /build && \
10+
python3 -m pip install aws-opentelemetry-distro/ -t /build/python && \
11+
mv otel_wrapper.py /build/python && \
12+
mv otel-instrument /build && \
13+
chmod 755 /build/otel-instrument && \
14+
rm -rf /build/python/boto* && \
15+
rm -rf /build/python/urllib3* && \
16+
cd /build && \
17+
zip -r aws-opentelemetry-python-layer.zip otel-instrument python
18+
19+
CMD ["cp", "/build/aws-opentelemetry-python-layer.zip", "/out/aws-opentelemetry-python-layer.zip"]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/bin/sh
2+
set -e
3+
4+
rm -rf build
5+
rm -rf ./aws-opentelemetry-distro
6+
cp -r ../../aws-opentelemetry-distro ./
7+
mkdir -p build
8+
docker build --progress plain -t aws-opentelemetry-python-layer .
9+
docker run --rm -v "$(pwd)/build:/out" aws-opentelemetry-python-layer
10+
rm -rf ./aws-opentelemetry-distro

lambda-layer/src/otel-instrument

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#!/bin/bash
2+
3+
set -ef -o pipefail
4+
5+
# Copyright The OpenTelemetry Authors
6+
#
7+
# Licensed under the Apache License, Version 2.0 (the "License");
8+
# you may not use this file except in compliance with the License.
9+
# You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
19+
: <<'END_DOCUMENTATION'
20+
`otel-instrument`
21+
22+
This script configures and sets up OpenTelemetry Python with the values we
23+
expect will be used by the common user. It does this by setting the environment
24+
variables OpenTelemetry uses, and then initializing OpenTelemetry using the
25+
`opentelemetry-instrument` auto instrumentation script from the
26+
`opentelemetry-instrumentation` package.
27+
28+
Additionally, this configuration assumes the user is using packages conforming
29+
to the `opentelemetry-instrumentation` and `opentelemetry-sdk` specifications.
30+
31+
DO NOT use this script for anything else besides SETTING ENVIRONMENT VARIABLES.
32+
33+
See more:
34+
https://docs.aws.amazon.com/lambda/latest/dg/runtimes-modify.html#runtime-wrapper
35+
36+
Usage
37+
-----
38+
We expect this file to be at the root of a Lambda Layer. Having it anywhere else
39+
seems to mean AWS Lambda cannot find it.
40+
41+
In the configuration of an AWS Lambda function with this file at the
42+
root level of a Lambda Layer:
43+
44+
.. code::
45+
46+
AWS_LAMBDA_EXEC_WRAPPER = /opt/otel-instrument
47+
48+
END_DOCUMENTATION
49+
50+
# Use constants to access the environment variables we want to use in this
51+
# script.
52+
53+
# See more:
54+
# https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime
55+
56+
# - Reserved environment variables
57+
58+
# - - $AWS_LAMBDA_FUNCTION_NAME
59+
# - - $LAMBDA_RUNTIME_DIR
60+
61+
# - Unreserved environment variables
62+
63+
# - - $PYTHONPATH
64+
65+
# Update the python paths for packages with `sys.path` and `PYTHONPATH`
66+
67+
# - We know that the path to the Lambda Layer OpenTelemetry Python packages are
68+
# well defined, so we can add them to the PYTHONPATH.
69+
#
70+
# See more:
71+
# https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html#configuration-layers-path
72+
73+
export LAMBDA_LAYER_PKGS_DIR="/opt/python";
74+
75+
# - Set Lambda Layer python packages in PYTHONPATH so `opentelemetry-instrument`
76+
# script can find them (it needs to find `opentelemetry` to find the auto
77+
# instrumentation `run()` method later)
78+
79+
export PYTHONPATH="$LAMBDA_LAYER_PKGS_DIR:$PYTHONPATH";
80+
81+
# - Set Lambda runtime python packages in PYTHONPATH so
82+
# `opentelemetry-instrument` script can find them during auto instrumentation
83+
# and instrument them.
84+
85+
export PYTHONPATH="$LAMBDA_RUNTIME_DIR:$PYTHONPATH";
86+
87+
# Configure OpenTelemetry Python with environment variables
88+
89+
# - We leave `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` to its default. This is
90+
# `http://localhost:4318/v1/traces` because we are using the HTTP exporter
91+
92+
# - If OTEL_EXPORTER_OTLP_PROTOCOL is not set by user, the default exporting protocol is http/protobuf.
93+
if [ -z "${OTEL_EXPORTER_OTLP_PROTOCOL}" ]; then
94+
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
95+
fi
96+
97+
# - Set the service name
98+
99+
if [ -z "${OTEL_SERVICE_NAME}" ]; then
100+
export OTEL_SERVICE_NAME=$AWS_LAMBDA_FUNCTION_NAME;
101+
fi
102+
103+
# - Set the propagators
104+
105+
if [[ -z "$OTEL_PROPAGATORS" ]]; then
106+
export OTEL_PROPAGATORS="tracecontext,baggage,xray"
107+
fi
108+
109+
export LAMBDA_RESOURCE_ATTRIBUTES="cloud.region=$AWS_REGION,cloud.provider=aws,faas.name=$AWS_LAMBDA_FUNCTION_NAME,faas.version=$AWS_LAMBDA_FUNCTION_VERSION,faas.instance=$AWS_LAMBDA_LOG_STREAM_NAME,aws.log.group.names=$AWS_LAMBDA_LOG_GROUP_NAME";
110+
111+
# - If Application Signals is enabled
112+
113+
if [ "${OTEL_AWS_APPLICATION_SIGNALS_ENABLED}" = "true" ]; then
114+
export OTEL_PYTHON_DISTRO="aws_distro";
115+
export OTEL_PYTHON_CONFIGURATOR="aws_configurator";
116+
export OTEL_METRICS_EXPORTER="none";
117+
export OTEL_LOGS_EXPORTER="none";
118+
fi
119+
120+
if [ -z "${OTEL_RESOURCE_ATTRIBUTES}" ]; then
121+
export OTEL_RESOURCE_ATTRIBUTES=$LAMBDA_RESOURCE_ATTRIBUTES;
122+
else
123+
export OTEL_RESOURCE_ATTRIBUTES="$LAMBDA_RESOURCE_ATTRIBUTES,$OTEL_RESOURCE_ATTRIBUTES";
124+
fi
125+
126+
# - Enable botocore instrumentation by default
127+
128+
if [ -z ${OTEL_PYTHON_DISABLED_INSTRUMENTATIONS} ]; then
129+
export OTEL_PYTHON_DISABLED_INSTRUMENTATIONS="aio-pika,aiohttp-client,aiopg,asgi,asyncpg,boto3sqs,boto,cassandra,celery,confluent-kafka,dbapi,django,elasticsearch,fastapi,falcon,flask,grpc,httpx,jinja2,kafka-python,logging,mysql,mysqlclient,pika,psycopg2,pymemcache,pymongo,pymysql,pyramid,redis,remoulade,requests,sklearn,sqlalchemy,sqlite3,starlette,system-metrics,tornado,tortoiseorm,urllib,urllib3,wsgi"
130+
fi
131+
132+
# - Use a wrapper because AWS Lambda's `python3 /var/runtime/bootstrap.py` will
133+
# use `imp.load_module` to load the function from the `_HANDLER` environment
134+
# variable. This RELOADS the module and REMOVES any instrumentation patching
135+
# done earlier. So we delay instrumentation until `bootstrap.py` imports
136+
# `otel_wrapper.py` at which we know the patching will be picked up.
137+
#
138+
# See more:
139+
# https://docs.python.org/3/library/imp.html#imp.load_module
140+
141+
export ORIG_HANDLER=$_HANDLER;
142+
export _HANDLER="otel_wrapper.lambda_handler";
143+
144+
# - Call the upstream auto instrumentation script
145+
146+
exec python3 $LAMBDA_LAYER_PKGS_DIR/bin/opentelemetry-instrument "$@"

lambda-layer/src/otel_wrapper.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
`otel_wrapper.py`
17+
18+
This file serves as a wrapper over the user's Lambda function.
19+
20+
Usage
21+
-----
22+
Patch the reserved `_HANDLER` Lambda environment variable to point to this
23+
file's `otel_wrapper.lambda_handler` property. Do this having saved the original
24+
`_HANDLER` in the `ORIG_HANDLER` environment variable. Doing this makes it so
25+
that **on import of this file, the handler is instrumented**.
26+
27+
Instrumenting any earlier will cause the instrumentation to be lost because the
28+
AWS Service uses `imp.load_module` to import the handler which RELOADS the
29+
module. This is why AwsLambdaInstrumentor cannot be instrumented with the
30+
`opentelemetry-instrument` script.
31+
32+
See more:
33+
https://docs.python.org/3/library/imp.html#imp.load_module
34+
35+
"""
36+
37+
import os
38+
from importlib import import_module
39+
40+
from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor
41+
42+
43+
def modify_module_name(module_name):
44+
"""Returns a valid modified module to get imported"""
45+
return ".".join(module_name.split("/"))
46+
47+
48+
class HandlerError(Exception):
49+
pass
50+
51+
52+
AwsLambdaInstrumentor().instrument()
53+
54+
path = os.environ.get("ORIG_HANDLER")
55+
56+
if path is None:
57+
raise HandlerError("ORIG_HANDLER is not defined.")
58+
59+
try:
60+
(mod_name, handler_name) = path.rsplit(".", 1)
61+
except ValueError as e:
62+
raise HandlerError("Bad path '{}' for ORIG_HANDLER: {}".format(path, str(e)))
63+
64+
modified_mod_name = modify_module_name(mod_name)
65+
handler_module = import_module(modified_mod_name)
66+
lambda_handler = getattr(handler_module, handler_name)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
16+
def handler(event, context):
17+
return "200 ok"

0 commit comments

Comments
 (0)