Skip to content

Commit 7603a1f

Browse files
tsloughterronyissrikanthccvshalevrocelotl
authored
update awslambda to use _X_AMZN_TRACE_ID as a Span Link (open-telemetry#1657)
Co-authored-by: Ron Yishai <[email protected]> Co-authored-by: Srikanth Chekuri <[email protected]> Co-authored-by: Shalev Roda <[email protected]> Co-authored-by: Diego Hurtado <[email protected]>
1 parent 74fcbf4 commit 7603a1f

File tree

4 files changed

+104
-114
lines changed

4 files changed

+104
-114
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
150150
([#1592](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1592))
151151
- `opentelemetry-instrumentation-django` Allow explicit `excluded_urls` configuration through `instrument()`
152152
([#1618](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1618))
153+
- `opentelemetry-instrumentation-aws-lambda` Use env var `_X_AMZN_TRACE_ID` as a
154+
Span Link instead of parent
155+
([#1657](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1657))
153156

154157
### Fixed
155158

instrumentation/opentelemetry-instrumentation-aws-lambda/src/opentelemetry/instrumentation/aws_lambda/__init__.py

+35-45
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def custom_event_context_extractor(lambda_event):
7171
import os
7272
import time
7373
from importlib import import_module
74-
from typing import Any, Callable, Collection
74+
from typing import Any, Callable, Collection, Optional, Sequence
7575
from urllib.parse import urlencode
7676

7777
from wrapt import wrap_function_wrapper
@@ -90,6 +90,7 @@ def custom_event_context_extractor(lambda_event):
9090
from opentelemetry.semconv.resource import ResourceAttributes
9191
from opentelemetry.semconv.trace import SpanAttributes
9292
from opentelemetry.trace import (
93+
Link,
9394
Span,
9495
SpanKind,
9596
TracerProvider,
@@ -106,9 +107,6 @@ def custom_event_context_extractor(lambda_event):
106107
OTEL_INSTRUMENTATION_AWS_LAMBDA_FLUSH_TIMEOUT = (
107108
"OTEL_INSTRUMENTATION_AWS_LAMBDA_FLUSH_TIMEOUT"
108109
)
109-
OTEL_LAMBDA_DISABLE_AWS_CONTEXT_PROPAGATION = (
110-
"OTEL_LAMBDA_DISABLE_AWS_CONTEXT_PROPAGATION"
111-
)
112110

113111

114112
def _default_event_context_extractor(lambda_event: Any) -> Context:
@@ -142,14 +140,12 @@ def _default_event_context_extractor(lambda_event: Any) -> Context:
142140

143141

144142
def _determine_parent_context(
145-
lambda_event: Any,
146-
event_context_extractor: Callable[[Any], Context],
147-
disable_aws_context_propagation: bool = False,
143+
lambda_event: Any, event_context_extractor: Callable[[Any], Context]
148144
) -> Context:
149145
"""Determine the parent context for the current Lambda invocation.
150146
151147
See more:
152-
https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/aws-lambda.md#determining-the-parent-of-a-span
148+
https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/aws-lambda.md
153149
154150
Args:
155151
lambda_event: user-defined, so it could be anything, but this
@@ -158,30 +154,11 @@ def _determine_parent_context(
158154
Event as input and extracts an OTel Context from it. By default,
159155
the context is extracted from the HTTP headers of an API Gateway
160156
request.
161-
disable_aws_context_propagation: By default, this instrumentation
162-
will try to read the context from the `_X_AMZN_TRACE_ID` environment
163-
variable set by Lambda, set this to `True` to disable this behavior.
164157
Returns:
165158
A Context with configuration found in the carrier.
166159
"""
167160
parent_context = None
168161

169-
if not disable_aws_context_propagation:
170-
xray_env_var = os.environ.get(_X_AMZN_TRACE_ID)
171-
172-
if xray_env_var:
173-
parent_context = AwsXRayPropagator().extract(
174-
{TRACE_HEADER_KEY: xray_env_var}
175-
)
176-
177-
if (
178-
parent_context
179-
and get_current_span(parent_context)
180-
.get_span_context()
181-
.trace_flags.sampled
182-
):
183-
return parent_context
184-
185162
if event_context_extractor:
186163
parent_context = event_context_extractor(lambda_event)
187164
else:
@@ -190,6 +167,33 @@ def _determine_parent_context(
190167
return parent_context
191168

192169

170+
def _determine_links() -> Optional[Sequence[Link]]:
171+
"""Determine if a Link should be added to the Span based on the
172+
environment variable `_X_AMZN_TRACE_ID`.
173+
174+
175+
See more:
176+
https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/aws-lambda.md#aws-x-ray-environment-span-link
177+
178+
Returns:
179+
A Link or None
180+
"""
181+
links = None
182+
183+
xray_env_var = os.environ.get(_X_AMZN_TRACE_ID)
184+
185+
if xray_env_var:
186+
env_context = AwsXRayPropagator().extract(
187+
{TRACE_HEADER_KEY: xray_env_var}
188+
)
189+
190+
span_context = get_current_span(env_context).get_span_context()
191+
if span_context.is_valid:
192+
links = [Link(span_context, {"source": "x-ray-env"})]
193+
194+
return links
195+
196+
193197
def _set_api_gateway_v1_proxy_attributes(
194198
lambda_event: Any, span: Span
195199
) -> Span:
@@ -284,7 +288,6 @@ def _instrument(
284288
flush_timeout,
285289
event_context_extractor: Callable[[Any], Context],
286290
tracer_provider: TracerProvider = None,
287-
disable_aws_context_propagation: bool = False,
288291
meter_provider: MeterProvider = None,
289292
):
290293
def _instrumented_lambda_handler_call( # noqa pylint: disable=too-many-branches
@@ -297,11 +300,11 @@ def _instrumented_lambda_handler_call( # noqa pylint: disable=too-many-branches
297300
lambda_event = args[0]
298301

299302
parent_context = _determine_parent_context(
300-
lambda_event,
301-
event_context_extractor,
302-
disable_aws_context_propagation,
303+
lambda_event, event_context_extractor
303304
)
304305

306+
links = _determine_links()
307+
305308
span_kind = None
306309
try:
307310
if lambda_event["Records"][0]["eventSource"] in {
@@ -327,6 +330,7 @@ def _instrumented_lambda_handler_call( # noqa pylint: disable=too-many-branches
327330
name=orig_handler_name,
328331
context=parent_context,
329332
kind=span_kind,
333+
links=links,
330334
) as span:
331335
if span.is_recording():
332336
lambda_context = args[1]
@@ -420,9 +424,6 @@ def _instrument(self, **kwargs):
420424
Event as input and extracts an OTel Context from it. By default,
421425
the context is extracted from the HTTP headers of an API Gateway
422426
request.
423-
``disable_aws_context_propagation``: By default, this instrumentation
424-
will try to read the context from the `_X_AMZN_TRACE_ID` environment
425-
variable set by Lambda, set this to `True` to disable this behavior.
426427
"""
427428
lambda_handler = os.environ.get(ORIG_HANDLER, os.environ.get(_HANDLER))
428429
# pylint: disable=attribute-defined-outside-init
@@ -444,16 +445,6 @@ def _instrument(self, **kwargs):
444445
flush_timeout_env,
445446
)
446447

447-
disable_aws_context_propagation = kwargs.get(
448-
"disable_aws_context_propagation", False
449-
) or os.getenv(
450-
OTEL_LAMBDA_DISABLE_AWS_CONTEXT_PROPAGATION, "False"
451-
).strip().lower() in (
452-
"true",
453-
"1",
454-
"t",
455-
)
456-
457448
_instrument(
458449
self._wrapped_module_name,
459450
self._wrapped_function_name,
@@ -462,7 +453,6 @@ def _instrument(self, **kwargs):
462453
"event_context_extractor", _default_event_context_extractor
463454
),
464455
tracer_provider=kwargs.get("tracer_provider"),
465-
disable_aws_context_propagation=disable_aws_context_propagation,
466456
meter_provider=kwargs.get("meter_provider"),
467457
)
468458

instrumentation/opentelemetry-instrumentation-aws-lambda/tests/test_aws_lambda_instrumentation_manual.py

+65-68
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
_HANDLER,
2828
_X_AMZN_TRACE_ID,
2929
OTEL_INSTRUMENTATION_AWS_LAMBDA_FLUSH_TIMEOUT,
30-
OTEL_LAMBDA_DISABLE_AWS_CONTEXT_PROPAGATION,
3130
AwsLambdaInstrumentor,
3231
)
3332
from opentelemetry.propagate import get_global_textmap
@@ -138,7 +137,9 @@ def test_active_tracing(self):
138137
self.assertEqual(len(spans), 1)
139138
span = spans[0]
140139
self.assertEqual(span.name, os.environ[_HANDLER])
141-
self.assertEqual(span.get_span_context().trace_id, MOCK_XRAY_TRACE_ID)
140+
self.assertNotEqual(
141+
span.get_span_context().trace_id, MOCK_XRAY_TRACE_ID
142+
)
142143
self.assertEqual(span.kind, SpanKind.SERVER)
143144
self.assertSpanHasAttributes(
144145
span,
@@ -149,11 +150,7 @@ def test_active_tracing(self):
149150
)
150151

151152
parent_context = span.parent
152-
self.assertEqual(
153-
parent_context.trace_id, span.get_span_context().trace_id
154-
)
155-
self.assertEqual(parent_context.span_id, MOCK_XRAY_PARENT_SPAN_ID)
156-
self.assertTrue(parent_context.is_remote)
153+
self.assertEqual(None, parent_context)
157154

158155
test_env_patch.stop()
159156

@@ -165,11 +162,8 @@ class TestCase:
165162
context: Dict
166163
expected_traceid: int
167164
expected_parentid: int
168-
xray_traceid: str
169165
expected_state_value: str = None
170166
expected_trace_state_len: int = 0
171-
disable_aws_context_propagation: bool = False
172-
disable_aws_context_propagation_envvar: str = ""
173167

174168
def custom_event_context_extractor(lambda_event):
175169
return get_global_textmap().extract(lambda_event["foo"]["headers"])
@@ -188,42 +182,9 @@ def custom_event_context_extractor(lambda_event):
188182
expected_parentid=MOCK_W3C_PARENT_SPAN_ID,
189183
expected_trace_state_len=3,
190184
expected_state_value=MOCK_W3C_TRACE_STATE_VALUE,
191-
xray_traceid=MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED,
192-
),
193-
TestCase(
194-
name="custom_extractor_not_sampled_xray",
195-
custom_extractor=custom_event_context_extractor,
196-
context={
197-
"foo": {
198-
"headers": {
199-
TraceContextTextMapPropagator._TRACEPARENT_HEADER_NAME: MOCK_W3C_TRACE_CONTEXT_SAMPLED,
200-
TraceContextTextMapPropagator._TRACESTATE_HEADER_NAME: f"{MOCK_W3C_TRACE_STATE_KEY}={MOCK_W3C_TRACE_STATE_VALUE},foo=1,bar=2",
201-
}
202-
}
203-
},
204-
expected_traceid=MOCK_W3C_TRACE_ID,
205-
expected_parentid=MOCK_W3C_PARENT_SPAN_ID,
206-
expected_trace_state_len=3,
207-
expected_state_value=MOCK_W3C_TRACE_STATE_VALUE,
208-
xray_traceid=MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED,
209-
),
210-
TestCase(
211-
name="custom_extractor_sampled_xray",
212-
custom_extractor=custom_event_context_extractor,
213-
context={
214-
"foo": {
215-
"headers": {
216-
TraceContextTextMapPropagator._TRACEPARENT_HEADER_NAME: MOCK_W3C_TRACE_CONTEXT_SAMPLED,
217-
TraceContextTextMapPropagator._TRACESTATE_HEADER_NAME: f"{MOCK_W3C_TRACE_STATE_KEY}={MOCK_W3C_TRACE_STATE_VALUE},foo=1,bar=2",
218-
}
219-
}
220-
},
221-
expected_traceid=MOCK_XRAY_TRACE_ID,
222-
expected_parentid=MOCK_XRAY_PARENT_SPAN_ID,
223-
xray_traceid=MOCK_XRAY_TRACE_CONTEXT_SAMPLED,
224185
),
225186
TestCase(
226-
name="custom_extractor_sampled_xray_disable_aws_propagation",
187+
name="custom_extractor",
227188
custom_extractor=custom_event_context_extractor,
228189
context={
229190
"foo": {
@@ -233,47 +194,24 @@ def custom_event_context_extractor(lambda_event):
233194
}
234195
}
235196
},
236-
disable_aws_context_propagation=True,
237197
expected_traceid=MOCK_W3C_TRACE_ID,
238198
expected_parentid=MOCK_W3C_PARENT_SPAN_ID,
239199
expected_trace_state_len=3,
240200
expected_state_value=MOCK_W3C_TRACE_STATE_VALUE,
241-
xray_traceid=MOCK_XRAY_TRACE_CONTEXT_SAMPLED,
242-
),
243-
TestCase(
244-
name="no_custom_extractor_xray_disable_aws_propagation_via_env_var",
245-
custom_extractor=None,
246-
context={
247-
"headers": {
248-
TraceContextTextMapPropagator._TRACEPARENT_HEADER_NAME: MOCK_W3C_TRACE_CONTEXT_SAMPLED,
249-
TraceContextTextMapPropagator._TRACESTATE_HEADER_NAME: f"{MOCK_W3C_TRACE_STATE_KEY}={MOCK_W3C_TRACE_STATE_VALUE},foo=1,bar=2",
250-
}
251-
},
252-
disable_aws_context_propagation=False,
253-
disable_aws_context_propagation_envvar="true",
254-
expected_traceid=MOCK_W3C_TRACE_ID,
255-
expected_parentid=MOCK_W3C_PARENT_SPAN_ID,
256-
expected_trace_state_len=3,
257-
expected_state_value=MOCK_W3C_TRACE_STATE_VALUE,
258-
xray_traceid=MOCK_XRAY_TRACE_CONTEXT_SAMPLED,
259201
),
260202
]
261203
for test in tests:
262204
test_env_patch = mock.patch.dict(
263205
"os.environ",
264206
{
265207
**os.environ,
266-
# NOT Active Tracing
267-
_X_AMZN_TRACE_ID: test.xray_traceid,
268-
OTEL_LAMBDA_DISABLE_AWS_CONTEXT_PROPAGATION: test.disable_aws_context_propagation_envvar,
269208
# NOT using the X-Ray Propagator
270209
OTEL_PROPAGATORS: "tracecontext",
271210
},
272211
)
273212
test_env_patch.start()
274213
AwsLambdaInstrumentor().instrument(
275-
event_context_extractor=test.custom_extractor,
276-
disable_aws_context_propagation=test.disable_aws_context_propagation,
214+
event_context_extractor=test.custom_extractor
277215
)
278216
mock_execute_lambda(test.context)
279217
spans = self.memory_exporter.get_finished_spans()
@@ -301,6 +239,65 @@ def custom_event_context_extractor(lambda_event):
301239
AwsLambdaInstrumentor().uninstrument()
302240
test_env_patch.stop()
303241

242+
def test_links_from_lambda_event(self):
243+
@dataclass
244+
class TestCase:
245+
name: str
246+
context: Dict
247+
expected_link_trace_id: int
248+
expected_link_attributes: dict
249+
xray_traceid: str
250+
251+
tests = [
252+
TestCase(
253+
name="valid_xray_trace",
254+
context={},
255+
expected_link_trace_id=MOCK_XRAY_TRACE_ID,
256+
expected_link_attributes={"source": "x-ray-env"},
257+
xray_traceid=MOCK_XRAY_TRACE_CONTEXT_SAMPLED,
258+
),
259+
TestCase(
260+
name="invalid_xray_trace",
261+
context={},
262+
expected_link_trace_id=None,
263+
expected_link_attributes={},
264+
xray_traceid="0",
265+
),
266+
]
267+
for test in tests:
268+
test_env_patch = mock.patch.dict(
269+
"os.environ",
270+
{
271+
**os.environ,
272+
# NOT Active Tracing
273+
_X_AMZN_TRACE_ID: test.xray_traceid,
274+
# NOT using the X-Ray Propagator
275+
OTEL_PROPAGATORS: "tracecontext",
276+
},
277+
)
278+
test_env_patch.start()
279+
AwsLambdaInstrumentor().instrument()
280+
mock_execute_lambda(test.context)
281+
spans = self.memory_exporter.get_finished_spans()
282+
assert spans
283+
self.assertEqual(len(spans), 1)
284+
span = spans[0]
285+
286+
if test.expected_link_trace_id is None:
287+
self.assertEqual(0, len(span.links))
288+
else:
289+
link = span.links[0]
290+
self.assertEqual(
291+
link.context.trace_id, test.expected_link_trace_id
292+
)
293+
self.assertEqual(
294+
link.attributes, test.expected_link_attributes
295+
)
296+
297+
self.memory_exporter.clear()
298+
AwsLambdaInstrumentor().uninstrument()
299+
test_env_patch.stop()
300+
304301
def test_lambda_no_error_with_invalid_flush_timeout(self):
305302
test_env_patch = mock.patch.dict(
306303
"os.environ",

tox.ini

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ envlist =
3434
; instrumentation-aiopg intentionally excluded from pypy3
3535

3636
; opentelemetry-instrumentation-aws-lambda
37-
py3{7,8,9}-test-instrumentation-aws-lambda
37+
py3{7,8,9,10,11}-test-instrumentation-aws-lambda
3838

3939
; opentelemetry-instrumentation-botocore
4040
py3{7,8,9,10,11}-test-instrumentation-botocore

0 commit comments

Comments
 (0)