Skip to content

Commit ce01e46

Browse files
committed
clean up tests
1 parent b83864a commit ce01e46

File tree

9 files changed

+145
-61
lines changed

9 files changed

+145
-61
lines changed

ddtrace/contrib/internal/tornado/__init__.py

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@
1515
1616
# create your handlers
1717
class MainHandler(tornado.web.RequestHandler):
18-
@tornado.gen.coroutine
19-
def get(self):
18+
async def get(self):
2019
self.write("Hello, world")
2120
2221
# create your application
@@ -33,10 +32,9 @@ def get(self):
3332
the ``trace()`` method as usual::
3433
3534
class MainHandler(tornado.web.RequestHandler):
36-
@tornado.gen.coroutine
37-
def get(self):
38-
yield self.notify()
39-
yield self.blocking_method()
35+
async def get(self):
36+
await self.notify()
37+
await self.blocking_method()
4038
with tracer.trace('tornado.before_write') as span:
4139
# trace more work in the handler
4240
@@ -46,17 +44,15 @@ def blocking_method(self):
4644
# do something expensive
4745
4846
@tracer.wrap('tornado.notify', service='tornado-notification')
49-
@tornado.gen.coroutine
50-
def notify(self):
47+
async def notify(self):
5148
# do something
5249
5350
If you are overriding the ``on_finish`` or ``log_exception`` methods on a
5451
``RequestHandler``, you will need to call the super method to ensure the
5552
tracer's patched methods are called::
5653
5754
class MainHandler(tornado.web.RequestHandler):
58-
@tornado.gen.coroutine
59-
def get(self):
55+
async def get(self):
6056
self.write("Hello, world")
6157
6258
def on_finish(self):

ddtrace/contrib/internal/tornado/application.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from inspect import iscoroutinefunction
12
from urllib.parse import urlparse
23

34
from tornado import template
@@ -6,9 +7,70 @@
67
from ddtrace import config
78
from ddtrace._trace.pin import Pin
89
from ddtrace.contrib.internal.tornado.constants import CONFIG_KEY
10+
from ddtrace.internal.logger import get_logger
911
from ddtrace.internal.schema import schematize_service_name
1012

1113

14+
log = get_logger(__name__)
15+
16+
17+
def _wrap_executor(tracer, fn, args, kwargs, span_name, service=None, resource=None, span_type=None):
18+
if iscoroutinefunction(fn):
19+
20+
async def async_wrapper():
21+
span = tracer.trace(span_name, service=service, resource=resource, span_type=span_type)
22+
try:
23+
result = await fn(*args, **kwargs)
24+
span.finish()
25+
return result
26+
except Exception:
27+
span.set_traceback()
28+
span.finish()
29+
raise
30+
31+
return async_wrapper()
32+
33+
span = tracer.trace(span_name, service=service, resource=resource, span_type=span_type)
34+
prev_active = tracer.context_provider.active()
35+
36+
def _finish_span(future):
37+
try:
38+
exc = future.exception()
39+
if exc is not None:
40+
span.set_exc_info(type(exc), exc, exc.__traceback__)
41+
except Exception:
42+
# If exception() is not available or raises, try to get the result
43+
# which will raise if there was an exception
44+
try:
45+
future.result()
46+
except Exception:
47+
span.set_traceback()
48+
finally:
49+
tracer.context_provider.activate(prev_active)
50+
span.finish()
51+
52+
try:
53+
result = fn(*args, **kwargs)
54+
# Duck-typing: if it has `add_done_callback` it's a Future-like object
55+
if callable(getattr(result, "add_done_callback", None)):
56+
tracer.context_provider.activate(span)
57+
result.add_done_callback(_finish_span)
58+
else:
59+
span.finish()
60+
return result
61+
except Exception:
62+
span.set_traceback()
63+
span.finish()
64+
raise
65+
66+
67+
def _wrapped_sleep(wrapped, instance, args, kwargs):
68+
log.warning(
69+
"tornado.gen.sleep is not supported with Tornado >6.1 (asyncio-based). Please use asyncio.sleep() instead."
70+
)
71+
return wrapped(*args, **kwargs)
72+
73+
1274
def tracer_config(__init__, app, args, kwargs):
1375
"""
1476
Wrap Tornado web application so that we can configure services info and
@@ -33,6 +95,8 @@ def tracer_config(__init__, app, args, kwargs):
3395
tracer = settings["tracer"]
3496
service = settings["default_service"]
3597

98+
tracer._wrap_executor = _wrap_executor
99+
36100
# extract extra settings
37101
# TODO: Remove `FILTERS` from supported settings
38102
trace_processors = settings.get("settings", {}).get("FILTERS")

ddtrace/contrib/internal/tornado/constants.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,3 @@
55

66
CONFIG_KEY = "datadog_trace"
77
REQUEST_SPAN_KEY = "__datadog_request_span"
8-
FUTURE_SPAN_KEY = "__datadog_future_span"

ddtrace/contrib/internal/tornado/patch.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from wrapt import wrap_function_wrapper as _w
66

77
from ddtrace import config
8+
from ddtrace import tracer
89
from ddtrace.internal.utils.formats import asbool
910
from ddtrace.internal.utils.wrappers import unwrap as _u
1011

@@ -30,39 +31,41 @@ def _supported_versions() -> Dict[str, str]:
3031
return {"tornado": ">=6.1"}
3132

3233

34+
DEFAULT_WRAP_EXECUTOR = None
35+
36+
3337
def patch():
34-
"""
35-
Tracing function that patches the Tornado web application so that it will be
36-
traced using the given ``tracer``.
37-
"""
38-
# patch only once
3938
if getattr(tornado, "__datadog_patch", False):
4039
return
4140
tornado.__datadog_patch = True
41+
global DEFAULT_WRAP_EXECUTOR
42+
DEFAULT_WRAP_EXECUTOR = getattr(tracer, "_wrap_executor", None)
43+
tracer._wrap_executor = application._wrap_executor
4244

43-
# patch Application to initialize properly our settings and tracer
4445
_w("tornado.web", "Application.__init__", application.tracer_config)
45-
46-
# patch RequestHandler to trace all Tornado handlers
4746
_w("tornado.web", "RequestHandler._execute", handlers.execute)
4847
_w("tornado.web", "RequestHandler.on_finish", handlers.on_finish)
4948
_w("tornado.web", "RequestHandler.log_exception", handlers.log_exception)
50-
51-
# patch Template system
5249
_w("tornado.template", "Template.generate", template.generate)
50+
# wrapt handles lazy imports, so we can wrap even if tornado.gen isn't imported yet
51+
try:
52+
_w("tornado.gen", "sleep", application._wrapped_sleep)
53+
except (ImportError, AttributeError):
54+
pass
5355

5456

5557
def unpatch():
56-
"""
57-
Remove all tracing functions in a Tornado web application.
58-
"""
5958
if not getattr(tornado, "__datadog_patch", False):
6059
return
6160
tornado.__datadog_patch = False
6261

63-
# unpatch Tornado
62+
tracer._wrap_executor = DEFAULT_WRAP_EXECUTOR
63+
6464
_u(tornado.web.RequestHandler, "_execute")
6565
_u(tornado.web.RequestHandler, "on_finish")
6666
_u(tornado.web.RequestHandler, "log_exception")
6767
_u(tornado.web.Application, "__init__")
6868
_u(tornado.template.Template, "generate")
69+
# Use getattr to avoid creating a local variable that shadows the module-level import
70+
if getattr(tornado, "gen", None) and hasattr(tornado.gen, "sleep"):
71+
_u(tornado.gen, "sleep")
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
fixes:
3+
- |
4+
Fixed ``tracer.wrap()`` to correctly measure span durations for Tornado coroutines and
5+
functions that return Futures, ensuring spans complete only after the asynchronous operation finishes.
6+
``tornado.gen.sleep`` is no longer supported for Tornado >=6.1; a warning will be logged if used.
7+
Users should migrate to ``asyncio.sleep``.
8+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""
2+
Test that tornado.gen.sleep logs a warning when used.
3+
"""
4+
5+
import logging
6+
7+
from .utils import TornadoTestCase
8+
9+
10+
class TestGenSleepDeprecation(TornadoTestCase):
11+
"""
12+
Ensure that tornado.gen.sleep logs a warning when used.
13+
"""
14+
15+
def test_gen_sleep_warning(self):
16+
from tornado import version_info
17+
18+
# tornado.gen.sleep was removed in Tornado >=6.3
19+
if version_info >= (6, 3):
20+
self.skipTest("tornado.gen.sleep does not exist in Tornado >=6.3")
21+
22+
import tornado.gen
23+
24+
with self.assertLogs("ddtrace.contrib.internal.tornado.application", level=logging.WARNING) as log:
25+
tornado.gen.sleep(0.001)
26+
27+
self.assertTrue(
28+
any("tornado.gen.sleep is not supported" in record.message for record in log.records),
29+
f"Expected warning not found in logs: {[r.message for r in log.records]}",
30+
)

tests/contrib/tornado/test_safety.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import asyncio
12
from concurrent.futures import ThreadPoolExecutor
23
import threading
34

45
from tornado import httpclient
5-
from tornado.gen import sleep as tornado_sleep
66
from tornado.testing import gen_test
77

88
from ddtrace.contrib.internal.tornado.patch import patch
@@ -19,7 +19,7 @@ class TestAsyncConcurrency(TornadoTestCase):
1919
"""
2020

2121
@gen_test
22-
def test_concurrent_requests(self):
22+
async def test_concurrent_requests(self):
2323
REQUESTS_NUMBER = 25
2424
responses = []
2525

@@ -41,7 +41,7 @@ def make_requests():
4141
t.start()
4242

4343
while len(responses) < REQUESTS_NUMBER:
44-
yield tornado_sleep(0.001)
44+
await asyncio.sleep(0.001)
4545

4646
for t in threads:
4747
t.join()

tests/contrib/tornado/test_wrap_decorator.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ def test_nested_wrap_handler(self):
3737
assert nested_span.duration >= 0.05
3838
assert request_span.get_tag("component") == "tornado"
3939
assert request_span.get_tag("span.kind") == "server"
40-
assert nested_span.get_tag("component") == "tornado"
4140

4241
def test_nested_exception_wrap_handler(self):
4342
# it should trace a handler that calls a coroutine that raises an exception
@@ -70,16 +69,13 @@ def test_nested_exception_wrap_handler(self):
7069
assert nested_span.duration >= 0.05
7170
assert request_span.get_tag("component") == "tornado"
7271
assert request_span.get_tag("span.kind") == "server"
73-
assert nested_span.get_tag("component") == "tornado"
7472

7573
def test_sync_nested_wrap_handler(self):
76-
# it should trace a handler that calls a coroutine
7774
response = self.fetch("/sync_nested_wrap/")
7875
assert 200 == response.code
7976
traces = self.pop_traces()
8077
assert 1 == len(traces)
8178
assert 2 == len(traces[0])
82-
# check request span
8379
request_span = traces[0][0]
8480
assert "tornado-web" == request_span.service
8581
assert "tornado.request" == request_span.name
@@ -89,17 +85,14 @@ def test_sync_nested_wrap_handler(self):
8985
assert_span_http_status_code(request_span, 200)
9086
assert self.get_url("/sync_nested_wrap/") == request_span.get_tag(http.URL)
9187
assert 0 == request_span.error
92-
# check nested span
9388
nested_span = traces[0][1]
9489
assert "tornado-web" == nested_span.service
9590
assert "tornado.func" == nested_span.name
9691
assert 0 == nested_span.error
97-
# check durations because of the yield sleep
98-
assert request_span.duration >= 0.05
92+
# Span duration includes async operation because wrap_executor waits for Future completion
9993
assert nested_span.duration >= 0.05
10094
assert request_span.get_tag("component") == "tornado"
10195
assert request_span.get_tag("span.kind") == "server"
102-
assert nested_span.get_tag("component") == "tornado"
10396

10497
def test_sync_nested_exception_wrap_handler(self):
10598
# it should trace a handler that calls a coroutine that raises an exception
@@ -132,7 +125,6 @@ def test_sync_nested_exception_wrap_handler(self):
132125
assert nested_span.duration >= 0.05
133126
assert request_span.get_tag("component") == "tornado"
134127
assert request_span.get_tag("span.kind") == "server"
135-
assert nested_span.get_tag("component") == "tornado"
136128

137129
def test_nested_wrap_executor_handler(self):
138130
# it should trace a handler that calls a blocking function in a different executor
@@ -161,7 +153,6 @@ def test_nested_wrap_executor_handler(self):
161153
assert nested_span.duration >= 0.05
162154
assert request_span.get_tag("component") == "tornado"
163155
assert request_span.get_tag("span.kind") == "server"
164-
assert nested_span.get_tag("component") == "tornado"
165156

166157
def test_nested_exception_wrap_executor_handler(self):
167158
# it should trace a handler that calls a blocking function in a different
@@ -195,4 +186,3 @@ def test_nested_exception_wrap_executor_handler(self):
195186
assert nested_span.duration >= 0.05
196187
assert request_span.get_tag("component") == "tornado"
197188
assert request_span.get_tag("span.kind") == "server"
198-
assert nested_span.get_tag("component") == "tornado"

0 commit comments

Comments
 (0)