Skip to content

Commit 1e56587

Browse files
committed
Support other propagators in Python Layer
1 parent a51a154 commit 1e56587

File tree

2 files changed

+158
-24
lines changed

2 files changed

+158
-24
lines changed

python/src/otel/otel_sdk/opentelemetry/instrumentation/aws_lambda/__init__.py

+102-14
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,30 @@ def lambda_handler(event, context):
4747
import logging
4848
import os
4949
from importlib import import_module
50-
from typing import Collection
51-
from wrapt import wrap_function_wrapper
50+
from typing import Any, Collection
51+
52+
from opentelemetry.context.context import Context
5253

53-
# TODO: aws propagator
54-
from opentelemetry.sdk.extension.aws.trace.propagation.aws_xray_format import (
55-
AwsXRayFormat,
56-
)
5754
from opentelemetry.instrumentation.aws_lambda.package import _instruments
5855
from opentelemetry.instrumentation.aws_lambda.version import __version__
5956
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
6057
from opentelemetry.instrumentation.utils import unwrap
58+
from opentelemetry.propagate import get_global_textmap
59+
60+
from opentelemetry.sdk.extension.aws.trace.propagation.aws_xray_format import (
61+
AwsXRayFormat,
62+
TRACE_HEADER_KEY,
63+
)
6164
from opentelemetry.semconv.trace import SpanAttributes
62-
from opentelemetry.trace import SpanKind, get_tracer, get_tracer_provider
65+
from opentelemetry.trace import (
66+
SpanKind,
67+
Tracer,
68+
get_tracer,
69+
get_tracer_provider,
70+
)
71+
from opentelemetry.trace.propagation import get_current_span
72+
73+
from wrapt import wrap_function_wrapper
6374

6475
logger = logging.getLogger(__name__)
6576

@@ -69,15 +80,22 @@ def instrumentation_dependencies(self) -> Collection[str]:
6980
return _instruments
7081

7182
def _instrument(self, **kwargs):
72-
"""Instruments Lambda Handlers on AWS Lambda
83+
"""Instruments Lambda Handlers on AWS Lambda.
84+
85+
Read more about how instrumentation is decided:
86+
https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/aws-lambda.md#instrumenting-aws-lambda
7387
7488
Args:
7589
**kwargs: Optional arguments
7690
``tracer_provider``: a TracerProvider, defaults to global
91+
``event_context_extractor``: a method which takes the Lambda
92+
Event as input and provides the object which contains the
93+
context as output (usually HTTP headers)
7794
"""
7895
tracer = get_tracer(
7996
__name__, __version__, kwargs.get("tracer_provider")
8097
)
98+
event_context_extractor = kwargs.get("event_context_extractor")
8199

82100
lambda_handler = os.environ.get(
83101
"ORIG_HANDLER", os.environ.get("_HANDLER")
@@ -87,7 +105,10 @@ def _instrument(self, **kwargs):
87105
self._wrapped_function_name = wrapped_names[1]
88106

89107
_instrument(
90-
tracer, self._wrapped_module_name, self._wrapped_function_name
108+
tracer,
109+
self._wrapped_module_name,
110+
self._wrapped_function_name,
111+
event_context_extractor,
91112
)
92113

93114
def _uninstrument(self, **kwargs):
@@ -97,16 +118,83 @@ def _uninstrument(self, **kwargs):
97118
)
98119

99120

100-
def _instrument(tracer, wrapped_module_name, wrapped_function_name):
121+
def _default_event_context_extractor(lambda_event: Any) -> Context:
122+
"""Default way of extracting the context from the Lambda Event.
123+
124+
Assumes the Lambda Event is a map with the headers under the 'headers' key.
125+
This is the mapping to use when the Lambda is invoked by an API Gateway
126+
REST API where API Gateway is acting as a pure proxy for the request.
127+
128+
Args:
129+
lambda_event: user-defined, so it could be anything, but this
130+
method counts it being a map with a 'headers' key
131+
Returns:
132+
A Context with configuration found in the carrier.
133+
134+
"""
135+
try:
136+
headers = lambda_event["headers"]
137+
except (TypeError, KeyError):
138+
logger.warning("Failed to extract context from Lambda Event.")
139+
headers = {}
140+
return get_global_textmap().extract(headers)
141+
142+
143+
def _instrument(
144+
tracer: Tracer,
145+
wrapped_module_name,
146+
wrapped_function_name,
147+
event_context_extractor=None,
148+
):
149+
def _determine_parent_context(lambda_event: Any) -> Context:
150+
"""Determine the parent context for the current Lambda invocation.
151+
152+
Refer:
153+
https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/instrumentation/aws-lambda.md#determining-the-parent-of-a-span
154+
155+
Args:
156+
lambda_event: user-defined, so it could be anything, but this
157+
method counts it being a map with a 'headers' key
158+
Returns:
159+
A Context with configuration found in the carrier.
160+
161+
"""
162+
parent_context = None
163+
164+
xray_env_var = os.environ.get("_X_AMZN_TRACE_ID")
165+
166+
if xray_env_var:
167+
parent_context = AwsXRayFormat().extract(
168+
{TRACE_HEADER_KEY: xray_env_var}
169+
)
170+
171+
if (
172+
parent_context
173+
and get_current_span(parent_context)
174+
.get_span_context()
175+
.trace_flags.sampled
176+
):
177+
return parent_context
178+
179+
logger.debug(
180+
"X-Ray propagation failed, use user-configured propagators to extract context from Lambda Event."
181+
)
182+
183+
if event_context_extractor:
184+
parent_context = event_context_extractor(lambda_event)
185+
else:
186+
parent_context = _default_event_context_extractor(lambda_event)
187+
188+
return parent_context
189+
101190
def _instrumented_lambda_handler_call(call_wrapped, instance, args, kwargs):
102191
orig_handler_name = ".".join(
103192
[wrapped_module_name, wrapped_function_name]
104193
)
105194

106-
# TODO: enable propagate from AWS by env variable
107-
xray_trace_id = os.environ.get("_X_AMZN_TRACE_ID", "")
108-
propagator = AwsXRayFormat()
109-
parent_context = propagator.extract({"X-Amzn-Trace-Id": xray_trace_id})
195+
lambda_event = args[0]
196+
197+
parent_context = _determine_parent_context(lambda_event)
110198

111199
with tracer.start_as_current_span(
112200
name=orig_handler_name, context=parent_context, kind=SpanKind.SERVER

python/src/otel/tests/test_otel.py

+56-10
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@
1919
from opentelemetry.instrumentation.aws_lambda import AwsLambdaInstrumentor
2020
from opentelemetry.sdk.extension.aws.trace.propagation.aws_xray_format import (
2121
TRACE_ID_FIRST_PART_LENGTH,
22+
TRACE_ID_VERSION
2223
)
2324
from opentelemetry.semconv.resource import ResourceAttributes
2425
from opentelemetry.semconv.trace import SpanAttributes
2526
from opentelemetry.test.test_base import TestBase
2627
from opentelemetry.trace import SpanKind
2728

2829
_HANDLER = "_HANDLER"
30+
_X_AMZN_TRACE_ID = "_X_AMZN_TRACE_ID"
2931
AWS_LAMBDA_EXEC_WRAPPER = "AWS_LAMBDA_EXEC_WRAPPER"
3032
INSTRUMENTATION_SRC_DIR = os.path.join(
3133
*(os.path.dirname(__file__), "..", "otel_sdk")
@@ -42,12 +44,17 @@ def __init__(self, aws_request_id, invoked_function_arn):
4244
aws_request_id="mock_aws_request_id",
4345
invoked_function_arn="arn://mock-lambda-function-arn",
4446
)
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"
5047

48+
MOCK_XRAY_TRACE_ID = 0x5fb7331105e8bb83207fa31d4d9cdb4c
49+
MOCK_XRAY_TRACE_ID_STR = f"{MOCK_XRAY_TRACE_ID:x}"
50+
MOCK_XRAY_PARENT_SPAN_ID = 0x3328b8445a6dbad2
51+
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}"
52+
MOCK_XRAY_TRACE_CONTEXT_SAMPLED = f"{MOCK_XRAY_TRACE_CONTEXT_COMMON};Sampled=1"
53+
MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED = f"{MOCK_XRAY_TRACE_CONTEXT_COMMON};Sampled=0"
54+
55+
MOCK_W3C_TRACE_ID = 0x5ce0e9a56015fec5aadfa328ae398115
56+
MOCK_W3C_PARENT_SPAN_ID = 0xab54a98ceb1f0ad2
57+
MOCK_W3C_TRACE_CONTEXT_SAMPLED = f"00-{MOCK_W3C_TRACE_ID:x}-{MOCK_W3C_PARENT_SPAN_ID:x}-01"
5158

5259
def mock_aws_lambda_exec_wrapper():
5360
"""Mocks automatically instrumenting user Lambda function by pointing
@@ -63,13 +70,13 @@ def mock_aws_lambda_exec_wrapper():
6370
exec(open(os.path.join(INSTRUMENTATION_SRC_DIR, "otel-instrument")).read())
6471

6572

66-
def mock_execute_lambda():
73+
def mock_execute_lambda(event=None):
6774
if os.environ[AWS_LAMBDA_EXEC_WRAPPER]:
6875
globals()[os.environ[AWS_LAMBDA_EXEC_WRAPPER]]()
6976

7077
module_name, handler_name = os.environ[_HANDLER].split(".")
7178
handler_module = import_module(".".join(module_name.split("/")))
72-
getattr(handler_module, handler_name)("mock_event", MOCK_LAMBDA_CONTEXT)
79+
getattr(handler_module, handler_name)(event, MOCK_LAMBDA_CONTEXT)
7380

7481

7582
class TestAwsLambdaInstrumentor(TestBase):
@@ -109,7 +116,7 @@ def test_active_tracing(self):
109116
"os.environ",
110117
{
111118
**os.environ,
112-
"_X_AMZN_TRACE_ID": MOCK_LAMBDA_TRACE_CONTEXT_SAMPLED,
119+
_X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_SAMPLED,
113120
},
114121
)
115122
test_env_patch.start()
@@ -123,7 +130,7 @@ def test_active_tracing(self):
123130
self.assertEqual(len(spans), 1)
124131
span = spans[0]
125132
self.assertEqual(span.name, os.environ["ORIG_HANDLER"])
126-
self.assertEqual(span.get_span_context().trace_id, MOCK_TRACE_ID)
133+
self.assertEqual(span.get_span_context().trace_id, MOCK_XRAY_TRACE_ID)
127134
self.assertEqual(span.kind, SpanKind.SERVER)
128135
self.assertSpanHasAttributes(
129136
span,
@@ -149,7 +156,46 @@ def test_active_tracing(self):
149156
self.assertEqual(
150157
parent_context.trace_id, span.get_span_context().trace_id
151158
)
152-
self.assertEqual(parent_context.span_id, MOCK_PARENT_SPAN_ID)
159+
self.assertEqual(parent_context.span_id, MOCK_XRAY_PARENT_SPAN_ID)
160+
self.assertTrue(parent_context.is_remote)
161+
162+
test_env_patch.stop()
163+
164+
def test_parent_context_from_lambda_event(self):
165+
test_env_patch = mock.patch.dict(
166+
"os.environ",
167+
{
168+
**os.environ,
169+
# NOT Active Tracing
170+
_X_AMZN_TRACE_ID: MOCK_XRAY_TRACE_CONTEXT_NOT_SAMPLED,
171+
# NOT using the X-Ray Propagator
172+
"OTEL_PROPAGATORS": "tracecontext",
173+
},
174+
)
175+
test_env_patch.start()
176+
177+
mock_execute_lambda(
178+
{
179+
"headers": {
180+
"traceparent": MOCK_W3C_TRACE_CONTEXT_SAMPLED,
181+
"tracestate": "vendor_specific_key=a_value,foo=1,bar=2",
182+
}
183+
}
184+
)
185+
186+
spans = self.memory_exporter.get_finished_spans()
187+
188+
assert spans
189+
190+
self.assertEqual(len(spans), 1)
191+
span = spans[0]
192+
self.assertEqual(span.get_span_context().trace_id, MOCK_W3C_TRACE_ID)
193+
194+
parent_context = span.parent
195+
self.assertEqual(
196+
parent_context.trace_id, span.get_span_context().trace_id
197+
)
198+
self.assertEqual(parent_context.span_id, MOCK_W3C_PARENT_SPAN_ID)
153199
self.assertTrue(parent_context.is_remote)
154200

155201
test_env_patch.stop()

0 commit comments

Comments
 (0)