11
11
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
# See the License for the specific language governing permissions and
13
13
# limitations under the License.
14
+ import fileinput
14
15
import os
15
16
import sys
16
- from importlib import import_module
17
+ from importlib import import_module , reload
18
+ from shutil import which
17
19
from unittest import mock
18
20
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
+ )
21
28
from opentelemetry .sdk .extension .aws .trace .propagation .aws_xray_format import (
22
29
TRACE_ID_FIRST_PART_LENGTH ,
23
30
TRACE_ID_VERSION ,
26
33
from opentelemetry .semconv .trace import SpanAttributes
27
34
from opentelemetry .test .test_base import TestBase
28
35
from opentelemetry .trace import SpanKind
36
+ from opentelemetry .trace .propagation .tracecontext import (
37
+ TraceContextTextMapPropagator ,
38
+ )
29
39
30
- _HANDLER = "_HANDLER"
31
- _X_AMZN_TRACE_ID = "_X_AMZN_TRACE_ID"
32
40
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 " )
35
43
)
44
+ TOX_PYTHON_DIRECTORY = os .path .dirname (os .path .dirname (which ("python3" )))
36
45
37
46
38
47
class MockLambdaContext :
@@ -55,8 +64,9 @@ def __init__(self, aws_request_id, invoked_function_arn):
55
64
f"{ MOCK_XRAY_TRACE_CONTEXT_COMMON } ;Sampled=0"
56
65
)
57
66
58
- # Read more:
67
+ # See more:
59
68
# https://www.w3.org/TR/trace-context/#examples-of-http-traceparent-headers
69
+
60
70
MOCK_W3C_TRACE_ID = 0x5CE0E9A56015FEC5AADFA328AE398115
61
71
MOCK_W3C_PARENT_SPAN_ID = 0xAB54A98CEB1F0AD2
62
72
MOCK_W3C_TRACE_CONTEXT_SAMPLED = (
@@ -67,33 +77,84 @@ def __init__(self, aws_request_id, invoked_function_arn):
67
77
MOCK_W3C_TRACE_STATE_VALUE = "test_value"
68
78
69
79
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
+
70
88
def mock_aws_lambda_exec_wrapper ():
71
89
"""Mocks automatically instrumenting user Lambda function by pointing
72
90
`AWS_LAMBDA_EXEC_WRAPPER` to the `otel-instrument` script.
73
91
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
-
77
92
See more:
78
93
https://aws-otel.github.io/docs/getting-started/lambda/lambda-python
79
94
"""
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
82
112
83
113
84
114
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.
88
125
89
126
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.
91
131
"""
92
132
if os .environ [AWS_LAMBDA_EXEC_WRAPPER ]:
93
133
globals ()[os .environ [AWS_LAMBDA_EXEC_WRAPPER ]]()
94
134
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
+
97
158
getattr (handler_module , handler_name )(event , MOCK_LAMBDA_CONTEXT )
98
159
99
160
@@ -103,7 +164,12 @@ class TestAwsLambdaInstrumentor(TestBase):
103
164
@classmethod
104
165
def setUpClass (cls ):
105
166
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
+ )
107
173
108
174
def setUp (self ):
109
175
super ().setUp ()
@@ -114,11 +180,16 @@ def setUp(self):
114
180
"AWS_LAMBDA_FUNCTION_NAME" : "test-python-lambda-function" ,
115
181
"AWS_LAMBDA_FUNCTION_VERSION" : "2" ,
116
182
"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" ,
118
185
},
119
186
)
120
187
self .common_env_patch .start ()
121
188
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
+
122
193
def tearDown (self ):
123
194
super ().tearDown ()
124
195
self .common_env_patch .stop ()
@@ -127,13 +198,19 @@ def tearDown(self):
127
198
@classmethod
128
199
def tearDownClass (cls ):
129
200
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
+ )
131
207
132
208
def test_active_tracing (self ):
133
209
test_env_patch = mock .patch .dict (
134
210
"os.environ" ,
135
211
{
136
212
** os .environ ,
213
+ # Using Active tracing
137
214
_X_AMZN_TRACE_ID : MOCK_XRAY_TRACE_CONTEXT_SAMPLED ,
138
215
},
139
216
)
@@ -147,7 +224,7 @@ def test_active_tracing(self):
147
224
148
225
self .assertEqual (len (spans ), 1 )
149
226
span = spans [0 ]
150
- self .assertEqual (span .name , os .environ [" ORIG_HANDLER" ])
227
+ self .assertEqual (span .name , os .environ [ORIG_HANDLER ])
151
228
self .assertEqual (span .get_span_context ().trace_id , MOCK_XRAY_TRACE_ID )
152
229
self .assertEqual (span .kind , SpanKind .SERVER )
153
230
self .assertSpanHasAttributes (
@@ -163,12 +240,17 @@ def test_active_tracing(self):
163
240
# Instrumentation) sets up the global TracerProvider which is the only
164
241
# time Resource Detectors can be configured.
165
242
#
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"])
172
254
173
255
parent_context = span .parent
174
256
self .assertEqual (
@@ -187,77 +269,16 @@ def test_parent_context_from_lambda_event(self):
187
269
# NOT Active Tracing
188
270
_X_AMZN_TRACE_ID : MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED ,
189
271
# NOT using the X-Ray Propagator
190
- " OTEL_PROPAGATORS" : "tracecontext" ,
272
+ OTEL_PROPAGATORS : "tracecontext" ,
191
273
},
192
274
)
193
275
test_env_patch .start ()
194
276
195
277
mock_execute_lambda (
196
278
{
197
279
"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" ,
261
282
}
262
283
}
263
284
)
0 commit comments