Skip to content

Commit 5916901

Browse files
committed
Lambda exec wrapper calls upstream OTel Python auto instr script
1 parent e6e7905 commit 5916901

File tree

5 files changed

+311
-224
lines changed

5 files changed

+311
-224
lines changed

Diff for: python/src/otel/otel_sdk/otel-instrument

+141-44
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,141 @@
1-
#!/usr/bin/env python3
2-
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3-
# SPDX-License-Identifier: MIT-0
4-
5-
from os import environ, system
6-
import sys
7-
8-
# the path to the interpreter and all of the originally intended arguments
9-
args = sys.argv[1:]
10-
11-
# enable OTel wrapper
12-
environ["ORIG_HANDLER"] = environ.get("_HANDLER")
13-
environ["_HANDLER"] = "otel_wrapper.lambda_handler"
14-
15-
# config default traces exporter if missing
16-
environ.setdefault("OTEL_TRACES_EXPORTER", "otlp_proto_grpc_span")
17-
18-
# set service name
19-
if environ.get("OTEL_RESOURCE_ATTRIBUTES") is None:
20-
environ["OTEL_RESOURCE_ATTRIBUTES"] = "service.name=%s" % (
21-
environ.get("AWS_LAMBDA_FUNCTION_NAME")
22-
)
23-
elif "service.name=" not in environ.get("OTEL_RESOURCE_ATTRIBUTES"):
24-
environ["OTEL_RESOURCE_ATTRIBUTES"] = "service.name=%s,%s" % (
25-
environ.get("AWS_LAMBDA_FUNCTION_NAME"),
26-
environ.get("OTEL_RESOURCE_ATTRIBUTES"),
27-
)
28-
29-
# TODO: Remove if sdk support resource detector env variable configuration.
30-
lambda_resource_attributes = (
31-
"cloud.region=%s,cloud.provider=aws,faas.name=%s,faas.version=%s"
32-
% (
33-
environ.get("AWS_REGION"),
34-
environ.get("AWS_LAMBDA_FUNCTION_NAME"),
35-
environ.get("AWS_LAMBDA_FUNCTION_VERSION"),
36-
)
37-
)
38-
environ["OTEL_RESOURCE_ATTRIBUTES"] = "%s,%s" % (
39-
lambda_resource_attributes,
40-
environ.get("OTEL_RESOURCE_ATTRIBUTES"),
41-
)
42-
43-
# start the runtime with the extra options
44-
system(" ".join(args))
1+
#!/usr/bin/env bash
2+
3+
# Copyright The OpenTelemetry Authors
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
: '
18+
`otel-instrument`
19+
20+
This script configures and sets up OpenTelemetry Python with the values we
21+
expect will be used by the common user. It does this by setting the environment
22+
variables OpenTelemetry uses, and then initializing OpenTelemetry using the
23+
`opentelemetry-instrument` auto instrumentation script from the
24+
`opentelemetry-instrumentation` package.
25+
26+
Additionally, this configuration assumes the user is using packages conforming
27+
to the `opentelemetry-instrumentation` and `opentelemetry-sdk` specifications.
28+
29+
DO NOT use this script for anything else besides SETTING ENVIRONMENT VARIABLES.
30+
31+
See more:
32+
https://docs.aws.amazon.com/lambda/latest/dg/runtimes-modify.html#runtime-wrapper
33+
34+
Usage
35+
-----
36+
We expect this file to be at the root of a Lambda Layer. Having it anywhere else
37+
seems to mean AWS Lambda cannot find it.
38+
39+
In the configuration of an AWS Lambda function with this file at the
40+
root level of a Lambda Layer:
41+
42+
.. code::
43+
44+
AWS_LAMBDA_EXEC_WRAPPER = /opt/otel-instrument
45+
46+
'
47+
48+
# Use constants to access the environment variables we want to use in this
49+
# script.
50+
51+
# See more:
52+
# https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime
53+
54+
# - Reserved environment variables
55+
56+
# - - $AWS_LAMBDA_FUNCTION_NAME
57+
# - - $LAMBDA_RUNTIME_DIR
58+
59+
# - Unreserved environment variables
60+
61+
# - - $PYTHONPATH
62+
63+
# Update the python paths for packages with `sys.path` and `PYTHONPATH`
64+
65+
# - We know that the path to the Lambda Layer OpenTelemetry Python packages are
66+
# well defined, so we can add them to the PYTHONPATH.
67+
#
68+
# See more:
69+
# https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html#configuration-layers-path
70+
71+
export LAMBDA_LAYER_PKGS_DIR="/opt/python"
72+
73+
# - Set Lambda Layer python packages in PYTHONPATH so `opentelemetry-instrument`
74+
# script can find them (it needs to find `opentelemetry` to find the auto
75+
# instrumentation `run()` method later)
76+
77+
if [ -z ${PYTHONPATH} ]; then
78+
export PYTHONPATH=$LAMBDA_LAYER_PKGS_DIR;
79+
else
80+
export PYTHONPATH="$LAMBDA_LAYER_PKGS_DIR:$PYTHONPATH";
81+
fi
82+
83+
# - Set Lambda runtime python packages in PYTHONPATH so
84+
# `opentelemetry-instrument` script can find them during auto instrumentation
85+
# and instrument them.
86+
87+
export PYTHONPATH="$LAMBDA_RUNTIME_DIR:$PYTHONPATH";
88+
89+
# Configure OpenTelemetry Python with environment variables
90+
91+
# - Set the default Trace Exporter
92+
93+
if [ -z ${OTEL_TRACES_EXPORTER} ]; then
94+
export OTEL_TRACES_EXPORTER="otlp_proto_grpc_span";
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 Resource Detectors (Resource Attributes)
104+
#
105+
# TODO: waiting on OTel Python support for configuring Resource Detectors from
106+
# an environment variable. Replace the bottom code with the following when
107+
# this is possible.
108+
#
109+
# export OTEL_RESOURCE_DETECTORS="aws_lambda";
110+
#
111+
export LAMBDA_RESOURCE_ATTRIBUTES="cloud.region=$AWS_REGION,cloud.provider=aws,faas.name=$AWS_LAMBDA_FUNCTION_NAME,faas.version=$AWS_LAMBDA_FUNCTION_VERSION"
112+
113+
if [ -z ${OTEL_RESOURCE_ATTRIBUTES} ]; then
114+
export OTEL_RESOURCE_ATTRIBUTES=$LAMBDA_RESOURCE_ATTRIBUTES;
115+
else
116+
export OTEL_RESOURCE_ATTRIBUTES="$LAMBDA_RESOURCE_ATTRIBUTES,$OTEL_RESOURCE_ATTRIBUTES";
117+
fi
118+
119+
# - Set the default propagators
120+
121+
if [ -z ${OTEL_PROPAGATORS} ]; then
122+
export OTEL_PROPAGATORS="tracecontext,b3,xray";
123+
fi
124+
125+
# - Use a wrapper because AWS Lambda's `python3 /var/runtime/bootstrap.py` will
126+
# use `imp.load_module` to load the function from the `_HANDLER` environment
127+
# variable. This RELOADS the module and REMOVES any instrumentation patching
128+
# done earlier. So we delay instrumentation until `boostrap.py` imports
129+
# `otel_wrapper.py` at which we know the patching will be picked up.
130+
#
131+
# See more:
132+
# https://docs.python.org/3/library/imp.html#imp.load_module
133+
134+
export ORIG_HANDLER=$_HANDLER;
135+
export _HANDLER="otel_wrapper.lambda_handler";
136+
137+
# - Call the upstream auto instrumentation script
138+
139+
python3 $LAMBDA_LAYER_PKGS_DIR/bin/opentelemetry-instrument "$@"
140+
141+

Diff for: python/src/otel/otel_sdk/otel_wrapper.py

+39-85
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,55 @@
1-
import logging
2-
import os
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+
"""
336

37+
import os
438
from importlib import import_module
5-
from pkg_resources import iter_entry_points
639

7-
from opentelemetry.instrumentation.dependencies import get_dist_dependency_conflicts
840
from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor
9-
from opentelemetry.environment_variables import OTEL_PYTHON_DISABLED_INSTRUMENTATIONS
10-
from opentelemetry.instrumentation.distro import BaseDistro, DefaultDistro
11-
12-
logging.basicConfig(level=logging.INFO)
13-
logger = logging.getLogger(__name__)
14-
15-
# TODO: waiting OTel Python supports env variable config for resource detector
16-
# from opentelemetry.resource import AwsLambdaResourceDetector
17-
# from opentelemetry.sdk.resources import Resource
18-
# resource = Resource.create().merge(AwsLambdaResourceDetector().detect())
19-
# trace.get_tracer_provider.resource = resource
20-
21-
def _load_distros() -> BaseDistro:
22-
for entry_point in iter_entry_points("opentelemetry_distro"):
23-
try:
24-
distro = entry_point.load()()
25-
if not isinstance(distro, BaseDistro):
26-
logger.debug(
27-
"%s is not an OpenTelemetry Distro. Skipping",
28-
entry_point.name,
29-
)
30-
continue
31-
logger.debug(
32-
"Distribution %s will be configured", entry_point.name
33-
)
34-
return distro
35-
except Exception as exc: # pylint: disable=broad-except
36-
logger.debug("Distribution %s configuration failed", entry_point.name)
37-
return DefaultDistro()
38-
39-
def _load_instrumentors(distro):
40-
package_to_exclude = os.environ.get(OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, [])
41-
if isinstance(package_to_exclude, str):
42-
package_to_exclude = package_to_exclude.split(",")
43-
# to handle users entering "requests , flask" or "requests, flask" with spaces
44-
package_to_exclude = [x.strip() for x in package_to_exclude]
45-
46-
for entry_point in iter_entry_points("opentelemetry_instrumentor"):
47-
if entry_point.name in package_to_exclude:
48-
logger.debug(
49-
"Instrumentation skipped for library %s", entry_point.name
50-
)
51-
continue
52-
53-
try:
54-
conflict = get_dist_dependency_conflicts(entry_point.dist)
55-
if conflict:
56-
logger.debug(
57-
"Skipping instrumentation %s: %s",
58-
entry_point.name,
59-
conflict,
60-
)
61-
continue
62-
63-
# tell instrumentation to not run dep checks again as we already did it above
64-
distro.load_instrumentor(entry_point, skip_dep_check=True)
65-
logger.info("Instrumented %s", entry_point.name)
66-
except Exception as exc: # pylint: disable=broad-except
67-
logger.debug("Instrumenting of %s failed", entry_point.name)
68-
69-
def _load_configurators():
70-
configured = None
71-
for entry_point in iter_entry_points("opentelemetry_configurator"):
72-
if configured is not None:
73-
logger.warning(
74-
"Configuration of %s not loaded, %s already loaded",
75-
entry_point.name,
76-
configured,
77-
)
78-
continue
79-
try:
80-
entry_point.load()().configure() # type: ignore
81-
configured = entry_point.name
82-
except Exception as exc: # pylint: disable=broad-except
83-
logger.debug("Configuration of %s failed", entry_point.name)
8441

8542

8643
def modify_module_name(module_name):
8744
"""Returns a valid modified module to get imported"""
8845
return ".".join(module_name.split("/"))
8946

47+
9048
class HandlerError(Exception):
9149
pass
9250

93-
distro = _load_distros()
94-
distro.configure()
95-
_load_configurators()
96-
_load_instrumentors(distro)
97-
# TODO: move to python-contrib
98-
AwsLambdaInstrumentor().instrument(skip_dep_check=True)
51+
52+
AwsLambdaInstrumentor().instrument()
9953

10054
path = os.environ.get("ORIG_HANDLER", None)
10155
if path is None:

Diff for: python/src/otel/tests/mock_user_lambda.py

-2
This file was deleted.

Diff for: python/src/otel/tests/mocks/lambda_function.py

+17
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)