Skip to content

Commit 49fa531

Browse files
devmonkey22xrmx
andauthored
Add tornado WebSocketHandler instrumentation support (#3498)
* Add tornado WebSocketHandler instrumentation support. (#2761) * Linting * Update CHANGELOG.md * Apply refactor changes from #3582 --------- Co-authored-by: Riccardo Magliocchetti <[email protected]>
1 parent 333fc5d commit 49fa531

File tree

4 files changed

+90
-7
lines changed

4 files changed

+90
-7
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616

1717
- `opentelemetry-instrumentation-pika` Added instrumentation for All `SelectConnection` adapters
1818
([#3584](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3584))
19+
- `opentelemetry-instrumentation-tornado` Add support for `WebSocketHandler` instrumentation
20+
([#3498](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3498))
1921

2022
### Fixed
2123

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

Lines changed: 27 additions & 7 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

@@ -362,12 +363,20 @@ def patch_handler_class(tracer, server_histograms, cls, request_hook=None):
362363
"prepare",
363364
partial(_prepare, tracer, server_histograms, request_hook),
364365
)
365-
_wrap(cls, "on_finish", partial(_on_finish, tracer, server_histograms))
366366
_wrap(
367367
cls,
368368
"log_exception",
369369
partial(_log_exception, tracer, server_histograms),
370370
)
371+
372+
if issubclass(cls, tornado.websocket.WebSocketHandler):
373+
_wrap(
374+
cls,
375+
"on_close",
376+
partial(_websockethandler_on_close, tracer, server_histograms),
377+
)
378+
else:
379+
_wrap(cls, "on_finish", partial(_on_finish, tracer, server_histograms))
371380
return True
372381

373382

@@ -376,8 +385,11 @@ def unpatch_handler_class(cls):
376385
return
377386

378387
unwrap(cls, "prepare")
379-
unwrap(cls, "on_finish")
380388
unwrap(cls, "log_exception")
389+
if issubclass(cls, tornado.websocket.WebSocketHandler):
390+
unwrap(cls, "on_close")
391+
else:
392+
unwrap(cls, "on_finish")
381393
delattr(cls, _OTEL_PATCHED_KEY)
382394

383395

@@ -405,13 +417,21 @@ def _prepare(
405417

406418

407419
def _on_finish(tracer, server_histograms, func, handler, args, kwargs):
408-
response = func(*args, **kwargs)
409-
410-
_record_on_finish_metrics(server_histograms, handler)
420+
try:
421+
return func(*args, **kwargs)
422+
finally:
423+
_record_on_finish_metrics(server_histograms, handler)
424+
_finish_span(tracer, handler)
411425

412-
_finish_span(tracer, handler)
413426

414-
return response
427+
def _websockethandler_on_close(
428+
tracer, server_histograms, func, handler, args, kwargs
429+
):
430+
try:
431+
func()
432+
finally:
433+
_record_on_finish_metrics(server_histograms, handler)
434+
_finish_span(tracer, handler)
415435

416436

417437
def _log_exception(tracer, server_histograms, func, handler, args, kwargs):

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

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

1515

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

19+
import tornado.websocket
1820
from http_server_mock import HttpServerMock
1921
from tornado.httpclient import HTTPClientError
2022
from tornado.testing import AsyncHTTPTestCase
@@ -454,6 +456,53 @@ def test_handler_on_finish(self):
454456

455457
self.assertEqual(auditor.kind, SpanKind.INTERNAL)
456458

459+
@tornado.testing.gen_test()
460+
async def test_websockethandler(self):
461+
ws_client = await tornado.websocket.websocket_connect(
462+
f"ws://127.0.0.1:{self.get_http_port()}/echo_socket"
463+
)
464+
465+
await ws_client.write_message("world")
466+
resp = await ws_client.read_message()
467+
self.assertEqual(resp, "hello world")
468+
469+
ws_client.close()
470+
await asyncio.sleep(0.5)
471+
472+
spans = self.sorted_spans(self.memory_exporter.get_finished_spans())
473+
self.assertEqual(len(spans), 3)
474+
close_span, msg_span, req_span = spans
475+
476+
self.assertEqual(req_span.name, "GET /echo_socket")
477+
self.assertEqual(req_span.context.trace_id, msg_span.context.trace_id)
478+
self.assertIsNone(req_span.parent)
479+
self.assertEqual(req_span.kind, SpanKind.SERVER)
480+
self.assertSpanHasAttributes(
481+
req_span,
482+
{
483+
HTTP_METHOD: "GET",
484+
HTTP_SCHEME: "http",
485+
HTTP_HOST: f"127.0.0.1:{self.get_http_port()}",
486+
HTTP_TARGET: "/echo_socket",
487+
HTTP_CLIENT_IP: "127.0.0.1",
488+
HTTP_STATUS_CODE: 101,
489+
"tornado.handler": "tests.tornado_test_app.EchoWebSocketHandler",
490+
},
491+
)
492+
493+
self.assertEqual(msg_span.name, "audit_message")
494+
self.assertFalse(msg_span.context.is_remote)
495+
self.assertEqual(msg_span.kind, SpanKind.INTERNAL)
496+
self.assertEqual(msg_span.parent.span_id, req_span.context.span_id)
497+
498+
self.assertEqual(close_span.name, "audit_on_close")
499+
self.assertFalse(close_span.context.is_remote)
500+
self.assertEqual(close_span.parent.span_id, req_span.context.span_id)
501+
self.assertEqual(
502+
close_span.context.trace_id, msg_span.context.trace_id
503+
)
504+
self.assertEqual(close_span.kind, SpanKind.INTERNAL)
505+
457506
def test_exclude_lists(self):
458507
def test_excluded(path):
459508
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
@@ -2,6 +2,7 @@
22
import time
33

44
import tornado.web
5+
import tornado.websocket
56
from tornado import gen
67

78

@@ -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)