|
21 | 21 | link_vector = ['https://websockets.readthedocs.io'] |
22 | 22 | support_matrix = { |
23 | 23 | 'websockets': { |
24 | | - '>=3.7': ['10.3', '10.4', '17.0.1'] |
| 24 | + '>=3.11': ['10.3', '10.4', '17.0.1'], |
| 25 | + '>=3.7': ['10.3', '10.4'] # websockets >= 14 requires Python >= 3.11 |
25 | 26 | } |
26 | 27 | } |
27 | | -note = """The websocket instrumentation only traces client side connection handshake, |
| 28 | +note = """Both the legacy (websockets.legacy, websockets <= 13) and the new asyncio |
| 29 | +(websockets.asyncio, websockets >= 13) client implementations are instrumented. |
| 30 | +The websocket instrumentation only traces client side connection handshake, |
28 | 31 | the actual message exchange (send/recv) is not traced since injecting headers to socket message |
29 | 32 | body is the only way to propagate the trace context, which requires customization of message structure |
30 | 33 | and extreme care. (Feel free to add this feature by instrumenting the send/recv methods commented out in the code |
|
33 | 36 |
|
34 | 37 |
|
35 | 38 | def install(): |
36 | | - from websockets.legacy.client import WebSocketClientProtocol |
| 39 | + import websockets # noqa: F401 -- absence is reported by the plugin loader |
| 40 | + |
| 41 | + try: |
| 42 | + from websockets.legacy.client import WebSocketClientProtocol |
| 43 | + _install_legacy_client(WebSocketClientProtocol) |
| 44 | + except ImportError: # websockets.legacy is deprecated since 14.0 and will be removed |
| 45 | + pass |
| 46 | + |
| 47 | + try: |
| 48 | + from websockets.asyncio.client import ClientConnection |
| 49 | + _install_new_client(ClientConnection) |
| 50 | + except ImportError: # websockets < 13 has no websockets.asyncio |
| 51 | + pass |
| 52 | + |
| 53 | + |
| 54 | +def _install_legacy_client(WebSocketClientProtocol): # noqa |
37 | 55 | _protocol_handshake_client = WebSocketClientProtocol.handshake |
38 | 56 |
|
39 | 57 | async def _sw_protocol_handshake_client(self, wsuri, |
@@ -75,6 +93,45 @@ async def _sw_protocol_handshake_client(self, wsuri, |
75 | 93 |
|
76 | 94 | WebSocketClientProtocol.handshake = _sw_protocol_handshake_client |
77 | 95 |
|
| 96 | + |
| 97 | +def _install_new_client(ClientConnection): # noqa |
| 98 | + """websockets >= 13 asyncio implementation: inject sw8 via handshake additional_headers""" |
| 99 | + _connection_handshake = ClientConnection.handshake |
| 100 | + |
| 101 | + async def _sw_connection_handshake(self, *args, **kwargs): |
| 102 | + uri = self.protocol.uri |
| 103 | + span = get_context().new_exit_span(op=uri.path or '/', peer=f'{uri.host}:{uri.port}', |
| 104 | + component=Component.Websockets) |
| 105 | + with span: |
| 106 | + carrier = span.inject() |
| 107 | + span.layer = Layer.Http |
| 108 | + # connect() passes (additional_headers, user_agent_header) positionally |
| 109 | + headers = args[0] if args else kwargs.get('additional_headers') |
| 110 | + headers = dict(headers) if headers else {} |
| 111 | + for item in carrier: |
| 112 | + headers[item.key] = item.val |
| 113 | + if args: |
| 114 | + args = (headers,) + args[1:] |
| 115 | + else: |
| 116 | + kwargs['additional_headers'] = headers |
| 117 | + |
| 118 | + span.tag(TagHttpMethod('websocket.connect')) |
| 119 | + |
| 120 | + scheme = 'wss' if uri.secure else 'ws' |
| 121 | + span.tag(TagHttpURL(f'{scheme}://{uri.host}:{uri.port}{uri.path}')) |
| 122 | + status_msg = 'connection open' |
| 123 | + try: |
| 124 | + await _connection_handshake(self, *args, **kwargs) |
| 125 | + except Exception as e: |
| 126 | + span.error_occurred = True |
| 127 | + span.log(e) |
| 128 | + status_msg = 'invalid handshake' |
| 129 | + raise e |
| 130 | + finally: |
| 131 | + span.tag(TagHttpStatusMsg(status_msg)) |
| 132 | + |
| 133 | + ClientConnection.handshake = _sw_connection_handshake |
| 134 | + |
78 | 135 | # To trace per message transactions |
79 | 136 | # _send = WebSocketCommonProtocol.send |
80 | 137 | # _recv = WebSocketCommonProtocol.recv |
|
0 commit comments