17
17
from unittest import mock
18
18
19
19
from opentelemetry .instrumentation .aws_lambda import AwsLambdaInstrumentor
20
+ from opentelemetry .propagate import get_global_textmap
20
21
from opentelemetry .sdk .extension .aws .trace .propagation .aws_xray_format import (
21
22
TRACE_ID_FIRST_PART_LENGTH ,
23
+ TRACE_ID_VERSION ,
22
24
)
23
25
from opentelemetry .semconv .resource import ResourceAttributes
24
26
from opentelemetry .semconv .trace import SpanAttributes
25
27
from opentelemetry .test .test_base import TestBase
26
28
from opentelemetry .trace import SpanKind
27
29
28
30
_HANDLER = "_HANDLER"
31
+ _X_AMZN_TRACE_ID = "_X_AMZN_TRACE_ID"
29
32
AWS_LAMBDA_EXEC_WRAPPER = "AWS_LAMBDA_EXEC_WRAPPER"
30
33
INSTRUMENTATION_SRC_DIR = os .path .join (
31
34
* (os .path .dirname (__file__ ), ".." , "otel_sdk" )
@@ -42,11 +45,24 @@ def __init__(self, aws_request_id, invoked_function_arn):
42
45
aws_request_id = "mock_aws_request_id" ,
43
46
invoked_function_arn = "arn://mock-lambda-function-arn" ,
44
47
)
45
- MOCK_TRACE_ID = 0x5FB7331105E8BB83207FA31D4D9CDB4C
46
- MOCK_TRACE_ID_HEX_STR = f"{ MOCK_TRACE_ID :32x} "
47
- MOCK_PARENT_SPAN_ID = 0x3328B8445A6DBAD2
48
- MOCK_PARENT_SPAN_ID_STR = f"{ MOCK_PARENT_SPAN_ID :32x} "
49
- MOCK_LAMBDA_TRACE_CONTEXT_SAMPLED = f"Root=1-{ MOCK_TRACE_ID_HEX_STR [:TRACE_ID_FIRST_PART_LENGTH ]} -{ MOCK_TRACE_ID_HEX_STR [TRACE_ID_FIRST_PART_LENGTH :]} ;Parent={ MOCK_PARENT_SPAN_ID_STR } ;Sampled=1"
48
+
49
+ MOCK_XRAY_TRACE_ID = 0x5FB7331105E8BB83207FA31D4D9CDB4C
50
+ MOCK_XRAY_TRACE_ID_STR = f"{ MOCK_XRAY_TRACE_ID :x} "
51
+ MOCK_XRAY_PARENT_SPAN_ID = 0x3328B8445A6DBAD2
52
+ 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} "
53
+ MOCK_XRAY_TRACE_CONTEXT_SAMPLED = f"{ MOCK_XRAY_TRACE_CONTEXT_COMMON } ;Sampled=1"
54
+ MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED = (
55
+ f"{ MOCK_XRAY_TRACE_CONTEXT_COMMON } ;Sampled=0"
56
+ )
57
+
58
+ MOCK_W3C_TRACE_ID = 0x5CE0E9A56015FEC5AADFA328AE398115
59
+ MOCK_W3C_PARENT_SPAN_ID = 0xAB54A98CEB1F0AD2
60
+ MOCK_W3C_TRACE_CONTEXT_SAMPLED = (
61
+ f"00-{ MOCK_W3C_TRACE_ID :x} -{ MOCK_W3C_PARENT_SPAN_ID :x} -01"
62
+ )
63
+
64
+ MOCK_W3C_TRACE_STATE_KEY = "vendor_specific_key"
65
+ MOCK_W3C_TRACE_STATE_VALUE = "test_value"
50
66
51
67
52
68
def mock_aws_lambda_exec_wrapper ():
@@ -63,13 +79,20 @@ def mock_aws_lambda_exec_wrapper():
63
79
exec (open (os .path .join (INSTRUMENTATION_SRC_DIR , "otel-instrument" )).read ())
64
80
65
81
66
- def mock_execute_lambda ():
82
+ def mock_execute_lambda (event = None ):
83
+ """Mocks Lambda importing and then calling the method at the current
84
+ `_HANDLER` environment variable. Like the real Lambda, if
85
+ `AWS_LAMBDA_EXEC_WRAPPER` is defined, if executes that before `_HANDLER`.
86
+
87
+ See more:
88
+ https://aws-otel.github.io/docs/getting-started/lambda/lambda-python
89
+ """
67
90
if os .environ [AWS_LAMBDA_EXEC_WRAPPER ]:
68
91
globals ()[os .environ [AWS_LAMBDA_EXEC_WRAPPER ]]()
69
92
70
93
module_name , handler_name = os .environ [_HANDLER ].split ("." )
71
94
handler_module = import_module ("." .join (module_name .split ("/" )))
72
- getattr (handler_module , handler_name )("mock_event" , MOCK_LAMBDA_CONTEXT )
95
+ getattr (handler_module , handler_name )(event , MOCK_LAMBDA_CONTEXT )
73
96
74
97
75
98
class TestAwsLambdaInstrumentor (TestBase ):
@@ -109,7 +132,7 @@ def test_active_tracing(self):
109
132
"os.environ" ,
110
133
{
111
134
** os .environ ,
112
- " _X_AMZN_TRACE_ID" : MOCK_LAMBDA_TRACE_CONTEXT_SAMPLED ,
135
+ _X_AMZN_TRACE_ID : MOCK_XRAY_TRACE_CONTEXT_SAMPLED ,
113
136
},
114
137
)
115
138
test_env_patch .start ()
@@ -123,7 +146,7 @@ def test_active_tracing(self):
123
146
self .assertEqual (len (spans ), 1 )
124
147
span = spans [0 ]
125
148
self .assertEqual (span .name , os .environ ["ORIG_HANDLER" ])
126
- self .assertEqual (span .get_span_context ().trace_id , MOCK_TRACE_ID )
149
+ self .assertEqual (span .get_span_context ().trace_id , MOCK_XRAY_TRACE_ID )
127
150
self .assertEqual (span .kind , SpanKind .SERVER )
128
151
self .assertSpanHasAttributes (
129
152
span ,
@@ -149,7 +172,112 @@ def test_active_tracing(self):
149
172
self .assertEqual (
150
173
parent_context .trace_id , span .get_span_context ().trace_id
151
174
)
152
- self .assertEqual (parent_context .span_id , MOCK_PARENT_SPAN_ID )
175
+ self .assertEqual (parent_context .span_id , MOCK_XRAY_PARENT_SPAN_ID )
176
+ self .assertTrue (parent_context .is_remote )
177
+
178
+ test_env_patch .stop ()
179
+
180
+ def test_parent_context_from_lambda_event (self ):
181
+ test_env_patch = mock .patch .dict (
182
+ "os.environ" ,
183
+ {
184
+ ** os .environ ,
185
+ # NOT Active Tracing
186
+ _X_AMZN_TRACE_ID : MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED ,
187
+ # NOT using the X-Ray Propagator
188
+ "OTEL_PROPAGATORS" : "tracecontext" ,
189
+ },
190
+ )
191
+ test_env_patch .start ()
192
+
193
+ mock_execute_lambda (
194
+ {
195
+ "headers" : {
196
+ "traceparent" : MOCK_W3C_TRACE_CONTEXT_SAMPLED ,
197
+ "tracestate" : f"{ MOCK_W3C_TRACE_STATE_KEY } ={ MOCK_W3C_TRACE_STATE_VALUE } ,foo=1,bar=2" ,
198
+ }
199
+ }
200
+ )
201
+
202
+ spans = self .memory_exporter .get_finished_spans ()
203
+
204
+ assert spans
205
+
206
+ self .assertEqual (len (spans ), 1 )
207
+ span = spans [0 ]
208
+ self .assertEqual (span .get_span_context ().trace_id , MOCK_W3C_TRACE_ID )
209
+
210
+ parent_context = span .parent
211
+ self .assertEqual (
212
+ parent_context .trace_id , span .get_span_context ().trace_id
213
+ )
214
+ self .assertEqual (parent_context .span_id , MOCK_W3C_PARENT_SPAN_ID )
215
+ self .assertEqual (len (parent_context .trace_state ), 3 )
216
+ self .assertEqual (
217
+ parent_context .trace_state .get (MOCK_W3C_TRACE_STATE_KEY ),
218
+ MOCK_W3C_TRACE_STATE_VALUE ,
219
+ )
220
+ self .assertTrue (parent_context .is_remote )
221
+
222
+ test_env_patch .stop ()
223
+
224
+ def test_using_custom_extractor (self ):
225
+ def custom_event_context_extractor (lambda_event ):
226
+ return get_global_textmap ().extract (lambda_event ["foo" ]["headers" ])
227
+
228
+ test_env_patch = mock .patch .dict (
229
+ "os.environ" ,
230
+ {
231
+ ** os .environ ,
232
+ # DO NOT use `otel-instrument` script, resort to "manual"
233
+ # instrumentation below
234
+ AWS_LAMBDA_EXEC_WRAPPER : "" ,
235
+ # NOT Active Tracing
236
+ _X_AMZN_TRACE_ID : MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED ,
237
+ # NOT using the X-Ray Propagator
238
+ "OTEL_PROPAGATORS" : "tracecontext" ,
239
+ },
240
+ )
241
+ test_env_patch .start ()
242
+
243
+ # NOTE: Instead of using `AWS_LAMBDA_EXEC_WRAPPER` to point `_HANDLER`
244
+ # to a module which instruments and calls the user `ORIG_HANDLER`, we
245
+ # leave `_HANDLER` as is and replace `AWS_LAMBDA_EXEC_WRAPPER` with this
246
+ # line below. This is like "manual" instrumentation for Lambda.
247
+ AwsLambdaInstrumentor ().instrument (
248
+ event_context_extractor = custom_event_context_extractor ,
249
+ skip_dep_check = True ,
250
+ )
251
+
252
+ mock_execute_lambda (
253
+ {
254
+ "foo" : {
255
+ "headers" : {
256
+ "traceparent" : MOCK_W3C_TRACE_CONTEXT_SAMPLED ,
257
+ "tracestate" : f"{ MOCK_W3C_TRACE_STATE_KEY } ={ MOCK_W3C_TRACE_STATE_VALUE } ,foo=1,bar=2" ,
258
+ }
259
+ }
260
+ }
261
+ )
262
+
263
+ spans = self .memory_exporter .get_finished_spans ()
264
+
265
+ assert spans
266
+
267
+ self .assertEqual (len (spans ), 1 )
268
+ span = spans [0 ]
269
+ self .assertEqual (span .get_span_context ().trace_id , MOCK_W3C_TRACE_ID )
270
+
271
+ parent_context = span .parent
272
+ self .assertEqual (
273
+ parent_context .trace_id , span .get_span_context ().trace_id
274
+ )
275
+ self .assertEqual (parent_context .span_id , MOCK_W3C_PARENT_SPAN_ID )
276
+ self .assertEqual (len (parent_context .trace_state ), 3 )
277
+ self .assertEqual (
278
+ parent_context .trace_state .get (MOCK_W3C_TRACE_STATE_KEY ),
279
+ MOCK_W3C_TRACE_STATE_VALUE ,
280
+ )
153
281
self .assertTrue (parent_context .is_remote )
154
282
155
283
test_env_patch .stop ()
0 commit comments