Skip to content

Commit 82620b7

Browse files
committed
Add tornado WebSocketHandler instrumentation support. (open-telemetry#2761)
1 parent 4a1e0ce commit 82620b7

File tree

4 files changed

+84
-10
lines changed

4 files changed

+84
-10
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
([#3464](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3464))
2020
- `opentelemetry-instrumentation-redis` Add support for redis client-specific instrumentation.
2121
([#3143](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3143))
22+
- `opentelemetry-instrumentation-tornado` Add support for `WebSocketHandler` instrumentation
23+
([#3448](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2761))
2224

2325
### Fixed
2426

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

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ def client_response_hook(span, future):
162162
from typing import Collection, Dict
163163

164164
import tornado.web
165+
import tornado.websocket
165166
import wrapt
166167
from wrapt import wrap_function_wrapper
167168

@@ -351,12 +352,16 @@ def patch_handler_class(tracer, server_histograms, cls, request_hook=None):
351352
"prepare",
352353
partial(_prepare, tracer, server_histograms, request_hook),
353354
)
354-
_wrap(cls, "on_finish", partial(_on_finish, tracer, server_histograms))
355355
_wrap(
356356
cls,
357357
"log_exception",
358358
partial(_log_exception, tracer, server_histograms),
359359
)
360+
361+
if issubclass(cls, tornado.websocket.WebSocketHandler):
362+
_wrap(cls, "on_close", partial(_WebSocketHandler_on_close, tracer, server_histograms))
363+
else:
364+
_wrap(cls, "on_finish", partial(_on_finish, tracer, server_histograms))
360365
return True
361366

362367

@@ -365,8 +370,11 @@ def unpatch_handler_class(cls):
365370
return
366371

367372
unwrap(cls, "prepare")
368-
unwrap(cls, "on_finish")
369373
unwrap(cls, "log_exception")
374+
if issubclass(cls, tornado.websocket.WebSocketHandler):
375+
unwrap(cls, "on_close")
376+
else:
377+
unwrap(cls, "on_finish")
370378
delattr(cls, _OTEL_PATCHED_KEY)
371379

372380

@@ -394,14 +402,18 @@ def _prepare(
394402

395403

396404
def _on_finish(tracer, server_histograms, func, handler, args, kwargs):
397-
response = func(*args, **kwargs)
398-
399-
_record_on_finish_metrics(server_histograms, handler)
400-
401-
_finish_span(tracer, handler)
402-
403-
return response
404-
405+
try:
406+
return func(*args, **kwargs)
407+
finally:
408+
_record_on_finish_metrics(server_histograms, handler)
409+
_finish_span(tracer, handler)
410+
411+
def _WebSocketHandler_on_close(tracer, server_histograms, func, handler, args, kwargs):
412+
try:
413+
func()
414+
finally:
415+
_record_on_finish_metrics(server_histograms, handler)
416+
_finish_span(tracer, handler)
405417

406418
def _log_exception(tracer, server_histograms, func, handler, args, kwargs):
407419
error = None

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
# limitations under the License.
1414

1515

16+
import asyncio
1617
from unittest.mock import Mock, patch
1718

1819
from http_server_mock import HttpServerMock
1920
from tornado.httpclient import HTTPClientError
2021
from tornado.testing import AsyncHTTPTestCase
22+
import tornado.websocket
2123

2224
from opentelemetry import trace
2325
from opentelemetry.instrumentation.propagators import (
@@ -450,6 +452,52 @@ def test_handler_on_finish(self):
450452

451453
self.assertEqual(auditor.kind, SpanKind.INTERNAL)
452454

455+
@tornado.testing.gen_test()
456+
async def test_websockethandler(self):
457+
ws_client = await tornado.websocket.websocket_connect(
458+
'ws://127.0.0.1:{}/echo_socket'.format(self.get_http_port())
459+
)
460+
461+
await ws_client.write_message('world')
462+
resp = await ws_client.read_message()
463+
self.assertEqual(resp, 'hello world')
464+
465+
ws_client.close()
466+
await asyncio.sleep(0.5)
467+
468+
spans = self.sorted_spans(self.memory_exporter.get_finished_spans())
469+
self.assertEqual(len(spans), 3)
470+
close_span, msg_span, req_span = spans
471+
472+
self.assertEqual(req_span.name, "GET /echo_socket")
473+
self.assertEqual(req_span.context.trace_id, msg_span.context.trace_id)
474+
self.assertIsNone(req_span.parent)
475+
self.assertEqual(req_span.kind, SpanKind.SERVER)
476+
self.assertSpanHasAttributes(
477+
req_span,
478+
{
479+
SpanAttributes.HTTP_METHOD: "GET",
480+
SpanAttributes.HTTP_SCHEME: "http",
481+
SpanAttributes.HTTP_HOST: "127.0.0.1:"
482+
+ str(self.get_http_port()),
483+
SpanAttributes.HTTP_TARGET: "/echo_socket",
484+
SpanAttributes.HTTP_CLIENT_IP: "127.0.0.1",
485+
SpanAttributes.HTTP_STATUS_CODE: 101,
486+
"tornado.handler": "tests.tornado_test_app.EchoWebSocketHandler",
487+
},
488+
)
489+
490+
self.assertEqual(msg_span.name, "audit_message")
491+
self.assertFalse(msg_span.context.is_remote)
492+
self.assertEqual(msg_span.kind, SpanKind.INTERNAL)
493+
self.assertEqual(msg_span.parent.span_id, req_span.context.span_id)
494+
495+
self.assertEqual(close_span.name, "audit_on_close")
496+
self.assertFalse(close_span.context.is_remote)
497+
self.assertEqual(close_span.parent.span_id, req_span.context.span_id)
498+
self.assertEqual(close_span.context.trace_id, msg_span.context.trace_id)
499+
self.assertEqual(close_span.kind, SpanKind.INTERNAL)
500+
453501
def test_exclude_lists(self):
454502
def test_excluded(path):
455503
self.fetch(path)

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import tornado.web
55
from tornado import gen
6+
import tornado.websocket
67

78

89
class AsyncHandler(tornado.web.RequestHandler):
@@ -110,6 +111,16 @@ def get(self):
110111
raise tornado.web.HTTPError(403)
111112

112113

114+
class EchoWebSocketHandler(tornado.websocket.WebSocketHandler):
115+
async def on_message(self, message):
116+
with self.application.tracer.start_as_current_span("audit_message"):
117+
self.write_message(f'hello {message}')
118+
119+
def on_close(self):
120+
with self.application.tracer.start_as_current_span("audit_on_close"):
121+
time.sleep(0.05)
122+
123+
113124
def make_app(tracer):
114125
app = tornado.web.Application(
115126
[
@@ -122,6 +133,7 @@ def make_app(tracer):
122133
(r"/ping", HealthCheckHandler),
123134
(r"/test_custom_response_headers", CustomResponseHeaderHandler),
124135
(r"/raise_403", RaiseHTTPErrorHandler),
136+
(r"/echo_socket", EchoWebSocketHandler),
125137
]
126138
)
127139
app.tracer = tracer

0 commit comments

Comments
 (0)