Skip to content

Commit e030fa0

Browse files
authored
Merge pull request #50 from DataDog/tian.chu/do-not-break-wrapped-lambda-function
Try never break the wrapped lambda function
2 parents 7e860e0 + b2e02d7 commit e030fa0

File tree

6 files changed

+86
-46
lines changed

6 files changed

+86
-46
lines changed

datadog_lambda/patch.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import logging
1010

1111
from wrapt import wrap_function_wrapper as wrap
12+
from wrapt.importer import when_imported
1213

1314
from datadog_lambda.tracing import get_dd_trace_context
1415

@@ -29,7 +30,7 @@ def patch_all():
2930
Datadog trace context.
3031
"""
3132
_patch_httplib()
32-
_patch_requests()
33+
_ensure_patch_requests()
3334

3435

3536
def _patch_httplib():
@@ -45,7 +46,20 @@ def _patch_httplib():
4546
logger.debug("Patched %s", httplib_module)
4647

4748

48-
def _patch_requests():
49+
def _ensure_patch_requests():
50+
"""
51+
`requests` is third-party, may not be installed or used,
52+
but ensure it gets patched if installed and used.
53+
"""
54+
if "requests" in sys.modules:
55+
# already imported, patch now
56+
_patch_requests(sys.modules["requests"])
57+
else:
58+
# patch when imported
59+
when_imported("requests")(_patch_requests)
60+
61+
62+
def _patch_requests(module):
4963
"""
5064
Patch the high-level HTTP client module `requests`
5165
if it's installed.
@@ -66,9 +80,9 @@ def _wrap_requests_request(func, instance, args, kwargs):
6680
into the outgoing requests.
6781
"""
6882
context = get_dd_trace_context()
69-
if "headers" in kwargs:
83+
if "headers" in kwargs and isinstance(kwargs["headers"], dict):
7084
kwargs["headers"].update(context)
71-
elif len(args) >= 5:
85+
elif len(args) >= 5 and isinstance(args[4], dict):
7286
args[4].update(context)
7387
else:
7488
kwargs["headers"] = context
@@ -86,9 +100,9 @@ def _wrap_httplib_request(func, instance, args, kwargs):
86100
the Datadog trace headers into the outgoing requests.
87101
"""
88102
context = get_dd_trace_context()
89-
if "headers" in kwargs:
103+
if "headers" in kwargs and isinstance(kwargs["headers"], dict):
90104
kwargs["headers"].update(context)
91-
elif len(args) >= 4:
105+
elif len(args) >= 4 and isinstance(args[3], dict):
92106
args[3].update(context)
93107
else:
94108
kwargs["headers"] = context

datadog_lambda/wrapper.py

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -53,37 +53,45 @@ class _LambdaDecorator(object):
5353
and extracts/injects trace context.
5454
"""
5555

56-
_force_new = False
56+
_force_wrap = False
5757

5858
def __new__(cls, func):
5959
"""
6060
If the decorator is accidentally applied to the same function multiple times,
61-
only the first one takes effect.
61+
wrap only once.
6262
63-
If _force_new, always return a real decorator, useful for unit tests.
63+
If _force_wrap, always return a real decorator, useful for unit tests.
6464
"""
65-
if cls._force_new or not getattr(func, "_dd_wrapped", False):
66-
wrapped = super(_LambdaDecorator, cls).__new__(cls)
67-
wrapped._dd_wrapped = True
68-
return wrapped
69-
else:
70-
return _NoopDecorator(func)
65+
try:
66+
if cls._force_wrap or not isinstance(func, _LambdaDecorator):
67+
wrapped = super(_LambdaDecorator, cls).__new__(cls)
68+
logger.debug("datadog_lambda_wrapper wrapped")
69+
return wrapped
70+
else:
71+
logger.debug("datadog_lambda_wrapper already wrapped")
72+
return _NoopDecorator(func)
73+
except Exception:
74+
traceback.print_exc()
75+
return func
7176

7277
def __init__(self, func):
7378
"""Executes when the wrapped function gets wrapped"""
74-
self.func = func
75-
self.flush_to_log = os.environ.get("DD_FLUSH_TO_LOG", "").lower() == "true"
76-
self.logs_injection = (
77-
os.environ.get("DD_LOGS_INJECTION", "true").lower() == "true"
78-
)
79-
80-
# Inject trace correlation ids to logs
81-
if self.logs_injection:
82-
inject_correlation_ids()
83-
84-
# Patch HTTP clients to propagate Datadog trace context
85-
patch_all()
86-
logger.debug("datadog_lambda_wrapper initialized")
79+
try:
80+
self.func = func
81+
self.flush_to_log = os.environ.get("DD_FLUSH_TO_LOG", "").lower() == "true"
82+
self.logs_injection = (
83+
os.environ.get("DD_LOGS_INJECTION", "true").lower() == "true"
84+
)
85+
86+
# Inject trace correlation ids to logs
87+
if self.logs_injection:
88+
inject_correlation_ids()
89+
90+
# Patch HTTP clients to propagate Datadog trace context
91+
patch_all()
92+
logger.debug("datadog_lambda_wrapper initialized")
93+
except Exception:
94+
traceback.print_exc()
8795

8896
def __call__(self, event, context, **kwargs):
8997
"""Executes when the wrapped function gets called"""
@@ -97,21 +105,23 @@ def __call__(self, event, context, **kwargs):
97105
self._after(event, context)
98106

99107
def _before(self, event, context):
100-
set_cold_start()
101108
try:
109+
set_cold_start()
102110
submit_invocations_metric(context)
103111
# Extract Datadog trace context from incoming requests
104112
extract_dd_trace_context(event)
105113

106114
# Set log correlation ids using extracted trace context
107115
set_correlation_ids()
116+
logger.debug("datadog_lambda_wrapper _before() done")
108117
except Exception:
109118
traceback.print_exc()
110119

111120
def _after(self, event, context):
112121
try:
113122
if not self.flush_to_log:
114123
lambda_stats.flush(float("inf"))
124+
logger.debug("datadog_lambda_wrapper _after() done")
115125
except Exception:
116126
traceback.print_exc()
117127

tests/integration/handle.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import json
2-
31
from datadog_lambda.metric import lambda_metric
42
from datadog_lambda.wrapper import datadog_lambda_wrapper
53

tests/integration/http_requests.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import json
21
import requests
32

43
from datadog_lambda.metric import lambda_metric
@@ -12,7 +11,7 @@ def handle(event, context):
1211
"tests.integration.count", 21, tags=["test:integration", "role:hello"]
1312
)
1413

15-
us_response = requests.get("https://ip-ranges.datadoghq.com/")
16-
eu_response = requests.get("https://ip-ranges.datadoghq.eu/")
14+
requests.get("https://ip-ranges.datadoghq.com/")
15+
requests.get("https://ip-ranges.datadoghq.eu/")
1716

1817
return {"statusCode": 200, "body": {"message": "hello, dog!"}}

tests/test_patch.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@
77

88
from datadog_lambda.patch import (
99
_patch_httplib,
10-
_patch_requests,
10+
_ensure_patch_requests,
1111
)
1212
from datadog_lambda.constants import TraceHeader
1313

1414

1515
class TestPatchHTTPClients(unittest.TestCase):
1616

1717
def setUp(self):
18-
patcher = patch('datadog_lambda.patch.get_dd_trace_context')
18+
patcher = patch("datadog_lambda.patch.get_dd_trace_context")
1919
self.mock_get_dd_trace_context = patcher.start()
2020
self.mock_get_dd_trace_context.return_value = {
21-
TraceHeader.TRACE_ID: '123',
22-
TraceHeader.PARENT_ID: '321',
23-
TraceHeader.SAMPLING_PRIORITY: '2',
21+
TraceHeader.TRACE_ID: "123",
22+
TraceHeader.PARENT_ID: "321",
23+
TraceHeader.SAMPLING_PRIORITY: "2",
2424
}
2525
self.addCleanup(patcher.stop)
2626

@@ -34,10 +34,29 @@ def test_patch_httplib(self):
3434
self.mock_get_dd_trace_context.assert_called()
3535

3636
def test_patch_requests(self):
37-
_patch_requests()
37+
_ensure_patch_requests()
3838
import requests
3939
r = requests.get("https://www.datadoghq.com/")
4040
self.mock_get_dd_trace_context.assert_called()
41-
self.assertEqual(r.request.headers[TraceHeader.TRACE_ID], '123')
42-
self.assertEqual(r.request.headers[TraceHeader.PARENT_ID], '321')
43-
self.assertEqual(r.request.headers[TraceHeader.SAMPLING_PRIORITY], '2')
41+
self.assertEqual(r.request.headers[TraceHeader.TRACE_ID], "123")
42+
self.assertEqual(r.request.headers[TraceHeader.PARENT_ID], "321")
43+
self.assertEqual(r.request.headers[TraceHeader.SAMPLING_PRIORITY], "2")
44+
45+
def test_patch_requests_with_headers(self):
46+
_ensure_patch_requests()
47+
import requests
48+
r = requests.get("https://www.datadoghq.com/", headers={"key": "value"})
49+
self.mock_get_dd_trace_context.assert_called()
50+
self.assertEqual(r.request.headers["key"], "value")
51+
self.assertEqual(r.request.headers[TraceHeader.TRACE_ID], "123")
52+
self.assertEqual(r.request.headers[TraceHeader.PARENT_ID], "321")
53+
self.assertEqual(r.request.headers[TraceHeader.SAMPLING_PRIORITY], "2")
54+
55+
def test_patch_requests_with_headers_none(self):
56+
_ensure_patch_requests()
57+
import requests
58+
r = requests.get("https://www.datadoghq.com/", headers=None)
59+
self.mock_get_dd_trace_context.assert_called()
60+
self.assertEqual(r.request.headers[TraceHeader.TRACE_ID], "123")
61+
self.assertEqual(r.request.headers[TraceHeader.PARENT_ID], "321")
62+
self.assertEqual(r.request.headers[TraceHeader.SAMPLING_PRIORITY], "2")

tests/test_wrapper.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class TestDatadogLambdaWrapper(unittest.TestCase):
2626
def setUp(self):
2727
# Force @datadog_lambda_wrapper to always create a real
2828
# (not no-op) wrapper.
29-
datadog_lambda_wrapper._force_new = True
29+
datadog_lambda_wrapper._force_wrap = True
3030

3131
patcher = patch("datadog_lambda.metric.lambda_stats")
3232
self.mock_metric_lambda_stats = patcher.start()
@@ -265,9 +265,9 @@ def test_only_one_wrapper_in_use(self):
265265
def lambda_handler(event, context):
266266
lambda_metric("test.metric", 100)
267267

268-
# Turn off _force_new to emulate the nested wrapper scenario,
268+
# Turn off _force_wrap to emulate the nested wrapper scenario,
269269
# the second @datadog_lambda_wrapper should actually be no-op.
270-
datadog_lambda_wrapper._force_new = False
270+
datadog_lambda_wrapper._force_wrap = False
271271

272272
lambda_handler_double_wrapped = datadog_lambda_wrapper(lambda_handler)
273273

0 commit comments

Comments
 (0)