Skip to content

Commit 9ce1df0

Browse files
committed
Only test Python auto instrumentation wrapper
1 parent e6e7905 commit 9ce1df0

File tree

3 files changed

+131
-95
lines changed

3 files changed

+131
-95
lines changed

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"

Diff for: python/src/otel/tests/test_otel.py renamed to python/src/otel/tests/test_aws_lambda_instrumentation-auto.py

+114-93
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,20 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
import fileinput
1415
import os
1516
import sys
16-
from importlib import import_module
17+
from importlib import import_module, reload
18+
from shutil import which
1719
from unittest import mock
1820

19-
from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor
20-
from opentelemetry.propagate import get_global_textmap
21+
from opentelemetry.environment_variables import OTEL_PROPAGATORS
22+
from opentelemetry.instrumentation.aws_lambda import (
23+
_HANDLER,
24+
_X_AMZN_TRACE_ID,
25+
ORIG_HANDLER,
26+
AwsLambdaInstrumentor,
27+
)
2128
from opentelemetry.sdk.extension.aws.trace.propagation.aws_xray_format import (
2229
TRACE_ID_FIRST_PART_LENGTH,
2330
TRACE_ID_VERSION,
@@ -26,13 +33,15 @@
2633
from opentelemetry.semconv.trace import SpanAttributes
2734
from opentelemetry.test.test_base import TestBase
2835
from opentelemetry.trace import SpanKind
36+
from opentelemetry.trace.propagation.tracecontext import (
37+
TraceContextTextMapPropagator,
38+
)
2939

30-
_HANDLER = "_HANDLER"
31-
_X_AMZN_TRACE_ID = "_X_AMZN_TRACE_ID"
3240
AWS_LAMBDA_EXEC_WRAPPER = "AWS_LAMBDA_EXEC_WRAPPER"
33-
INSTRUMENTATION_SRC_DIR = os.path.join(
34-
*(os.path.dirname(__file__), "..", "otel_sdk")
41+
CONFIGURE_OTEL_SDK_SCRIPTS_DIR = os.path.join(
42+
*(os.path.dirname(__file__), "..", "scripts")
3543
)
44+
TOX_PYTHON_DIRECTORY = os.path.dirname(os.path.dirname(which("python3")))
3645

3746

3847
class MockLambdaContext:
@@ -55,8 +64,9 @@ def __init__(self, aws_request_id, invoked_function_arn):
5564
f"{MOCK_XRAY_TRACE_CONTEXT_COMMON};Sampled=0"
5665
)
5766

58-
# Read more:
67+
# See more:
5968
# https://www.w3.org/TR/trace-context/#examples-of-http-traceparent-headers
69+
6070
MOCK_W3C_TRACE_ID = 0x5CE0E9A56015FEC5AADFA328AE398115
6171
MOCK_W3C_PARENT_SPAN_ID = 0xAB54A98CEB1F0AD2
6272
MOCK_W3C_TRACE_CONTEXT_SAMPLED = (
@@ -67,33 +77,84 @@ def __init__(self, aws_request_id, invoked_function_arn):
6777
MOCK_W3C_TRACE_STATE_VALUE = "test_value"
6878

6979

80+
def replace_in_file(file_path, search_text, new_text):
81+
with fileinput.input(file_path, inplace=True) as file_object:
82+
for line in file_object:
83+
new_line = line.replace(search_text, new_text)
84+
# This directs the output to the file, not the console
85+
print(new_line, end="")
86+
87+
7088
def mock_aws_lambda_exec_wrapper():
7189
"""Mocks automatically instrumenting user Lambda function by pointing
7290
`AWS_LAMBDA_EXEC_WRAPPER` to the `otel-instrument` script.
7391
74-
TODO: It would be better if `moto`'s `mock_lambda` supported setting
75-
AWS_LAMBDA_EXEC_WRAPPER so we could make the call to Lambda instead.
76-
7792
See more:
7893
https://aws-otel.github.io/docs/getting-started/lambda/lambda-python
7994
"""
80-
# NOTE: AwsLambdaInstrumentor().instrument() is done at this point
81-
exec(open(os.path.join(INSTRUMENTATION_SRC_DIR, "otel-instrument")).read())
95+
96+
# NOTE: Do NOT run as a sub process because this script needs to update
97+
# the environment variables in this same process.
98+
99+
original_sys_argv = sys.argv
100+
otel_instrument_file = os.path.join(
101+
CONFIGURE_OTEL_SDK_SCRIPTS_DIR, "otel-instrument"
102+
)
103+
sys.argv = [
104+
otel_instrument_file,
105+
which("python3"),
106+
"-c",
107+
"pass",
108+
]
109+
with open(otel_instrument_file) as config_otel_script:
110+
exec(config_otel_script.read())
111+
sys.argv = original_sys_argv
82112

83113

84114
def mock_execute_lambda(event=None):
85-
"""Mocks Lambda importing and then calling the method at the current
86-
`_HANDLER` environment variable. Like the real Lambda, if
87-
`AWS_LAMBDA_EXEC_WRAPPER` is defined, if executes that before `_HANDLER`.
115+
"""Mocks the AWS Lambda execution. Like the real Lambda, if
116+
`AWS_LAMBDA_EXEC_WRAPPER` is defined, it calls that script first.
117+
118+
NOTE: Normally AWS Lambda would give the script the arguments used to start
119+
the program. We don't do that because we want to give the code below which
120+
mocks the `/var/runtime/bootstrap.py` starter file different Lambda event
121+
test cases. We don't want `bootstrap.py` to constrcut them.
122+
123+
NOTE: We can't use `moto`'s `mock_lambda` because it does not support
124+
AWS_LAMBDA_EXEC_WRAPPER and doesn't mimic the reload behavior we have here.
88125
89126
See more:
90-
https://aws-otel.github.io/docs/getting-started/lambda/lambda-python
127+
https://docs.aws.amazon.com/lambda/latest/dg/runtimes-modify.html#runtime-wrapper
128+
129+
Args:
130+
event: The Lambda event which may or may not be used by instrumentation.
91131
"""
92132
if os.environ[AWS_LAMBDA_EXEC_WRAPPER]:
93133
globals()[os.environ[AWS_LAMBDA_EXEC_WRAPPER]]()
94134

95-
module_name, handler_name = os.environ[_HANDLER].split(".")
96-
handler_module = import_module(".".join(module_name.split("/")))
135+
# NOTE: Mocks Lambda's `python3 /var/runtime/bootstrap.py`. Which _reloads_
136+
# the import of a module using the deprecated `imp.load_module`. This is
137+
# prevents us from simply using `otel-instrument`, and what requires that we
138+
# use `otel_wrapper.py` as well.
139+
#
140+
# See more:
141+
# https://docs.python.org/3/library/imp.html#imp.load_module
142+
143+
module_name, handler_name = os.environ[_HANDLER].rsplit(".", 1)
144+
handler_module = import_module(module_name.replace("/", "."))
145+
146+
# NOTE: The first time, this reload produces a `warning` that we are
147+
# "Attempting to instrument while already instrumented". This is fine
148+
# because we are simulating Lambda's "reloading" import so we
149+
# instrument twice.
150+
#
151+
# TODO: (NathanielRN) On subsequent tests, the first import above does
152+
# not run `instrument()` on import. Only `reload` below will run it, no
153+
# warning appears in the logs. Instrumentation still works fine if we
154+
# remove the reload. Not sure why this happens.
155+
156+
handler_module = reload(handler_module)
157+
97158
getattr(handler_module, handler_name)(event, MOCK_LAMBDA_CONTEXT)
98159

99160

@@ -103,7 +164,12 @@ class TestAwsLambdaInstrumentor(TestBase):
103164
@classmethod
104165
def setUpClass(cls):
105166
super().setUpClass()
106-
sys.path.append(INSTRUMENTATION_SRC_DIR)
167+
sys.path.append(CONFIGURE_OTEL_SDK_SCRIPTS_DIR)
168+
replace_in_file(
169+
os.path.join(CONFIGURE_OTEL_SDK_SCRIPTS_DIR, "otel-instrument"),
170+
'LAMBDA_LAYER_PKGS_DIR = os.path.abspath(os.path.join(os.sep, "opt", "python"))',
171+
f'LAMBDA_LAYER_PKGS_DIR = "{TOX_PYTHON_DIRECTORY}"',
172+
)
107173

108174
def setUp(self):
109175
super().setUp()
@@ -114,11 +180,16 @@ def setUp(self):
114180
"AWS_LAMBDA_FUNCTION_NAME": "test-python-lambda-function",
115181
"AWS_LAMBDA_FUNCTION_VERSION": "2",
116182
"AWS_REGION": "us-east-1",
117-
_HANDLER: "mock_user_lambda.handler",
183+
_HANDLER: "mocks.lambda_function.handler",
184+
"LAMBDA_RUNTIME_DIR": "mock-directory-since-tox-knows-pkgs-loc",
118185
},
119186
)
120187
self.common_env_patch.start()
121188

189+
# NOTE: Whether AwsLambdaInstrumentor().instrument() is run is decided
190+
# by each test case. It depends on if the test is for auto or manual
191+
# instrumentation.
192+
122193
def tearDown(self):
123194
super().tearDown()
124195
self.common_env_patch.stop()
@@ -127,13 +198,19 @@ def tearDown(self):
127198
@classmethod
128199
def tearDownClass(cls):
129200
super().tearDownClass()
130-
sys.path.remove(INSTRUMENTATION_SRC_DIR)
201+
sys.path.remove(CONFIGURE_OTEL_SDK_SCRIPTS_DIR)
202+
replace_in_file(
203+
os.path.join(CONFIGURE_OTEL_SDK_SCRIPTS_DIR, "otel-instrument"),
204+
f'LAMBDA_LAYER_PKGS_DIR = "{TOX_PYTHON_DIRECTORY}"',
205+
'LAMBDA_LAYER_PKGS_DIR = os.path.abspath(os.path.join(os.sep, "opt", "python"))',
206+
)
131207

132208
def test_active_tracing(self):
133209
test_env_patch = mock.patch.dict(
134210
"os.environ",
135211
{
136212
**os.environ,
213+
# Using Active tracing
137214
_X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_SAMPLED,
138215
},
139216
)
@@ -147,7 +224,7 @@ def test_active_tracing(self):
147224

148225
self.assertEqual(len(spans), 1)
149226
span = spans[0]
150-
self.assertEqual(span.name, os.environ["ORIG_HANDLER"])
227+
self.assertEqual(span.name, os.environ[ORIG_HANDLER])
151228
self.assertEqual(span.get_span_context().trace_id, MOCK_XRAY_TRACE_ID)
152229
self.assertEqual(span.kind, SpanKind.SERVER)
153230
self.assertSpanHasAttributes(
@@ -163,12 +240,17 @@ def test_active_tracing(self):
163240
# Instrumentation) sets up the global TracerProvider which is the only
164241
# time Resource Detectors can be configured.
165242
#
166-
# resource_atts = span.resource.attributes
167-
# self.assertEqual(resource_atts[ResourceAttributes.CLOUD_PLATFORM], CloudPlatformValues.AWS_LAMBDA.value)
168-
# self.assertEqual(resource_atts[ResourceAttributes.CLOUD_PROVIDER], CloudProviderValues.AWS.value)
169-
# self.assertEqual(resource_atts[ResourceAttributes.CLOUD_REGION], os.environ["AWS_REGION"])
170-
# self.assertEqual(resource_atts[ResourceAttributes.FAAS_NAME], os.environ["AWS_LAMBDA_FUNCTION_NAME"])
171-
# self.assertEqual(resource_atts[ResourceAttributes.FAAS_VERSION], os.environ["AWS_LAMBDA_FUNCTION_VERSION"])
243+
# environ["OTEL_RESOURCE_DETECTORS"] = "aws_lambda"
244+
#
245+
# We would configure this environment variable in
246+
# `otel-instrument`.
247+
#
248+
# res_atts = span.resource.attributes
249+
# self.assertEqual(res_atts[ResourceAttributes.CLOUD_PLATFORM], CloudPlatformValues.AWS_LAMBDA.value)
250+
# self.assertEqual(res_atts[ResourceAttributes.CLOUD_PROVIDER], CloudProviderValues.AWS.value)
251+
# self.assertEqual(res_atts[ResourceAttributes.CLOUD_REGION], os.environ["AWS_REGION"])
252+
# self.assertEqual(res_atts[ResourceAttributes.FAAS_NAME], os.environ["AWS_LAMBDA_FUNCTION_NAME"])
253+
# self.assertEqual(res_atts[ResourceAttributes.FAAS_VERSION], os.environ["AWS_LAMBDA_FUNCTION_VERSION"])
172254

173255
parent_context = span.parent
174256
self.assertEqual(
@@ -187,77 +269,16 @@ def test_parent_context_from_lambda_event(self):
187269
# NOT Active Tracing
188270
_X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED,
189271
# NOT using the X-Ray Propagator
190-
"OTEL_PROPAGATORS": "tracecontext",
272+
OTEL_PROPAGATORS: "tracecontext",
191273
},
192274
)
193275
test_env_patch.start()
194276

195277
mock_execute_lambda(
196278
{
197279
"headers": {
198-
"traceparent": MOCK_W3C_TRACE_CONTEXT_SAMPLED,
199-
"tracestate": f"{MOCK_W3C_TRACE_STATE_KEY}={MOCK_W3C_TRACE_STATE_VALUE},foo=1,bar=2",
200-
}
201-
}
202-
)
203-
204-
spans = self.memory_exporter.get_finished_spans()
205-
206-
assert spans
207-
208-
self.assertEqual(len(spans), 1)
209-
span = spans[0]
210-
self.assertEqual(span.get_span_context().trace_id, MOCK_W3C_TRACE_ID)
211-
212-
parent_context = span.parent
213-
self.assertEqual(
214-
parent_context.trace_id, span.get_span_context().trace_id
215-
)
216-
self.assertEqual(parent_context.span_id, MOCK_W3C_PARENT_SPAN_ID)
217-
self.assertEqual(len(parent_context.trace_state), 3)
218-
self.assertEqual(
219-
parent_context.trace_state.get(MOCK_W3C_TRACE_STATE_KEY),
220-
MOCK_W3C_TRACE_STATE_VALUE,
221-
)
222-
self.assertTrue(parent_context.is_remote)
223-
224-
test_env_patch.stop()
225-
226-
def test_using_custom_extractor(self):
227-
def custom_event_context_extractor(lambda_event):
228-
return get_global_textmap().extract(lambda_event["foo"]["headers"])
229-
230-
test_env_patch = mock.patch.dict(
231-
"os.environ",
232-
{
233-
**os.environ,
234-
# DO NOT use `otel-instrument` script, resort to "manual"
235-
# instrumentation below
236-
AWS_LAMBDA_EXEC_WRAPPER: "",
237-
# NOT Active Tracing
238-
_X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED,
239-
# NOT using the X-Ray Propagator
240-
"OTEL_PROPAGATORS": "tracecontext",
241-
},
242-
)
243-
test_env_patch.start()
244-
245-
# NOTE: Instead of using `AWS_LAMBDA_EXEC_WRAPPER` to point `_HANDLER`
246-
# to a module which instruments and calls the user `ORIG_HANDLER`, we
247-
# leave `_HANDLER` as is and replace `AWS_LAMBDA_EXEC_WRAPPER` with this
248-
# line below. This is like "manual" instrumentation for Lambda.
249-
AwsLambdaInstrumentor().instrument(
250-
event_context_extractor=custom_event_context_extractor,
251-
skip_dep_check=True,
252-
)
253-
254-
mock_execute_lambda(
255-
{
256-
"foo": {
257-
"headers": {
258-
"traceparent": MOCK_W3C_TRACE_CONTEXT_SAMPLED,
259-
"tracestate": f"{MOCK_W3C_TRACE_STATE_KEY}={MOCK_W3C_TRACE_STATE_VALUE},foo=1,bar=2",
260-
}
280+
TraceContextTextMapPropagator._TRACEPARENT_HEADER_NAME: MOCK_W3C_TRACE_CONTEXT_SAMPLED,
281+
TraceContextTextMapPropagator._TRACESTATE_HEADER_NAME: f"{MOCK_W3C_TRACE_STATE_KEY}={MOCK_W3C_TRACE_STATE_VALUE},foo=1,bar=2",
261282
}
262283
}
263284
)

0 commit comments

Comments
 (0)