Skip to content

Commit c8ec25a

Browse files
authored
implements context propagation for lambda invoke + tests (#458)
1 parent c8103f5 commit c8ec25a

File tree

5 files changed

+115
-3
lines changed

5 files changed

+115
-3
lines changed

Diff for: CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.2.0-0.21b0...HEAD)
88

9+
### Added
10+
- `opentelemetry-instrumentation-botocore` now supports
11+
context propagation for lambda invoke via Payload embedded headers.
12+
([#458](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/458))
13+
914
## [0.21b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.2.0-0.21b0) - 2021-05-11
1015
### Changed
1116

Diff for: instrumentation/opentelemetry-instrumentation-boto/setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ install_requires =
4747
[options.extras_require]
4848
test =
4949
boto~=2.0
50-
moto~=1.0
50+
moto~=2.0
5151
opentelemetry-test == 0.22.dev0
5252

5353
[options.packages.find]

Diff for: instrumentation/opentelemetry-instrumentation-botocore/setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ install_requires =
4545

4646
[options.extras_require]
4747
test =
48-
moto ~= 1.0
48+
moto[all] ~= 2.0
4949
opentelemetry-test == 0.22.dev0
5050

5151
[options.packages.find]

Diff for: instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py

+28
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
---
4747
"""
4848

49+
import json
4950
import logging
5051

5152
from botocore.client import BaseClient
@@ -99,6 +100,27 @@ def _instrument(self, **kwargs):
99100
def _uninstrument(self, **kwargs):
100101
unwrap(BaseClient, "_make_api_call")
101102

103+
@staticmethod
104+
def _is_lambda_invoke(service_name, operation_name, api_params):
105+
return (
106+
service_name == "lambda"
107+
and operation_name == "Invoke"
108+
and isinstance(api_params, dict)
109+
and "Payload" in api_params
110+
)
111+
112+
@staticmethod
113+
def _patch_lambda_invoke(api_params):
114+
try:
115+
payload_str = api_params["Payload"]
116+
payload = json.loads(payload_str)
117+
headers = payload.get("headers", {})
118+
inject(headers)
119+
payload["headers"] = headers
120+
api_params["Payload"] = json.dumps(payload)
121+
except ValueError:
122+
pass
123+
102124
# pylint: disable=too-many-branches
103125
def _patched_api_call(self, original_func, instance, args, kwargs):
104126
if context_api.get_value("suppress_instrumentation"):
@@ -111,6 +133,12 @@ def _patched_api_call(self, original_func, instance, args, kwargs):
111133
error = None
112134
result = None
113135

136+
# inject trace context into payload headers for lambda Invoke
137+
if BotocoreInstrumentor._is_lambda_invoke(
138+
service_name, operation_name, api_params
139+
):
140+
BotocoreInstrumentor._patch_lambda_invoke(api_params)
141+
114142
with self._tracer.start_as_current_span(
115143
"{}".format(service_name), kind=SpanKind.CLIENT,
116144
) as span:

Diff for: instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py

+80-1
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@
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-
14+
import io
15+
import json
16+
import zipfile
1517
from unittest.mock import Mock, patch
1618

1719
import botocore.session
1820
from botocore.exceptions import ParamValidationError
1921
from moto import ( # pylint: disable=import-error
2022
mock_dynamodb2,
2123
mock_ec2,
24+
mock_iam,
2225
mock_kinesis,
2326
mock_kms,
2427
mock_lambda,
@@ -37,6 +40,24 @@
3740
from opentelemetry.test.test_base import TestBase
3841

3942

43+
def get_as_zip_file(file_name, content):
44+
zip_output = io.BytesIO()
45+
with zipfile.ZipFile(zip_output, "w", zipfile.ZIP_DEFLATED) as zip_file:
46+
zip_file.writestr(file_name, content)
47+
zip_output.seek(0)
48+
return zip_output.read()
49+
50+
51+
def return_headers_lambda_str():
52+
pfunc = """
53+
def lambda_handler(event, context):
54+
print("custom log event")
55+
headers = event.get('headers', event.get('attributes', {}))
56+
return headers
57+
"""
58+
return pfunc
59+
60+
4061
class TestBotocoreInstrumentor(TestBase):
4162
"""Botocore integration testsuite"""
4263

@@ -328,6 +349,64 @@ def test_lambda_client(self):
328349
},
329350
)
330351

352+
@mock_iam
353+
def get_role_name(self):
354+
iam = self.session.create_client("iam", "us-east-1")
355+
return iam.create_role(
356+
RoleName="my-role",
357+
AssumeRolePolicyDocument="some policy",
358+
Path="/my-path/",
359+
)["Role"]["Arn"]
360+
361+
@mock_lambda
362+
def test_lambda_invoke_propagation(self):
363+
364+
previous_propagator = get_global_textmap()
365+
try:
366+
set_global_textmap(MockTextMapPropagator())
367+
368+
lamb = self.session.create_client(
369+
"lambda", region_name="us-east-1"
370+
)
371+
lamb.create_function(
372+
FunctionName="testFunction",
373+
Runtime="python2.7",
374+
Role=self.get_role_name(),
375+
Handler="lambda_function.lambda_handler",
376+
Code={
377+
"ZipFile": get_as_zip_file(
378+
"lambda_function.py", return_headers_lambda_str()
379+
)
380+
},
381+
Description="test lambda function",
382+
Timeout=3,
383+
MemorySize=128,
384+
Publish=True,
385+
)
386+
response = lamb.invoke(
387+
Payload=json.dumps({}),
388+
FunctionName="testFunction",
389+
InvocationType="RequestResponse",
390+
)
391+
392+
spans = self.memory_exporter.get_finished_spans()
393+
assert spans
394+
self.assertEqual(len(spans), 3)
395+
396+
results = response["Payload"].read().decode("utf-8")
397+
headers = json.loads(results)
398+
399+
self.assertIn(MockTextMapPropagator.TRACE_ID_KEY, headers)
400+
self.assertEqual(
401+
"0", headers[MockTextMapPropagator.TRACE_ID_KEY],
402+
)
403+
self.assertIn(MockTextMapPropagator.SPAN_ID_KEY, headers)
404+
self.assertEqual(
405+
"0", headers[MockTextMapPropagator.SPAN_ID_KEY],
406+
)
407+
finally:
408+
set_global_textmap(previous_propagator)
409+
331410
@mock_kms
332411
def test_kms_client(self):
333412
kms = self.session.create_client("kms", region_name="us-east-1")

0 commit comments

Comments
 (0)