Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 0f15526

Browse files
committedOct 14, 2021
Add instrumentation for AWS Lambda Service
1 parent 9f6c97a commit 0f15526

File tree

14 files changed

+1278
-0
lines changed

14 files changed

+1278
-0
lines changed
 

‎CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.6.0-0.25b0...HEAD)
99

10+
- `opentelemetry-instrumentation-aws_lambda` Add Instrumentation for AWS Lambda package
11+
([#739](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/739))
12+
1013
## [1.6.0-0.25b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.6.0-0.25b0) - 2021-10-13
1114

1215

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
Apache License
2+
Version 2.0, January 2004
3+
http://www.apache.org/licenses/
4+
5+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6+
7+
1. Definitions.
8+
9+
"License" shall mean the terms and conditions for use, reproduction,
10+
and distribution as defined by Sections 1 through 9 of this document.
11+
12+
"Licensor" shall mean the copyright owner or entity authorized by
13+
the copyright owner that is granting the License.
14+
15+
"Legal Entity" shall mean the union of the acting entity and all
16+
other entities that control, are controlled by, or are under common
17+
control with that entity. For the purposes of this definition,
18+
"control" means (i) the power, direct or indirect, to cause the
19+
direction or management of such entity, whether by contract or
20+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
21+
outstanding shares, or (iii) beneficial ownership of such entity.
22+
23+
"You" (or "Your") shall mean an individual or Legal Entity
24+
exercising permissions granted by this License.
25+
26+
"Source" form shall mean the preferred form for making modifications,
27+
including but not limited to software source code, documentation
28+
source, and configuration files.
29+
30+
"Object" form shall mean any form resulting from mechanical
31+
transformation or translation of a Source form, including but
32+
not limited to compiled object code, generated documentation,
33+
and conversions to other media types.
34+
35+
"Work" shall mean the work of authorship, whether in Source or
36+
Object form, made available under the License, as indicated by a
37+
copyright notice that is included in or attached to the work
38+
(an example is provided in the Appendix below).
39+
40+
"Derivative Works" shall mean any work, whether in Source or Object
41+
form, that is based on (or derived from) the Work and for which the
42+
editorial revisions, annotations, elaborations, or other modifications
43+
represent, as a whole, an original work of authorship. For the purposes
44+
of this License, Derivative Works shall not include works that remain
45+
separable from, or merely link (or bind by name) to the interfaces of,
46+
the Work and Derivative Works thereof.
47+
48+
"Contribution" shall mean any work of authorship, including
49+
the original version of the Work and any modifications or additions
50+
to that Work or Derivative Works thereof, that is intentionally
51+
submitted to Licensor for inclusion in the Work by the copyright owner
52+
or by an individual or Legal Entity authorized to submit on behalf of
53+
the copyright owner. For the purposes of this definition, "submitted"
54+
means any form of electronic, verbal, or written communication sent
55+
to the Licensor or its representatives, including but not limited to
56+
communication on electronic mailing lists, source code control systems,
57+
and issue tracking systems that are managed by, or on behalf of, the
58+
Licensor for the purpose of discussing and improving the Work, but
59+
excluding communication that is conspicuously marked or otherwise
60+
designated in writing by the copyright owner as "Not a Contribution."
61+
62+
"Contributor" shall mean Licensor and any individual or Legal Entity
63+
on behalf of whom a Contribution has been received by Licensor and
64+
subsequently incorporated within the Work.
65+
66+
2. Grant of Copyright License. Subject to the terms and conditions of
67+
this License, each Contributor hereby grants to You a perpetual,
68+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69+
copyright license to reproduce, prepare Derivative Works of,
70+
publicly display, publicly perform, sublicense, and distribute the
71+
Work and such Derivative Works in Source or Object form.
72+
73+
3. Grant of Patent License. Subject to the terms and conditions of
74+
this License, each Contributor hereby grants to You a perpetual,
75+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76+
(except as stated in this section) patent license to make, have made,
77+
use, offer to sell, sell, import, and otherwise transfer the Work,
78+
where such license applies only to those patent claims licensable
79+
by such Contributor that are necessarily infringed by their
80+
Contribution(s) alone or by combination of their Contribution(s)
81+
with the Work to which such Contribution(s) was submitted. If You
82+
institute patent litigation against any entity (including a
83+
cross-claim or counterclaim in a lawsuit) alleging that the Work
84+
or a Contribution incorporated within the Work constitutes direct
85+
or contributory patent infringement, then any patent licenses
86+
granted to You under this License for that Work shall terminate
87+
as of the date such litigation is filed.
88+
89+
4. Redistribution. You may reproduce and distribute copies of the
90+
Work or Derivative Works thereof in any medium, with or without
91+
modifications, and in Source or Object form, provided that You
92+
meet the following conditions:
93+
94+
(a) You must give any other recipients of the Work or
95+
Derivative Works a copy of this License; and
96+
97+
(b) You must cause any modified files to carry prominent notices
98+
stating that You changed the files; and
99+
100+
(c) You must retain, in the Source form of any Derivative Works
101+
that You distribute, all copyright, patent, trademark, and
102+
attribution notices from the Source form of the Work,
103+
excluding those notices that do not pertain to any part of
104+
the Derivative Works; and
105+
106+
(d) If the Work includes a "NOTICE" text file as part of its
107+
distribution, then any Derivative Works that You distribute must
108+
include a readable copy of the attribution notices contained
109+
within such NOTICE file, excluding those notices that do not
110+
pertain to any part of the Derivative Works, in at least one
111+
of the following places: within a NOTICE text file distributed
112+
as part of the Derivative Works; within the Source form or
113+
documentation, if provided along with the Derivative Works; or,
114+
within a display generated by the Derivative Works, if and
115+
wherever such third-party notices normally appear. The contents
116+
of the NOTICE file are for informational purposes only and
117+
do not modify the License. You may add Your own attribution
118+
notices within Derivative Works that You distribute, alongside
119+
or as an addendum to the NOTICE text from the Work, provided
120+
that such additional attribution notices cannot be construed
121+
as modifying the License.
122+
123+
You may add Your own copyright statement to Your modifications and
124+
may provide additional or different license terms and conditions
125+
for use, reproduction, or distribution of Your modifications, or
126+
for any such Derivative Works as a whole, provided Your use,
127+
reproduction, and distribution of the Work otherwise complies with
128+
the conditions stated in this License.
129+
130+
5. Submission of Contributions. Unless You explicitly state otherwise,
131+
any Contribution intentionally submitted for inclusion in the Work
132+
by You to the Licensor shall be under the terms and conditions of
133+
this License, without any additional terms or conditions.
134+
Notwithstanding the above, nothing herein shall supersede or modify
135+
the terms of any separate license agreement you may have executed
136+
with Licensor regarding such Contributions.
137+
138+
6. Trademarks. This License does not grant permission to use the trade
139+
names, trademarks, service marks, or product names of the Licensor,
140+
except as required for reasonable and customary use in describing the
141+
origin of the Work and reproducing the content of the NOTICE file.
142+
143+
7. Disclaimer of Warranty. Unless required by applicable law or
144+
agreed to in writing, Licensor provides the Work (and each
145+
Contributor provides its Contributions) on an "AS IS" BASIS,
146+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147+
implied, including, without limitation, any warranties or conditions
148+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149+
PARTICULAR PURPOSE. You are solely responsible for determining the
150+
appropriateness of using or redistributing the Work and assume any
151+
risks associated with Your exercise of permissions under this License.
152+
153+
8. Limitation of Liability. In no event and under no legal theory,
154+
whether in tort (including negligence), contract, or otherwise,
155+
unless required by applicable law (such as deliberate and grossly
156+
negligent acts) or agreed to in writing, shall any Contributor be
157+
liable to You for damages, including any direct, indirect, special,
158+
incidental, or consequential damages of any character arising as a
159+
result of this License or out of the use or inability to use the
160+
Work (including but not limited to damages for loss of goodwill,
161+
work stoppage, computer failure or malfunction, or any and all
162+
other commercial damages or losses), even if such Contributor
163+
has been advised of the possibility of such damages.
164+
165+
9. Accepting Warranty or Additional Liability. While redistributing
166+
the Work or Derivative Works thereof, You may choose to offer,
167+
and charge a fee for, acceptance of support, warranty, indemnity,
168+
or other liability obligations and/or rights consistent with this
169+
License. However, in accepting such obligations, You may act only
170+
on Your own behalf and on Your sole responsibility, not on behalf
171+
of any other Contributor, and only if You agree to indemnify,
172+
defend, and hold each Contributor harmless for any liability
173+
incurred by, or claims asserted against, such Contributor by reason
174+
of your accepting any such warranty or additional liability.
175+
176+
END OF TERMS AND CONDITIONS
177+
178+
APPENDIX: How to apply the Apache License to your work.
179+
180+
To apply the Apache License to your work, attach the following
181+
boilerplate notice, with the fields enclosed by brackets "[]"
182+
replaced with your own identifying information. (Don't include
183+
the brackets!) The text should be enclosed in the appropriate
184+
comment syntax for the file format. We also recommend that a
185+
file or class name and description of purpose be included on the
186+
same "printed page" as the copyright notice for easier
187+
identification within third-party archives.
188+
189+
Copyright The OpenTelemetry Authors
190+
191+
Licensed under the Apache License, Version 2.0 (the "License");
192+
you may not use this file except in compliance with the License.
193+
You may obtain a copy of the License at
194+
195+
http://www.apache.org/licenses/LICENSE-2.0
196+
197+
Unless required by applicable law or agreed to in writing, software
198+
distributed under the License is distributed on an "AS IS" BASIS,
199+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200+
See the License for the specific language governing permissions and
201+
limitations under the License.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
graft src
2+
graft tests
3+
global-exclude *.pyc
4+
global-exclude *.pyo
5+
global-exclude __pycache__/*
6+
include MANIFEST.in
7+
include README.rst
8+
include LICENSE
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
OpenTelemetry AWS Lambda Tracing
2+
================================
3+
4+
|pypi|
5+
6+
.. |pypi| image:: https://badge.fury.io/py/opentelemetry-instrumentation-aws-lambda.svg
7+
:target: https://pypi.org/project/opentelemetry-instrumentation-aws-lambda/
8+
9+
This library provides an Instrumentor used to trace requests made by the Lambda
10+
functions on the AWS Lambda service.
11+
12+
It also provides scripts used by AWS Lambda Layers to automatically initialize
13+
the OpenTelemetry SDK. Learn more on the AWS Distro for OpenTelemetry (ADOT)
14+
`documentation for the Python Lambda Layer https://aws-otel.github.io/docs/getting-started/lambda/lambda-python`_.
15+
16+
Installation
17+
------------
18+
19+
::
20+
21+
pip install opentelemetry-instrumentation-aws-lambda
22+
23+
24+
Usage
25+
-----
26+
27+
.. code:: python
28+
# Copy this snippet into an AWS Lambda function
29+
30+
import boto3
31+
from opentelemetry.instrumentation.botocore import AwsBotocoreInstrumentor
32+
from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor
33+
34+
35+
# Enable instrumentation
36+
AwsBotocoreInstrumentor().instrument()
37+
AwsLambdaInstrumentor().instrument()
38+
39+
# Lambda function
40+
def lambda_handler(event, context):
41+
s3 = boto3.resource('s3')
42+
for bucket in s3.buckets.all():
43+
print(bucket.name)
44+
45+
return "200 OK"
46+
47+
Using a custom `event_context_extractor` to parent traces with a Trace Context
48+
found in the Lambda Event.
49+
50+
.. code:: python
51+
52+
from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor
53+
54+
def custom_event_context_extractor(lambda_event):
55+
# If the `TraceContextTextMapPropagator` is the global propagator, we
56+
# can use it to parse out the context from the HTTP Headers.
57+
return get_global_textmap().extract(lambda_event["foo"]["headers"])
58+
59+
AwsLambdaInstrumentor().instrument(
60+
event_context_extractor=custom_event_context_extractor
61+
)
62+
63+
References
64+
----------
65+
66+
* `OpenTelemetry AWS Lambda Tracing <https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/aws_lambda/aws_lambda.html>`_
67+
* `OpenTelemetry Project <https://opentelemetry.io/>`_
68+
* `OpenTelemetry Python Examples <https://github.com/open-telemetry/opentelemetry-python/tree/main/docs/examples>`_
69+
* `ADOT Python Lambda Layer Documentation https://aws-otel.github.io/docs/getting-started/lambda/lambda-python`_
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import logging
2+
import os
3+
from importlib import import_module
4+
from os import environ
5+
6+
from pkg_resources import iter_entry_points
7+
8+
from opentelemetry.instrumentation.aws_lambda import ORIG_HANDLER
9+
from opentelemetry.instrumentation.dependencies import (
10+
get_dist_dependency_conflicts,
11+
)
12+
from opentelemetry.instrumentation.distro import BaseDistro, DefaultDistro
13+
from opentelemetry.instrumentation.environment_variables import (
14+
OTEL_PYTHON_DISABLED_INSTRUMENTATIONS,
15+
)
16+
17+
logging.basicConfig(level=logging.INFO)
18+
logger = logging.getLogger(__name__)
19+
20+
21+
def configure_otel_env_vars():
22+
aws_lambda_function_name = "AWS_LAMBDA_FUNCTION_NAME"
23+
otel_resource_attributes = "OTEL_RESOURCE_ATTRIBUTES"
24+
25+
# Set the default Trace Exporter
26+
environ.setdefault("OTEL_TRACES_EXPORTER", "otlp_proto_grpc_span")
27+
28+
# Set the service name
29+
service_name_resource_attribute = "service.name"
30+
if environ.get(otel_resource_attributes) is None:
31+
environ[otel_resource_attributes] = "%s=%s" % (
32+
service_name_resource_attribute,
33+
environ.get(aws_lambda_function_name),
34+
)
35+
elif service_name_resource_attribute not in environ.get(
36+
otel_resource_attributes
37+
):
38+
environ[otel_resource_attributes] = "%s=%s,%s" % (
39+
service_name_resource_attribute,
40+
environ.get(aws_lambda_function_name),
41+
environ.get(otel_resource_attributes),
42+
)
43+
44+
# Set the Resource Detectors (Resource Attributes)
45+
# TODO: waiting on OTel Python support for configuring Resource Detectors from
46+
# an environment variable. Replace the bottom code with the following when this
47+
# is possible.
48+
#
49+
# environ["OTEL_RESOURCE_DETECTORS"] = "aws_lambda"
50+
#
51+
lambda_resource_attributes = (
52+
"cloud.region=%s,cloud.provider=aws,faas.name=%s,faas.version=%s"
53+
% (
54+
environ.get("AWS_REGION"),
55+
environ.get(aws_lambda_function_name),
56+
environ.get("AWS_LAMBDA_FUNCTION_VERSION"),
57+
)
58+
)
59+
environ[otel_resource_attributes] = "%s,%s" % (
60+
lambda_resource_attributes,
61+
environ.get(otel_resource_attributes),
62+
)
63+
64+
# Set the default propagators
65+
environ.setdefault("OTEL_PROPAGATORS", "tracecontext,b3,xray")
66+
67+
68+
def setup_otel():
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(
84+
"Configuration of %s failed",
85+
entry_point.name,
86+
exc_info=exc,
87+
)
88+
89+
def _load_distros() -> BaseDistro:
90+
for entry_point in iter_entry_points("opentelemetry_distro"):
91+
try:
92+
distro = entry_point.load()()
93+
if not isinstance(distro, BaseDistro):
94+
logger.debug(
95+
"%s is not an OpenTelemetry Distro. Skipping",
96+
entry_point.name,
97+
)
98+
continue
99+
logger.debug(
100+
"Distribution %s will be configured", entry_point.name
101+
)
102+
return distro
103+
except Exception as exc: # pylint: disable=broad-except
104+
logger.debug(
105+
"Distribution %s configuration failed",
106+
entry_point.name,
107+
exc_info=exc,
108+
)
109+
return DefaultDistro()
110+
111+
def _load_instrumentors(distro):
112+
package_to_exclude = os.environ.get(
113+
OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, []
114+
)
115+
if isinstance(package_to_exclude, str):
116+
package_to_exclude = package_to_exclude.split(",")
117+
# to handle users entering "requests , flask" or "requests, flask" with spaces
118+
package_to_exclude = [x.strip() for x in package_to_exclude]
119+
120+
for entry_point in iter_entry_points("opentelemetry_instrumentor"):
121+
if entry_point.name in package_to_exclude:
122+
logger.debug(
123+
"Instrumentation skipped for library %s", entry_point.name
124+
)
125+
continue
126+
127+
try:
128+
conflict = get_dist_dependency_conflicts(entry_point.dist)
129+
if conflict:
130+
logger.debug(
131+
"Skipping instrumentation %s: %s",
132+
entry_point.name,
133+
conflict,
134+
)
135+
continue
136+
137+
# tell instrumentation to not run dep checks again as we already did it above
138+
distro.load_instrumentor(entry_point, skip_dep_check=True)
139+
logger.info("Instrumented %s", entry_point.name)
140+
except Exception as exc: # pylint: disable=broad-except
141+
logger.debug(
142+
"Instrumenting of %s failed",
143+
entry_point.name,
144+
exc_info=exc,
145+
)
146+
147+
distro = _load_distros()
148+
distro.configure()
149+
_load_configurators()
150+
_load_instrumentors(distro)
151+
152+
153+
configure_otel_env_vars()
154+
setup_otel()
155+
156+
157+
class HandlerError(Exception):
158+
pass
159+
160+
161+
path = os.environ.get(ORIG_HANDLER, None)
162+
if path is None:
163+
raise HandlerError(f"{ORIG_HANDLER} is not defined.")
164+
parts = path.rsplit(".", 1)
165+
if len(parts) != 2:
166+
raise HandlerError(f"Value {path} for {ORIG_HANDLER} has invalid format.")
167+
168+
169+
def modify_module_name(module_name):
170+
"""Returns a valid modified module to get imported"""
171+
return ".".join(module_name.split("/"))
172+
173+
174+
(mod_name, handler_name) = parts
175+
modified_mod_name = modify_module_name(mod_name)
176+
handler_module = import_module(modified_mod_name)
177+
lambda_handler = getattr(handler_module, handler_name)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env python3
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+
`patch-with-configure_otel_sdk_on_lambda.py`
19+
20+
This script patches the AWS Service `_HANDLER` environment variable to call our
21+
`configure_otel_sdk_on_lambda.py` `lambda_handler` instead of the user-defined
22+
Lambda Handler. This way, we can automatically setup OpenTelemetry Python for
23+
the user with some defaults. We use what we expect will be the most common user
24+
configuration.
25+
26+
We save the original user-defined Lambda Handler in `ORIG_HANDLER` which is used
27+
by `AwsLambdaInstrumentor` during instrumentation.
28+
29+
Usage
30+
-----
31+
In the configuration of an AWS Lambda function with
32+
`patch-with-configure_otel_sdk_on_lambda.py` at the root level of a Lambda
33+
Layer:
34+
35+
.. code::
36+
37+
AWS_LAMBDA_EXEC_WRAPPER = /opt/patch-with-configure_otel_sdk_on_lambda.py
38+
39+
"""
40+
41+
import sys
42+
from os import environ, system
43+
44+
# Patch the Lambda Handler with our own "Configure OTel SDK Wrapper" Lambda
45+
# Handler. Simply importing the `configure_otel_sdk_on_lambda.py` module
46+
# configures the OTel SDK, we let Lambda call the `lambda_handler` when it is
47+
# ready to do so.
48+
environ["ORIG_HANDLER"] = environ.get("_HANDLER")
49+
environ["_HANDLER"] = "configure_otel_sdk_on_lambda.lambda_handler"
50+
51+
# Start the program with the originally intended arguments. The system _is_ the
52+
# Python interpreter so we don't need to include it.
53+
system(" ".join(sys.argv[1:]))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
[metadata]
16+
name = opentelemetry-instrumentation-aws_lambda
17+
description = OpenTelemetry AWS Lambda instrumentation
18+
long_description = file: README.rst
19+
long_description_content_type = text/x-rst
20+
author = OpenTelemetry Authors
21+
author_email = cncf-opentelemetry-contributors@lists.cncf.io
22+
url = https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation/opentelemetry-instrumentation-aws_lambda
23+
platforms = any
24+
license = Apache-2.0
25+
classifiers =
26+
Development Status :: 4 - Beta
27+
Intended Audience :: Developers
28+
License :: OSI Approved :: Apache Software License
29+
Programming Language :: Python
30+
Programming Language :: Python :: 3
31+
Programming Language :: Python :: 3.6
32+
Programming Language :: Python :: 3.7
33+
Programming Language :: Python :: 3.8
34+
35+
[options]
36+
python_requires = >=3.6
37+
package_dir=
38+
=src
39+
packages=find_namespace:
40+
install_requires =
41+
opentelemetry-instrumentation == 0.25b0
42+
opentelemetry-propagator-aws-xray == 1.0.0
43+
opentelemetry-semantic-conventions == 0.25b0
44+
45+
[options.extras_require]
46+
test =
47+
opentelemetry-test == 0.25b0
48+
49+
[options.packages.find]
50+
where = src
51+
52+
[options.entry_points]
53+
opentelemetry_instrumentor =
54+
aws_lambda = opentelemetry.instrumentation.aws_lambda:AwsLambdaInstrumentor
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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+
# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM templates/instrumentation_setup.py.txt.
17+
# RUN `python scripts/generate_setup.py` TO REGENERATE.
18+
19+
20+
import distutils.cmd
21+
import json
22+
import os
23+
from configparser import ConfigParser
24+
25+
import setuptools
26+
27+
config = ConfigParser()
28+
config.read("setup.cfg")
29+
30+
# We provide extras_require parameter to setuptools.setup later which
31+
# overwrites the extra_require section from setup.cfg. To support extra_require
32+
# secion in setup.cfg, we load it here and merge it with the extra_require param.
33+
extras_require = {}
34+
if "options.extras_require" in config:
35+
for key, value in config["options.extras_require"].items():
36+
extras_require[key] = [v for v in value.split("\n") if v.strip()]
37+
38+
BASE_DIR = os.path.dirname(__file__)
39+
PACKAGE_INFO = {}
40+
41+
VERSION_FILENAME = os.path.join(
42+
BASE_DIR,
43+
"src",
44+
"opentelemetry",
45+
"instrumentation",
46+
"aws_lambda",
47+
"version.py",
48+
)
49+
with open(VERSION_FILENAME, encoding="utf-8") as f:
50+
exec(f.read(), PACKAGE_INFO)
51+
52+
PACKAGE_FILENAME = os.path.join(
53+
BASE_DIR,
54+
"src",
55+
"opentelemetry",
56+
"instrumentation",
57+
"aws_lambda",
58+
"package.py",
59+
)
60+
with open(PACKAGE_FILENAME, encoding="utf-8") as f:
61+
exec(f.read(), PACKAGE_INFO)
62+
63+
# Mark any instruments/runtime dependencies as test dependencies as well.
64+
extras_require["instruments"] = PACKAGE_INFO["_instruments"]
65+
test_deps = extras_require.get("test", [])
66+
for dep in extras_require["instruments"]:
67+
test_deps.append(dep)
68+
69+
extras_require["test"] = test_deps
70+
71+
72+
class JSONMetadataCommand(distutils.cmd.Command):
73+
74+
description = (
75+
"print out package metadata as JSON. This is used by OpenTelemetry dev scripts to ",
76+
"auto-generate code in other places",
77+
)
78+
user_options = []
79+
80+
def initialize_options(self):
81+
pass
82+
83+
def finalize_options(self):
84+
pass
85+
86+
def run(self):
87+
metadata = {
88+
"name": config["metadata"]["name"],
89+
"version": PACKAGE_INFO["__version__"],
90+
"instruments": PACKAGE_INFO["_instruments"],
91+
}
92+
print(json.dumps(metadata))
93+
94+
95+
setuptools.setup(
96+
cmdclass={"meta": JSONMetadataCommand},
97+
version=PACKAGE_INFO["__version__"],
98+
extras_require=extras_require,
99+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
# Copyright 2020, 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+
The opentelemetry-instrumentation-aws-lambda package provides an `Instrumentor`
17+
to traces calls whithin a Python AWS Lambda function.
18+
19+
Usage
20+
-----
21+
22+
.. code:: python
23+
# Copy this snippet into an AWS Lambda function
24+
25+
import boto3
26+
from opentelemetry.instrumentation.botocore import AwsBotocoreInstrumentor
27+
from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor
28+
29+
30+
# Enable instrumentation
31+
AwsBotocoreInstrumentor().instrument()
32+
AwsLambdaInstrumentor().instrument()
33+
34+
# Lambda function
35+
def lambda_handler(event, context):
36+
s3 = boto3.resource('s3')
37+
for bucket in s3.buckets.all():
38+
print(bucket.name)
39+
40+
return "200 OK"
41+
42+
API
43+
---
44+
45+
The `instrument` method accepts the following keyword args:
46+
47+
tracer_provider (TracerProvider) - an optional tracer provider
48+
event_context_extractor (Callable) - a function that returns an OTel Trace
49+
Context given the Lambda Event the AWS Lambda was invoked with
50+
this function signature is: def event_context_extractor(lambda_event: Any) -> Context
51+
for example:
52+
53+
.. code:: python
54+
55+
from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor
56+
57+
def custom_event_context_extractor(lambda_event):
58+
# If the `TraceContextTextMapPropagator` is the global propagator, we
59+
# can use it to parse out the context from the HTTP Headers.
60+
return get_global_textmap().extract(lambda_event["foo"]["headers"])
61+
62+
AwsLambdaInstrumentor().instrument(
63+
event_context_extractor=custom_event_context_extractor
64+
)
65+
"""
66+
67+
import logging
68+
import os
69+
from importlib import import_module
70+
from typing import Any, Collection
71+
72+
from wrapt import wrap_function_wrapper
73+
74+
from opentelemetry.context.context import Context
75+
from opentelemetry.instrumentation.aws_lambda.package import _instruments
76+
from opentelemetry.instrumentation.aws_lambda.version import __version__
77+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
78+
from opentelemetry.instrumentation.utils import unwrap
79+
from opentelemetry.propagate import get_global_textmap
80+
from opentelemetry.propagators.aws.aws_xray_propagator import (
81+
TRACE_HEADER_KEY,
82+
AwsXRayPropagator,
83+
)
84+
from opentelemetry.semconv.resource import ResourceAttributes
85+
from opentelemetry.semconv.trace import SpanAttributes
86+
from opentelemetry.trace import (
87+
SpanKind,
88+
TracerProvider,
89+
get_tracer,
90+
get_tracer_provider,
91+
)
92+
from opentelemetry.trace.propagation import get_current_span
93+
94+
logger = logging.getLogger(__name__)
95+
96+
_HANDLER = "_HANDLER"
97+
_X_AMZN_TRACE_ID = "_X_AMZN_TRACE_ID"
98+
ORIG_HANDLER = "ORIG_HANDLER"
99+
100+
101+
class AwsLambdaInstrumentor(BaseInstrumentor):
102+
def instrumentation_dependencies(self) -> Collection[str]:
103+
return _instruments
104+
105+
def _instrument(self, **kwargs):
106+
"""Instruments Lambda Handlers on AWS Lambda.
107+
108+
See more:
109+
https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/aws-lambda.md#instrumenting-aws-lambda
110+
111+
Args:
112+
**kwargs: Optional arguments
113+
``tracer_provider``: a TracerProvider, defaults to global
114+
``event_context_extractor``: a method which takes the Lambda
115+
Event as input and extracts an OTel Context from it. By default,
116+
the context is extracted from the HTTP headers of an API Gateway
117+
request.
118+
"""
119+
lambda_handler = os.environ.get(ORIG_HANDLER, os.environ.get(_HANDLER))
120+
wrapped_names = lambda_handler.rsplit(".", 1)
121+
self._wrapped_module_name = wrapped_names[0]
122+
self._wrapped_function_name = wrapped_names[1]
123+
124+
_instrument(
125+
self._wrapped_module_name,
126+
self._wrapped_function_name,
127+
tracer_provider=kwargs.get("tracer_provider"),
128+
event_context_extractor=kwargs.get("event_context_extractor"),
129+
)
130+
131+
def _uninstrument(self, **kwargs):
132+
unwrap(
133+
import_module(self._wrapped_module_name),
134+
self._wrapped_function_name,
135+
)
136+
137+
138+
def _default_event_context_extractor(lambda_event: Any) -> Context:
139+
"""Default way of extracting the context from the Lambda Event.
140+
141+
Assumes the Lambda Event is a map with the headers under the 'headers' key.
142+
This is the mapping to use when the Lambda is invoked by an API Gateway
143+
REST API where API Gateway is acting as a pure proxy for the request.
144+
145+
See more:
146+
https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format
147+
148+
Args:
149+
lambda_event: user-defined, so it could be anything, but this
150+
method counts it being a map with a 'headers' key
151+
Returns:
152+
A Context with configuration found in the event.
153+
"""
154+
try:
155+
headers = lambda_event["headers"]
156+
except (TypeError, KeyError):
157+
logger.debug(
158+
"Extracting context from Lambda Event failed: either enable X-Ray active tracing or configure API Gateway to trigger this Lambda function as a pure proxy. Otherwise, generated spans will have an invalid (empty) parent context."
159+
)
160+
headers = {}
161+
return get_global_textmap().extract(headers)
162+
163+
164+
def _instrument(
165+
wrapped_module_name,
166+
wrapped_function_name,
167+
tracer_provider: TracerProvider = None,
168+
event_context_extractor=None,
169+
):
170+
def _determine_parent_context(lambda_event: Any) -> Context:
171+
"""Determine the parent context for the current Lambda invocation.
172+
173+
See more:
174+
https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/aws-lambda.md#determining-the-parent-of-a-span
175+
176+
Args:
177+
lambda_event: user-defined, so it could be anything, but this
178+
method counts it being a map with a 'headers' key
179+
Returns:
180+
A Context with configuration found in the carrier.
181+
"""
182+
parent_context = None
183+
184+
xray_env_var = os.environ.get(_X_AMZN_TRACE_ID)
185+
186+
if xray_env_var:
187+
parent_context = AwsXRayPropagator().extract(
188+
{TRACE_HEADER_KEY: xray_env_var}
189+
)
190+
191+
if (
192+
parent_context
193+
and get_current_span(parent_context)
194+
.get_span_context()
195+
.trace_flags.sampled
196+
):
197+
return parent_context
198+
199+
if event_context_extractor:
200+
parent_context = event_context_extractor(lambda_event)
201+
else:
202+
parent_context = _default_event_context_extractor(lambda_event)
203+
204+
return parent_context
205+
206+
def _instrumented_lambda_handler_call(
207+
call_wrapped, instance, args, kwargs
208+
):
209+
orig_handler_name = ".".join(
210+
[wrapped_module_name, wrapped_function_name]
211+
)
212+
213+
lambda_event = args[0]
214+
215+
parent_context = _determine_parent_context(lambda_event)
216+
217+
tracer = get_tracer(__name__, __version__, tracer_provider)
218+
219+
with tracer.start_as_current_span(
220+
name=orig_handler_name,
221+
context=parent_context,
222+
kind=SpanKind.SERVER,
223+
) as span:
224+
if span.is_recording():
225+
lambda_context = args[1]
226+
# NOTE: The specs mention an exception here, allowing the
227+
# `ResourceAttributes.FAAS_ID` attribute to be set as a span
228+
# attribute instead of a resource attribute.
229+
#
230+
# See more:
231+
# https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/faas.md#example
232+
span.set_attribute(
233+
ResourceAttributes.FAAS_ID,
234+
lambda_context.invoked_function_arn,
235+
)
236+
span.set_attribute(
237+
SpanAttributes.FAAS_EXECUTION,
238+
lambda_context.aws_request_id,
239+
)
240+
241+
result = call_wrapped(*args, **kwargs)
242+
243+
_tracer_provider = tracer_provider or get_tracer_provider()
244+
if hasattr(_tracer_provider, "force_flush"):
245+
# NOTE: force_flush before function quit in case of Lambda freeze.
246+
# Assumes we are using the OpenTelemetry SDK implementation of the
247+
# `TracerProvider`.
248+
_tracer_provider.force_flush()
249+
250+
return result
251+
252+
wrap_function_wrapper(
253+
wrapped_module_name,
254+
wrapped_function_name,
255+
_instrumented_lambda_handler_call,
256+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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+
_instruments = tuple()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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+
__version__ = "0.25b0"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def handler(event, context):
2+
return "200 ok"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
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+
import os
15+
import sys
16+
from importlib import import_module
17+
from unittest import mock
18+
19+
from opentelemetry.environment_variables import OTEL_PROPAGATORS
20+
from opentelemetry.instrumentation.aws_lambda import (
21+
_HANDLER,
22+
_X_AMZN_TRACE_ID,
23+
ORIG_HANDLER,
24+
AwsLambdaInstrumentor,
25+
)
26+
from opentelemetry.propagate import get_global_textmap
27+
from opentelemetry.propagators.aws.aws_xray_propagator import (
28+
TRACE_ID_FIRST_PART_LENGTH,
29+
TRACE_ID_VERSION,
30+
)
31+
from opentelemetry.semconv.resource import ResourceAttributes
32+
from opentelemetry.semconv.trace import SpanAttributes
33+
from opentelemetry.test.test_base import TestBase
34+
from opentelemetry.trace import SpanKind
35+
from opentelemetry.trace.propagation.tracecontext import (
36+
TraceContextTextMapPropagator,
37+
)
38+
39+
AWS_LAMBDA_EXEC_WRAPPER = "AWS_LAMBDA_EXEC_WRAPPER"
40+
CONFIGURE_OTEL_SDK_SCRIPTS_DIR = os.path.join(
41+
*(os.path.dirname(__file__), "..", "scripts")
42+
)
43+
44+
45+
class MockLambdaContext:
46+
def __init__(self, aws_request_id, invoked_function_arn):
47+
self.invoked_function_arn = invoked_function_arn
48+
self.aws_request_id = aws_request_id
49+
50+
51+
MOCK_LAMBDA_CONTEXT = MockLambdaContext(
52+
aws_request_id="mock_aws_request_id",
53+
invoked_function_arn="arn://mock-lambda-function-arn",
54+
)
55+
56+
MOCK_XRAY_TRACE_ID = 0x5FB7331105E8BB83207FA31D4D9CDB4C
57+
MOCK_XRAY_TRACE_ID_STR = f"{MOCK_XRAY_TRACE_ID:x}"
58+
MOCK_XRAY_PARENT_SPAN_ID = 0x3328B8445A6DBAD2
59+
MOCK_XRAY_TRACE_CONTEXT_COMMON = f"Root={TRACE_ID_VERSION}-{MOCK_XRAY_TRACE_ID_STR[:TRACE_ID_FIRST_PART_LENGTH]}-{MOCK_XRAY_TRACE_ID_STR[TRACE_ID_FIRST_PART_LENGTH:]};Parent={MOCK_XRAY_PARENT_SPAN_ID:x}"
60+
MOCK_XRAY_TRACE_CONTEXT_SAMPLED = f"{MOCK_XRAY_TRACE_CONTEXT_COMMON};Sampled=1"
61+
MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED = (
62+
f"{MOCK_XRAY_TRACE_CONTEXT_COMMON};Sampled=0"
63+
)
64+
65+
# Read more:
66+
# https://www.w3.org/TR/trace-context/#examples-of-http-traceparent-headers
67+
MOCK_W3C_TRACE_ID = 0x5CE0E9A56015FEC5AADFA328AE398115
68+
MOCK_W3C_PARENT_SPAN_ID = 0xAB54A98CEB1F0AD2
69+
MOCK_W3C_TRACE_CONTEXT_SAMPLED = (
70+
f"00-{MOCK_W3C_TRACE_ID:x}-{MOCK_W3C_PARENT_SPAN_ID:x}-01"
71+
)
72+
73+
MOCK_W3C_TRACE_STATE_KEY = "vendor_specific_key"
74+
MOCK_W3C_TRACE_STATE_VALUE = "test_value"
75+
76+
77+
def mock_aws_lambda_exec_wrapper():
78+
"""Mocks automatically instrumenting user Lambda function by pointing
79+
`AWS_LAMBDA_EXEC_WRAPPER` to the
80+
`patch-with-configure_otel_sdk_on_lambda.py` script.
81+
82+
TODO: It would be better if `moto`'s `mock_lambda` supported setting
83+
AWS_LAMBDA_EXEC_WRAPPER so we could make the call to Lambda instead.
84+
85+
See more:
86+
https://aws-otel.github.io/docs/getting-started/lambda/lambda-python
87+
"""
88+
89+
with open(
90+
os.path.join(
91+
CONFIGURE_OTEL_SDK_SCRIPTS_DIR,
92+
"patch-with-configure_otel_sdk_on_lambda.py",
93+
)
94+
) as config_otel_script:
95+
exec(config_otel_script.read())
96+
97+
98+
def mock_execute_lambda(event=None):
99+
"""Mocks Lambda importing and then calling the method at the current
100+
`_HANDLER` environment variable. Like the real Lambda, if
101+
`AWS_LAMBDA_EXEC_WRAPPER` is defined, if executes that before `_HANDLER`.
102+
103+
NOTE: Because this function **imports** the
104+
`configure_otel_sdk_on_lambda.py` script, subsequent calls will just use the
105+
cache and not call the functions in that file again. The consequence is that
106+
the configured OTel Python SDK package stays configured and all
107+
Instrumentors stay instrumented across different unit tests.
108+
109+
See more:
110+
https://aws-otel.github.io/docs/getting-started/lambda/lambda-python
111+
"""
112+
if os.environ[AWS_LAMBDA_EXEC_WRAPPER]:
113+
globals()[os.environ[AWS_LAMBDA_EXEC_WRAPPER]]()
114+
115+
# NOTE: We can only call this once because of Python import caching!
116+
# Fortunately, the setup it performs is the same across tests.
117+
module_name, handler_name = os.environ[_HANDLER].rsplit(".", maxsplit=1)
118+
handler_module = import_module(module_name.replace("/", "."))
119+
getattr(handler_module, handler_name)(event, MOCK_LAMBDA_CONTEXT)
120+
121+
122+
class TestAwsLambdaInstrumentor(TestBase):
123+
"""AWS Lambda Instrumentation Testsuite"""
124+
125+
@classmethod
126+
def setUpClass(cls):
127+
super().setUpClass()
128+
sys.path.append(CONFIGURE_OTEL_SDK_SCRIPTS_DIR)
129+
130+
def setUp(self):
131+
super().setUp()
132+
self.common_env_patch = mock.patch.dict(
133+
"os.environ",
134+
{
135+
AWS_LAMBDA_EXEC_WRAPPER: "mock_aws_lambda_exec_wrapper",
136+
"AWS_LAMBDA_FUNCTION_NAME": "test-python-lambda-function",
137+
"AWS_LAMBDA_FUNCTION_VERSION": "2",
138+
"AWS_REGION": "us-east-1",
139+
_HANDLER: "mocks.mock_user_lambda.handler",
140+
},
141+
)
142+
self.common_env_patch.start()
143+
144+
def tearDown(self):
145+
super().tearDown()
146+
self.common_env_patch.stop()
147+
# NOTE: We do not uninstrument the `AwsLambdaInstrumentor` here
148+
# because python caching means once we import
149+
# `configure_otel_sdk_on_lambda.py`, we cannot import it again and
150+
# therefore cannot instrument it again.
151+
152+
@classmethod
153+
def tearDownClass(cls):
154+
super().tearDownClass()
155+
sys.path.remove(CONFIGURE_OTEL_SDK_SCRIPTS_DIR)
156+
157+
def test_active_tracing(self):
158+
test_env_patch = mock.patch.dict(
159+
"os.environ",
160+
{
161+
**os.environ,
162+
# Using Active tracing
163+
_X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_SAMPLED,
164+
},
165+
)
166+
test_env_patch.start()
167+
168+
mock_execute_lambda()
169+
170+
spans = self.memory_exporter.get_finished_spans()
171+
172+
assert spans
173+
174+
self.assertEqual(len(spans), 1)
175+
span = spans[0]
176+
self.assertEqual(span.name, os.environ[ORIG_HANDLER])
177+
self.assertEqual(span.get_span_context().trace_id, MOCK_XRAY_TRACE_ID)
178+
self.assertEqual(span.kind, SpanKind.SERVER)
179+
self.assertSpanHasAttributes(
180+
span,
181+
{
182+
ResourceAttributes.FAAS_ID: MOCK_LAMBDA_CONTEXT.invoked_function_arn,
183+
SpanAttributes.FAAS_EXECUTION: MOCK_LAMBDA_CONTEXT.aws_request_id,
184+
},
185+
)
186+
187+
# TODO: Waiting on OTel Python support for setting Resource Detectors
188+
# using environment variables. Auto Instrumentation (used by this Lambda
189+
# Instrumentation) sets up the global TracerProvider which is the only
190+
# time Resource Detectors can be configured.
191+
#
192+
# We would configure this environment variable in
193+
# `patch-with-configure_otel_sdk_on_lambda.py`.
194+
#
195+
# resource_atts = span.resource.attributes
196+
# self.assertEqual(resource_atts[ResourceAttributes.CLOUD_PLATFORM], CloudPlatformValues.AWS_LAMBDA.value)
197+
# self.assertEqual(resource_atts[ResourceAttributes.CLOUD_PROVIDER], CloudProviderValues.AWS.value)
198+
# self.assertEqual(resource_atts[ResourceAttributes.CLOUD_REGION], os.environ["AWS_REGION"])
199+
# self.assertEqual(resource_atts[ResourceAttributes.FAAS_NAME], os.environ["AWS_LAMBDA_FUNCTION_NAME"])
200+
# self.assertEqual(resource_atts[ResourceAttributes.FAAS_VERSION], os.environ["AWS_LAMBDA_FUNCTION_VERSION"])
201+
202+
parent_context = span.parent
203+
self.assertEqual(
204+
parent_context.trace_id, span.get_span_context().trace_id
205+
)
206+
self.assertEqual(parent_context.span_id, MOCK_XRAY_PARENT_SPAN_ID)
207+
self.assertTrue(parent_context.is_remote)
208+
209+
test_env_patch.stop()
210+
211+
def test_parent_context_from_lambda_event(self):
212+
test_env_patch = mock.patch.dict(
213+
"os.environ",
214+
{
215+
**os.environ,
216+
# NOT Active Tracing
217+
_X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED,
218+
# NOT using the X-Ray Propagator
219+
OTEL_PROPAGATORS: "tracecontext",
220+
},
221+
)
222+
test_env_patch.start()
223+
224+
mock_execute_lambda(
225+
{
226+
"headers": {
227+
TraceContextTextMapPropagator._TRACEPARENT_HEADER_NAME: MOCK_W3C_TRACE_CONTEXT_SAMPLED,
228+
TraceContextTextMapPropagator._TRACESTATE_HEADER_NAME: f"{MOCK_W3C_TRACE_STATE_KEY}={MOCK_W3C_TRACE_STATE_VALUE},foo=1,bar=2",
229+
}
230+
}
231+
)
232+
233+
spans = self.memory_exporter.get_finished_spans()
234+
235+
assert spans
236+
237+
self.assertEqual(len(spans), 1)
238+
span = spans[0]
239+
self.assertEqual(span.get_span_context().trace_id, MOCK_W3C_TRACE_ID)
240+
241+
parent_context = span.parent
242+
self.assertEqual(
243+
parent_context.trace_id, span.get_span_context().trace_id
244+
)
245+
self.assertEqual(parent_context.span_id, MOCK_W3C_PARENT_SPAN_ID)
246+
self.assertEqual(len(parent_context.trace_state), 3)
247+
self.assertEqual(
248+
parent_context.trace_state.get(MOCK_W3C_TRACE_STATE_KEY),
249+
MOCK_W3C_TRACE_STATE_VALUE,
250+
)
251+
self.assertTrue(parent_context.is_remote)
252+
253+
test_env_patch.stop()
254+
255+
def test_using_custom_extractor(self):
256+
def custom_event_context_extractor(lambda_event):
257+
return get_global_textmap().extract(lambda_event["foo"]["headers"])
258+
259+
test_env_patch = mock.patch.dict(
260+
"os.environ",
261+
{
262+
**os.environ,
263+
# DO NOT use `patch-with-configure_otel_sdk_on_lambda.py`
264+
# script, resort to "manual" instrumentation below
265+
AWS_LAMBDA_EXEC_WRAPPER: "",
266+
# NOT Active Tracing
267+
_X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED,
268+
# NOT using the X-Ray Propagator
269+
OTEL_PROPAGATORS: "tracecontext",
270+
},
271+
)
272+
test_env_patch.start()
273+
274+
# NOTE: We uninstrument anticipating that other tests might have left it
275+
# instrumented.
276+
AwsLambdaInstrumentor().uninstrument()
277+
278+
# NOTE: Instead of using `AWS_LAMBDA_EXEC_WRAPPER` to point `_HANDLER`
279+
# to a module which instruments and calls the user `ORIG_HANDLER`, we
280+
# leave `_HANDLER` as is and replace `AWS_LAMBDA_EXEC_WRAPPER` with this
281+
# line below. This is like "manual" instrumentation for Lambda.
282+
AwsLambdaInstrumentor().instrument(
283+
event_context_extractor=custom_event_context_extractor,
284+
)
285+
286+
mock_execute_lambda(
287+
{
288+
"foo": {
289+
"headers": {
290+
TraceContextTextMapPropagator._TRACEPARENT_HEADER_NAME: MOCK_W3C_TRACE_CONTEXT_SAMPLED,
291+
TraceContextTextMapPropagator._TRACESTATE_HEADER_NAME: f"{MOCK_W3C_TRACE_STATE_KEY}={MOCK_W3C_TRACE_STATE_VALUE},foo=1,bar=2",
292+
}
293+
}
294+
}
295+
)
296+
297+
spans = self.memory_exporter.get_finished_spans()
298+
299+
assert spans
300+
301+
self.assertEqual(len(spans), 1)
302+
span = spans[0]
303+
self.assertEqual(span.get_span_context().trace_id, MOCK_W3C_TRACE_ID)
304+
305+
parent_context = span.parent
306+
self.assertEqual(
307+
parent_context.trace_id, span.get_span_context().trace_id
308+
)
309+
self.assertEqual(parent_context.span_id, MOCK_W3C_PARENT_SPAN_ID)
310+
self.assertEqual(len(parent_context.trace_state), 3)
311+
self.assertEqual(
312+
parent_context.trace_state.get(MOCK_W3C_TRACE_STATE_KEY),
313+
MOCK_W3C_TRACE_STATE_VALUE,
314+
)
315+
self.assertTrue(parent_context.is_remote)
316+
317+
test_env_patch.stop()
318+
319+
AwsLambdaInstrumentor().uninstrument()

‎tox.ini

+6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ envlist =
1717
py3{6,7,8,9}-test-instrumentation-aiopg
1818
; instrumentation-aiopg intentionally excluded from pypy3
1919

20+
; opentelemetry-instrumentation-aws_lambda
21+
py3{6,7,8,9}-test-instrumentation-aws_lambda
22+
2023
; opentelemetry-instrumentation-botocore
2124
py3{6,7,8,9}-test-instrumentation-botocore
2225
pypy3-test-instrumentation-botocore
@@ -203,6 +206,7 @@ changedir =
203206
test-instrumentation-aiopg: instrumentation/opentelemetry-instrumentation-aiopg/tests
204207
test-instrumentation-asgi: instrumentation/opentelemetry-instrumentation-asgi/tests
205208
test-instrumentation-asyncpg: instrumentation/opentelemetry-instrumentation-asyncpg/tests
209+
test-instrumentation-aws_lambda: instrumentation/opentelemetry-instrumentation-aws_lambda/tests
206210
test-instrumentation-boto: instrumentation/opentelemetry-instrumentation-boto/tests
207211
test-instrumentation-botocore: instrumentation/opentelemetry-instrumentation-botocore/tests
208212
test-instrumentation-celery: instrumentation/opentelemetry-instrumentation-celery/tests
@@ -262,6 +266,8 @@ commands_pre =
262266

263267
asyncpg: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg[test]
264268

269+
aws_lambda: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-aws_lambda[test]
270+
265271
boto: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-botocore[test]
266272
boto: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-boto[test]
267273

0 commit comments

Comments
 (0)
Please sign in to comment.