Skip to content

Commit 4aec1e4

Browse files
authored
Request/Response hooks for Tornado server and client (#426)
1 parent 0fcb60d commit 4aec1e4

File tree

6 files changed

+130
-10
lines changed

6 files changed

+130
-10
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3232
([#407](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/407))
3333
- `opentelemetry-instrumentation-falcon` FalconInstrumentor now supports request/response hooks.
3434
([#415](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/415))
35+
- `opentelemetry-instrumentation-tornado` Add request/response hooks.
36+
([#426](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/426))
3537

3638
### Removed
3739
- Remove `http.status_text` from span attributes

docs-requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,5 @@ PyMySQL~=0.9.3
3333
pyramid>=1.7
3434
redis>=2.6
3535
sqlalchemy>=1.0
36+
tornado>=6.0
3637
ddtrace>=0.34.0
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
OpenTelemetry Tornado Instrumentation
2+
======================================
3+
4+
.. automodule:: opentelemetry.instrumentation.tornado
5+
:members:
6+
:undoc-members:
7+
:show-inheritance:

instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/__init__.py

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,43 @@ def get(self):
3333
app = tornado.web.Application([(r"/", Handler)])
3434
app.listen(8080)
3535
tornado.ioloop.IOLoop.current().start()
36+
37+
Hooks
38+
*******
39+
40+
Tornado instrumentation supports extending tracing behaviour with the help of hooks.
41+
It's ``instrument()`` method accepts three optional functions that get called back with the
42+
created span and some other contextual information. Example:
43+
44+
.. code-block:: python
45+
46+
# will be called for each incoming request to Tornado
47+
# web server. `handler` is an instance of
48+
# `tornado.web.RequestHandler`.
49+
def server_request_hook(span, handler):
50+
pass
51+
52+
# will be called just before sending out a request with
53+
# `tornado.httpclient.AsyncHTTPClient.fetch`.
54+
# `request` is an instance of ``tornado.httpclient.HTTPRequest`.
55+
def client_request_hook(span, request):
56+
pass
57+
58+
# will be called after a outgoing request made with
59+
# `tornado.httpclient.AsyncHTTPClient.fetch` finishes.
60+
# `response`` is an instance of ``Future[tornado.httpclient.HTTPResponse]`.
61+
def client_resposne_hook(span, future):
62+
pass
63+
64+
# apply tornado instrumentation with hooks
65+
TornadoInstrumentor().instrument(
66+
server_request_hook=server_request_hook,
67+
client_request_hook=client_request_hook,
68+
client_response_hook=client_resposne_hook
69+
)
70+
71+
API
72+
---
3673
"""
3774

3875

@@ -96,9 +133,13 @@ def _instrument(self, **kwargs):
96133
tracer_provider = kwargs.get("tracer_provider")
97134
tracer = trace.get_tracer(__name__, __version__, tracer_provider)
98135

136+
client_request_hook = kwargs.get("client_request_hook", None)
137+
client_response_hook = kwargs.get("client_response_hook", None)
138+
server_request_hook = kwargs.get("server_request_hook", None)
139+
99140
def handler_init(init, handler, args, kwargs):
100141
cls = handler.__class__
101-
if patch_handler_class(tracer, cls):
142+
if patch_handler_class(tracer, cls, server_request_hook):
102143
self.patched_handlers.append(cls)
103144
return init(*args, **kwargs)
104145

@@ -108,7 +149,9 @@ def handler_init(init, handler, args, kwargs):
108149
wrap_function_wrapper(
109150
"tornado.httpclient",
110151
"AsyncHTTPClient.fetch",
111-
partial(fetch_async, tracer),
152+
partial(
153+
fetch_async, tracer, client_request_hook, client_response_hook
154+
),
112155
)
113156

114157
def _uninstrument(self, **kwargs):
@@ -119,12 +162,12 @@ def _uninstrument(self, **kwargs):
119162
self.patched_handlers = []
120163

121164

122-
def patch_handler_class(tracer, cls):
165+
def patch_handler_class(tracer, cls, request_hook=None):
123166
if getattr(cls, _OTEL_PATCHED_KEY, False):
124167
return False
125168

126169
setattr(cls, _OTEL_PATCHED_KEY, True)
127-
_wrap(cls, "prepare", partial(_prepare, tracer))
170+
_wrap(cls, "prepare", partial(_prepare, tracer, request_hook))
128171
_wrap(cls, "on_finish", partial(_on_finish, tracer))
129172
_wrap(cls, "log_exception", partial(_log_exception, tracer))
130173
return True
@@ -146,12 +189,14 @@ def _wrap(cls, method_name, wrapper):
146189
wrapt.apply_patch(cls, method_name, wrapper)
147190

148191

149-
def _prepare(tracer, func, handler, args, kwargs):
192+
def _prepare(tracer, request_hook, func, handler, args, kwargs):
150193
start_time = _time_ns()
151194
request = handler.request
152195
if _excluded_urls.url_disabled(request.uri):
153196
return func(*args, **kwargs)
154-
_start_span(tracer, handler, start_time)
197+
ctx = _start_span(tracer, handler, start_time)
198+
if request_hook:
199+
request_hook(ctx.span, handler)
155200
return func(*args, **kwargs)
156201

157202

instrumentation/opentelemetry-instrumentation-tornado/src/opentelemetry/instrumentation/tornado/client.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def _normalize_request(args, kwargs):
3939
return (new_args, new_kwargs)
4040

4141

42-
def fetch_async(tracer, func, _, args, kwargs):
42+
def fetch_async(tracer, request_hook, response_hook, func, _, args, kwargs):
4343
start_time = _time_ns()
4444

4545
# Return immediately if no args were provided (error)
@@ -55,6 +55,8 @@ def fetch_async(tracer, func, _, args, kwargs):
5555
span = tracer.start_span(
5656
request.method, kind=trace.SpanKind.CLIENT, start_time=start_time,
5757
)
58+
if request_hook:
59+
request_hook(span, request)
5860

5961
if span.is_recording():
6062
attributes = {
@@ -68,12 +70,16 @@ def fetch_async(tracer, func, _, args, kwargs):
6870
inject(request.headers)
6971
future = func(*args, **kwargs)
7072
future.add_done_callback(
71-
functools.partial(_finish_tracing_callback, span=span)
73+
functools.partial(
74+
_finish_tracing_callback,
75+
span=span,
76+
response_hook=response_hook,
77+
)
7278
)
7379
return future
7480

7581

76-
def _finish_tracing_callback(future, span):
82+
def _finish_tracing_callback(future, span, response_hook):
7783
status_code = None
7884
description = None
7985
exc = future.exception()
@@ -92,4 +98,6 @@ def _finish_tracing_callback(future, span):
9298
description=description,
9399
)
94100
)
101+
if response_hook:
102+
response_hook(span, future)
95103
span.end()

instrumentation/opentelemetry-instrumentation-tornado/tests/test_instrumentation.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ def get_app(self):
4242
return app
4343

4444
def setUp(self):
45-
TornadoInstrumentor().instrument()
45+
TornadoInstrumentor().instrument(
46+
server_request_hook=getattr(self, "server_request_hook", None),
47+
client_request_hook=getattr(self, "client_request_hook", None),
48+
client_response_hook=getattr(self, "client_response_hook", None),
49+
)
4650
super().setUp()
4751
# pylint: disable=protected-access
4852
self.env_patch = patch.dict(
@@ -367,6 +371,59 @@ def test_traced_attrs(self):
367371
self.memory_exporter.clear()
368372

369373

374+
class TornadoHookTest(TornadoTest):
375+
_client_request_hook = None
376+
_client_response_hook = None
377+
_server_request_hook = None
378+
379+
def client_request_hook(self, span, handler):
380+
if self._client_request_hook is not None:
381+
self._client_request_hook(span, handler)
382+
383+
def client_response_hook(self, span, handler):
384+
if self._client_response_hook is not None:
385+
self._client_response_hook(span, handler)
386+
387+
def server_request_hook(self, span, handler):
388+
if self._server_request_hook is not None:
389+
self._server_request_hook(span, handler)
390+
391+
def test_hooks(self):
392+
def server_request_hook(span, handler):
393+
span.update_name("name from server hook")
394+
handler.set_header("hello", "world")
395+
396+
def client_request_hook(span, request):
397+
span.update_name("name from client hook")
398+
399+
def client_response_hook(span, request):
400+
span.set_attribute("attr-from-hook", "value")
401+
402+
self._server_request_hook = server_request_hook
403+
self._client_request_hook = client_request_hook
404+
self._client_response_hook = client_response_hook
405+
406+
response = self.fetch("/")
407+
self.assertEqual(response.headers.get("hello"), "world")
408+
409+
spans = self.sorted_spans(self.memory_exporter.get_finished_spans())
410+
self.assertEqual(len(spans), 3)
411+
server_span = spans[1]
412+
self.assertEqual(server_span.kind, SpanKind.SERVER)
413+
self.assertEqual(server_span.name, "name from server hook")
414+
self.assert_span_has_attributes(server_span, {"uri": "/"})
415+
self.memory_exporter.clear()
416+
417+
client_span = spans[2]
418+
self.assertEqual(client_span.kind, SpanKind.CLIENT)
419+
self.assertEqual(client_span.name, "name from client hook")
420+
self.assert_span_has_attributes(
421+
client_span, {"attr-from-hook": "value"}
422+
)
423+
424+
self.memory_exporter.clear()
425+
426+
370427
class TestTornadoUninstrument(TornadoTest):
371428
def test_uninstrument(self):
372429
response = self.fetch("/")

0 commit comments

Comments
 (0)