sf-veritas 0.11.10__cp314-cp314-manylinux_2_28_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. sf_veritas/__init__.py +46 -0
  2. sf_veritas/_auto_preload.py +73 -0
  3. sf_veritas/_sfconfig.c +162 -0
  4. sf_veritas/_sfconfig.cpython-314-x86_64-linux-gnu.so +0 -0
  5. sf_veritas/_sfcrashhandler.c +267 -0
  6. sf_veritas/_sfcrashhandler.cpython-314-x86_64-linux-gnu.so +0 -0
  7. sf_veritas/_sffastlog.c +953 -0
  8. sf_veritas/_sffastlog.cpython-314-x86_64-linux-gnu.so +0 -0
  9. sf_veritas/_sffastnet.c +994 -0
  10. sf_veritas/_sffastnet.cpython-314-x86_64-linux-gnu.so +0 -0
  11. sf_veritas/_sffastnetworkrequest.c +727 -0
  12. sf_veritas/_sffastnetworkrequest.cpython-314-x86_64-linux-gnu.so +0 -0
  13. sf_veritas/_sffuncspan.c +2791 -0
  14. sf_veritas/_sffuncspan.cpython-314-x86_64-linux-gnu.so +0 -0
  15. sf_veritas/_sffuncspan_config.c +730 -0
  16. sf_veritas/_sffuncspan_config.cpython-314-x86_64-linux-gnu.so +0 -0
  17. sf_veritas/_sfheadercheck.c +341 -0
  18. sf_veritas/_sfheadercheck.cpython-314-x86_64-linux-gnu.so +0 -0
  19. sf_veritas/_sfnetworkhop.c +1454 -0
  20. sf_veritas/_sfnetworkhop.cpython-314-x86_64-linux-gnu.so +0 -0
  21. sf_veritas/_sfservice.c +1223 -0
  22. sf_veritas/_sfservice.cpython-314-x86_64-linux-gnu.so +0 -0
  23. sf_veritas/_sfteepreload.c +6227 -0
  24. sf_veritas/app_config.py +57 -0
  25. sf_veritas/cli.py +336 -0
  26. sf_veritas/constants.py +10 -0
  27. sf_veritas/custom_excepthook.py +304 -0
  28. sf_veritas/custom_log_handler.py +146 -0
  29. sf_veritas/custom_output_wrapper.py +153 -0
  30. sf_veritas/custom_print.py +153 -0
  31. sf_veritas/django_app.py +5 -0
  32. sf_veritas/env_vars.py +186 -0
  33. sf_veritas/exception_handling_middleware.py +18 -0
  34. sf_veritas/exception_metaclass.py +69 -0
  35. sf_veritas/fast_frame_info.py +116 -0
  36. sf_veritas/fast_network_hop.py +293 -0
  37. sf_veritas/frame_tools.py +112 -0
  38. sf_veritas/funcspan_config_loader.py +693 -0
  39. sf_veritas/function_span_profiler.py +1313 -0
  40. sf_veritas/get_preload_path.py +34 -0
  41. sf_veritas/import_hook.py +62 -0
  42. sf_veritas/infra_details/__init__.py +3 -0
  43. sf_veritas/infra_details/get_infra_details.py +24 -0
  44. sf_veritas/infra_details/kubernetes/__init__.py +3 -0
  45. sf_veritas/infra_details/kubernetes/get_cluster_name.py +147 -0
  46. sf_veritas/infra_details/kubernetes/get_details.py +7 -0
  47. sf_veritas/infra_details/running_on/__init__.py +17 -0
  48. sf_veritas/infra_details/running_on/kubernetes.py +11 -0
  49. sf_veritas/interceptors.py +543 -0
  50. sf_veritas/libsfnettee.so +0 -0
  51. sf_veritas/local_env_detect.py +118 -0
  52. sf_veritas/package_metadata.py +6 -0
  53. sf_veritas/patches/__init__.py +0 -0
  54. sf_veritas/patches/_patch_tracker.py +74 -0
  55. sf_veritas/patches/concurrent_futures.py +19 -0
  56. sf_veritas/patches/constants.py +1 -0
  57. sf_veritas/patches/exceptions.py +82 -0
  58. sf_veritas/patches/multiprocessing.py +32 -0
  59. sf_veritas/patches/network_libraries/__init__.py +99 -0
  60. sf_veritas/patches/network_libraries/aiohttp.py +294 -0
  61. sf_veritas/patches/network_libraries/curl_cffi.py +363 -0
  62. sf_veritas/patches/network_libraries/http_client.py +670 -0
  63. sf_veritas/patches/network_libraries/httpcore.py +580 -0
  64. sf_veritas/patches/network_libraries/httplib2.py +315 -0
  65. sf_veritas/patches/network_libraries/httpx.py +557 -0
  66. sf_veritas/patches/network_libraries/niquests.py +218 -0
  67. sf_veritas/patches/network_libraries/pycurl.py +399 -0
  68. sf_veritas/patches/network_libraries/requests.py +595 -0
  69. sf_veritas/patches/network_libraries/ssl_socket.py +822 -0
  70. sf_veritas/patches/network_libraries/tornado.py +360 -0
  71. sf_veritas/patches/network_libraries/treq.py +270 -0
  72. sf_veritas/patches/network_libraries/urllib_request.py +483 -0
  73. sf_veritas/patches/network_libraries/utils.py +598 -0
  74. sf_veritas/patches/os.py +17 -0
  75. sf_veritas/patches/threading.py +231 -0
  76. sf_veritas/patches/web_frameworks/__init__.py +54 -0
  77. sf_veritas/patches/web_frameworks/aiohttp.py +798 -0
  78. sf_veritas/patches/web_frameworks/async_websocket_consumer.py +337 -0
  79. sf_veritas/patches/web_frameworks/blacksheep.py +532 -0
  80. sf_veritas/patches/web_frameworks/bottle.py +513 -0
  81. sf_veritas/patches/web_frameworks/cherrypy.py +683 -0
  82. sf_veritas/patches/web_frameworks/cors_utils.py +122 -0
  83. sf_veritas/patches/web_frameworks/django.py +963 -0
  84. sf_veritas/patches/web_frameworks/eve.py +401 -0
  85. sf_veritas/patches/web_frameworks/falcon.py +931 -0
  86. sf_veritas/patches/web_frameworks/fastapi.py +738 -0
  87. sf_veritas/patches/web_frameworks/flask.py +526 -0
  88. sf_veritas/patches/web_frameworks/klein.py +501 -0
  89. sf_veritas/patches/web_frameworks/litestar.py +616 -0
  90. sf_veritas/patches/web_frameworks/pyramid.py +440 -0
  91. sf_veritas/patches/web_frameworks/quart.py +841 -0
  92. sf_veritas/patches/web_frameworks/robyn.py +708 -0
  93. sf_veritas/patches/web_frameworks/sanic.py +874 -0
  94. sf_veritas/patches/web_frameworks/starlette.py +742 -0
  95. sf_veritas/patches/web_frameworks/strawberry.py +1446 -0
  96. sf_veritas/patches/web_frameworks/tornado.py +485 -0
  97. sf_veritas/patches/web_frameworks/utils.py +170 -0
  98. sf_veritas/print_override.py +13 -0
  99. sf_veritas/regular_data_transmitter.py +444 -0
  100. sf_veritas/request_interceptor.py +401 -0
  101. sf_veritas/request_utils.py +550 -0
  102. sf_veritas/segfault_handler.py +116 -0
  103. sf_veritas/server_status.py +1 -0
  104. sf_veritas/shutdown_flag.py +11 -0
  105. sf_veritas/subprocess_startup.py +3 -0
  106. sf_veritas/test_cli.py +145 -0
  107. sf_veritas/thread_local.py +1319 -0
  108. sf_veritas/timeutil.py +114 -0
  109. sf_veritas/transmit_exception_to_sailfish.py +28 -0
  110. sf_veritas/transmitter.py +132 -0
  111. sf_veritas/types.py +47 -0
  112. sf_veritas/unified_interceptor.py +1678 -0
  113. sf_veritas/utils.py +39 -0
  114. sf_veritas-0.11.10.dist-info/METADATA +97 -0
  115. sf_veritas-0.11.10.dist-info/RECORD +141 -0
  116. sf_veritas-0.11.10.dist-info/WHEEL +5 -0
  117. sf_veritas-0.11.10.dist-info/entry_points.txt +2 -0
  118. sf_veritas-0.11.10.dist-info/top_level.txt +1 -0
  119. sf_veritas.libs/libbrotlicommon-6ce2a53c.so.1.0.6 +0 -0
  120. sf_veritas.libs/libbrotlidec-811d1be3.so.1.0.6 +0 -0
  121. sf_veritas.libs/libcom_err-730ca923.so.2.1 +0 -0
  122. sf_veritas.libs/libcrypt-52aca757.so.1.1.0 +0 -0
  123. sf_veritas.libs/libcrypto-bdaed0ea.so.1.1.1k +0 -0
  124. sf_veritas.libs/libcurl-eaa3cf66.so.4.5.0 +0 -0
  125. sf_veritas.libs/libgssapi_krb5-323bbd21.so.2.2 +0 -0
  126. sf_veritas.libs/libidn2-2f4a5893.so.0.3.6 +0 -0
  127. sf_veritas.libs/libk5crypto-9a74ff38.so.3.1 +0 -0
  128. sf_veritas.libs/libkeyutils-2777d33d.so.1.6 +0 -0
  129. sf_veritas.libs/libkrb5-a55300e8.so.3.3 +0 -0
  130. sf_veritas.libs/libkrb5support-e6594cfc.so.0.1 +0 -0
  131. sf_veritas.libs/liblber-2-d20824ef.4.so.2.10.9 +0 -0
  132. sf_veritas.libs/libldap-2-cea2a960.4.so.2.10.9 +0 -0
  133. sf_veritas.libs/libnghttp2-39367a22.so.14.17.0 +0 -0
  134. sf_veritas.libs/libpcre2-8-516f4c9d.so.0.7.1 +0 -0
  135. sf_veritas.libs/libpsl-99becdd3.so.5.3.1 +0 -0
  136. sf_veritas.libs/libsasl2-7de4d792.so.3.0.0 +0 -0
  137. sf_veritas.libs/libselinux-d0805dcb.so.1 +0 -0
  138. sf_veritas.libs/libssh-c11d285b.so.4.8.7 +0 -0
  139. sf_veritas.libs/libssl-60250281.so.1.1.1k +0 -0
  140. sf_veritas.libs/libunistring-05abdd40.so.2.1.0 +0 -0
  141. sf_veritas.libs/libuuid-95b83d40.so.1.3.0 +0 -0
@@ -0,0 +1,822 @@
1
+ """
2
+ Ultra-fast SSL socket tee for HTTPS traffic capture.
3
+ ~15-20ns overhead per recv/send operation.
4
+
5
+ CRITICAL: This must be patched FIRST before any other HTTP library patches,
6
+ because requests/httpx/urllib3/aiohttp all use ssl.SSLSocket underneath.
7
+
8
+ Architecture:
9
+ 1. Hot path (~15-20ns): Tee data to thread-local deque, return immediately
10
+ 2. Background thread: Aggregates bytes → HTTP transactions
11
+ 3. Push complete transactions to C ring buffer via ctypes
12
+ 4. C ring handles queueing, retries, HTTP/2, telemetry delivery
13
+
14
+ Performance:
15
+ - Thread-local deque: No locks, ~10ns append
16
+ - Graceful degradation: Drop if queue full, never block caller
17
+ - Zero hot path impact: All parsing/transmission happens in background
18
+
19
+ **IMPORTANT: This code is currently DISABLED by default.**
20
+ **Set SF_ENABLE_PYTHON_SSL_TEE=1 to enable.**
21
+ **By default, SF_SSL_PYTHON_MODE=1 just disables C SSL hooks without Python capture.**
22
+
23
+ Environment Variables (respects SF_NETWORKREQUEST_CAPTURE_* settings):
24
+ - SF_NETWORKREQUEST_CAPTURE_ENABLED: Enable/disable all capture (default: true)
25
+ - SF_NETWORKREQUEST_CAPTURE_REQUEST_HEADERS: Capture request headers (default: true)
26
+ - SF_NETWORKREQUEST_CAPTURE_REQUEST_BODY: Capture request body (default: true)
27
+ - SF_NETWORKREQUEST_CAPTURE_RESPONSE_HEADERS: Capture response headers (default: true)
28
+ - SF_NETWORKREQUEST_CAPTURE_RESPONSE_BODY: Capture response body (default: true)
29
+ - SF_NETWORKREQUEST_REQUEST_LIMIT_MB: Max request body size in MB (default: 1)
30
+ - SF_NETWORKREQUEST_RESPONSE_LIMIT_MB: Max response body size in MB (default: 1)
31
+ """
32
+
33
+ import ssl
34
+ import socket
35
+ import threading
36
+ import time
37
+ import ctypes
38
+ from collections import deque
39
+ from typing import Optional, Dict, Any
40
+ import os
41
+
42
+ # Import record_network_request for collectNetworkRequest mutation
43
+ from .utils import record_network_request, init_fast_network_tracking
44
+
45
+ # ============================================================================
46
+ # CONFIGURATION
47
+ # ============================================================================
48
+
49
+ # Check if Python SSL tee is enabled (DISABLED by default)
50
+ _PYTHON_SSL_TEE_ENABLED = os.getenv('SF_ENABLE_PYTHON_SSL_TEE', 'false').lower() == 'true'
51
+ _C_RING_AVAILABLE = os.getenv('SF_SSL_PYTHON_MODE', '1') == '1' and _PYTHON_SSL_TEE_ENABLED
52
+ _SF_DEBUG = os.getenv('SF_DEBUG', 'false').lower() == 'true'
53
+
54
+ # SF_NETWORKREQUEST_CAPTURE_* environment variables (for collectNetworkRequest mutations)
55
+ # These control what data is captured and sent to the C ring
56
+ _CAPTURE_ENABLED = os.getenv('SF_NETWORKREQUEST_CAPTURE_ENABLED', 'true').lower() == 'true'
57
+ _CAPTURE_REQUEST_HEADERS = os.getenv('SF_NETWORKREQUEST_CAPTURE_REQUEST_HEADERS', 'true').lower() == 'true'
58
+ _CAPTURE_REQUEST_BODY = os.getenv('SF_NETWORKREQUEST_CAPTURE_REQUEST_BODY', 'true').lower() == 'true'
59
+ _CAPTURE_RESPONSE_HEADERS = os.getenv('SF_NETWORKREQUEST_CAPTURE_RESPONSE_HEADERS', 'true').lower() == 'true'
60
+ _CAPTURE_RESPONSE_BODY = os.getenv('SF_NETWORKREQUEST_CAPTURE_RESPONSE_BODY', 'true').lower() == 'true'
61
+
62
+ # Size limits (MB to bytes conversion)
63
+ _REQUEST_LIMIT_MB = float(os.getenv('SF_NETWORKREQUEST_REQUEST_LIMIT_MB', '1'))
64
+ _RESPONSE_LIMIT_MB = float(os.getenv('SF_NETWORKREQUEST_RESPONSE_LIMIT_MB', '1'))
65
+ MAX_REQUEST_BODY_CAPTURE = int(_REQUEST_LIMIT_MB * 1024 * 1024)
66
+ MAX_RESPONSE_BODY_CAPTURE = int(_RESPONSE_LIMIT_MB * 1024 * 1024)
67
+
68
+ # ============================================================================
69
+ # THREAD-LOCAL CAPTURE QUEUES (Zero-lock, ~10ns append)
70
+ # ============================================================================
71
+
72
+ _capture_queues = {} # thread_id -> deque
73
+ _capture_queues_lock = threading.Lock()
74
+
75
+
76
+ def _get_capture_queue():
77
+ """Get thread-local capture queue (~5ns lookup)"""
78
+ tid = threading.get_ident()
79
+ if tid not in _capture_queues:
80
+ with _capture_queues_lock:
81
+ if tid not in _capture_queues:
82
+ _capture_queues[tid] = deque(maxlen=1000) # Bounded to prevent OOM
83
+ return _capture_queues[tid]
84
+
85
+
86
+ # ============================================================================
87
+ # HOT PATH - ULTRA FAST (~15-20ns overhead)
88
+ # ============================================================================
89
+
90
+ def _tee_capture(sock, data, direction):
91
+ """
92
+ Tee-style capture: ~15ns overhead, non-blocking
93
+
94
+ Recursion Prevention:
95
+ - Detects X-Sf3-TelemetryOutbound header in requests (telemetry marker)
96
+ - Marks socket as telemetry and skips all future captures
97
+ - Prevents infinite recursion from capturing our own telemetry traffic
98
+
99
+ Performance breakdown:
100
+ - Get thread-local queue: ~5ns
101
+ - Append to deque: ~5-10ns
102
+ - Wake background thread: ~5ns
103
+ Total: ~15-20ns
104
+ """
105
+ # Defensive: ensure data is bytes-like before processing
106
+ if not data:
107
+ return
108
+ if not isinstance(data, (bytes, bytearray)):
109
+ # Invalid data type (should never happen, but be defensive)
110
+ return
111
+ if len(data) == 0:
112
+ return
113
+
114
+ # CRITICAL: Recursion prevention - detect telemetry traffic by X-Sf3-TelemetryOutbound header
115
+ # This header is ONLY added to our own telemetry requests (in request_utils.py)
116
+ # Normal application traffic (even to echo endpoints) will NOT have this header
117
+ if b'X-Sf3-TelemetryOutbound:' in data or b'x-sf3-telemetryoutbound:' in data:
118
+ # Mark this socket as telemetry to skip all future captures
119
+ sock._is_telemetry_socket = True
120
+ if _SF_DEBUG:
121
+ print(f"[ssl_socket.py] _tee_capture: Detected telemetry socket, marking to skip", log=False)
122
+ return
123
+
124
+ # Skip if already marked as telemetry socket
125
+ if getattr(sock, '_is_telemetry_socket', False):
126
+ return
127
+
128
+ try:
129
+ queue = _get_capture_queue()
130
+ sock_id = id(sock)
131
+
132
+ if direction == 'TX' and _SF_DEBUG:
133
+ chunk = bytes(data)
134
+ existing = getattr(sock, '_sf_hdr_buf', b'')
135
+ combined = existing + chunk
136
+ if b'\r\n\r\n' in combined and not getattr(sock, '_sf_headers_logged', False):
137
+ header_bytes = combined.split(b'\r\n\r\n', 1)[0]
138
+ try:
139
+ header_text = header_bytes.decode('latin-1', errors='ignore')
140
+ except Exception:
141
+ header_text = "<failed to decode headers>"
142
+ print(f"[ssl_socket.py] PRE-SEND HEADERS (sock_id={sock_id}):\n{header_text}", log=False)
143
+ sock._sf_headers_logged = True
144
+ sock._sf_hdr_buf = b''
145
+ else:
146
+ sock._sf_hdr_buf = combined[-8192:] # keep manageable buffer
147
+
148
+ # Debug: Log first capture for each socket
149
+ if _SF_DEBUG and not hasattr(sock, '_first_capture_logged'):
150
+ sock._first_capture_logged = True
151
+ peername = getattr(sock, '_peername_cache', None)
152
+ print(f"[ssl_socket.py] _tee_capture: FIRST CAPTURE sock_id={sock_id}, direction={direction}, size={len(data)} bytes, peername={peername}", log=False)
153
+
154
+ queue.append({
155
+ 'sock_id': sock_id,
156
+ 'peername': getattr(sock, '_peername_cache', None),
157
+ 'data': data,
158
+ 'direction': direction,
159
+ 'timestamp': time.perf_counter_ns()
160
+ })
161
+ _background_event.set() # Wake background processor (non-blocking)
162
+ except Exception as e:
163
+ # Never let capture errors break the hot path
164
+ if _SF_DEBUG:
165
+ print(f"[ssl_socket.py] _tee_capture: Exception: {e}", log=False)
166
+
167
+
168
+ # ============================================================================
169
+ # SSL SOCKET MONKEY PATCHING
170
+ # ============================================================================
171
+
172
+ _original_recv = None
173
+ _original_send = None
174
+ _original_sendall = None
175
+ _original_read = None
176
+ _original_write = None
177
+ _patched = False
178
+
179
+
180
+ def patch_ssl_sockets():
181
+ """
182
+ Patch ssl.SSLSocket for tee-style HTTPS capture.
183
+
184
+ CRITICAL: Call this FIRST before patching requests/httpx/urllib3,
185
+ because all HTTP libraries use ssl.SSLSocket underneath.
186
+
187
+ Patches: recv, send, sendall, read, write
188
+
189
+ NOTE: By default, this function is a NO-OP (Python SSL tee is DISABLED).
190
+ Only when SF_ENABLE_PYTHON_SSL_TEE=1 will it actually patch SSL sockets.
191
+ SF_SSL_PYTHON_MODE=1 by default only disables C SSL hooks.
192
+ """
193
+ global _original_recv, _original_send, _original_sendall, _original_read, _original_write, _patched
194
+
195
+ # Always log entry when SF_DEBUG is enabled
196
+ if _SF_DEBUG:
197
+ print(f"[ssl_socket.py] patch_ssl_sockets() CALLED - _PYTHON_SSL_TEE_ENABLED={_PYTHON_SSL_TEE_ENABLED}, _patched={_patched}, SF_ENABLE_PYTHON_SSL_TEE={os.getenv('SF_ENABLE_PYTHON_SSL_TEE', 'NOT_SET')}", log=False)
198
+
199
+ if _patched:
200
+ if _SF_DEBUG:
201
+ print("[ssl_socket.py] Already patched, skipping", log=False)
202
+ return
203
+
204
+ # Check if Python SSL tee is enabled
205
+ if not _PYTHON_SSL_TEE_ENABLED:
206
+ if _SF_DEBUG:
207
+ print("[ssl_socket.py] Python SSL tee DISABLED (SF_ENABLE_PYTHON_SSL_TEE not set to 'true')", log=False)
208
+ print("[ssl_socket.py] SF_SSL_PYTHON_MODE=1 only disables C SSL hooks (no Python capture)", log=False)
209
+ print("[ssl_socket.py] httpcore/requests/etc will handle their own capture instead", log=False)
210
+ return
211
+
212
+ if _SF_DEBUG:
213
+ print("[ssl_socket.py] Python SSL tee ENABLED - Patching ssl.SSLSocket for tee capture", log=False)
214
+ print("[ssl_socket.py] This should only be used for testing/debugging!", log=False)
215
+ print(f"[ssl_socket.py] Capture config: enabled={_CAPTURE_ENABLED}, "
216
+ f"req_headers={_CAPTURE_REQUEST_HEADERS}, req_body={_CAPTURE_REQUEST_BODY}, "
217
+ f"resp_headers={_CAPTURE_RESPONSE_HEADERS}, resp_body={_CAPTURE_RESPONSE_BODY}", log=False)
218
+ print(f"[ssl_socket.py] Size limits: request={_REQUEST_LIMIT_MB}MB, response={_RESPONSE_LIMIT_MB}MB", log=False)
219
+
220
+ # Initialize fast network tracking for collectNetworkRequest emission
221
+ init_fast_network_tracking()
222
+ if _SF_DEBUG:
223
+ print("[ssl_socket.py] Initialized fast network tracking for collectNetworkRequest", log=False)
224
+
225
+ # Save originals
226
+ _original_recv = ssl.SSLSocket.recv
227
+ _original_send = ssl.SSLSocket.send
228
+ _original_sendall = ssl.SSLSocket.sendall
229
+ _original_read = ssl.SSLSocket.read
230
+ _original_write = ssl.SSLSocket.write
231
+
232
+ # Patch recv/send (socket interface)
233
+ def fast_recv(self, bufsize, flags=0):
234
+ """~15ns overhead tee wrapper"""
235
+ if _SF_DEBUG and not hasattr(self, '_recv_logged'):
236
+ self._recv_logged = True
237
+ print(f"[ssl_socket.py] fast_recv: FIRST CALL on sock_id={id(self)}", log=False)
238
+ data = _original_recv(self, bufsize, flags)
239
+ if data:
240
+ _tee_capture(self, data, 'RX')
241
+ return data
242
+
243
+ def fast_send(self, data, flags=0):
244
+ """~15ns overhead tee wrapper"""
245
+ if _SF_DEBUG and not hasattr(self, '_send_logged'):
246
+ self._send_logged = True
247
+ print(f"[ssl_socket.py] fast_send: FIRST CALL on sock_id={id(self)}, sending {len(data)} bytes", log=False)
248
+ result = _original_send(self, data, flags)
249
+ if result > 0:
250
+ _tee_capture(self, data[:result], 'TX')
251
+ return result
252
+
253
+ def fast_sendall(self, data):
254
+ """~15ns overhead tee wrapper for sendall()"""
255
+ # sendall() doesn't return number of bytes sent, it sends all or raises exception
256
+ if _SF_DEBUG and not hasattr(self, '_sendall_logged'):
257
+ self._sendall_logged = True
258
+ print(f"[ssl_socket.py] fast_sendall: FIRST CALL on sock_id={id(self)}, sending {len(data)} bytes", log=False)
259
+ _original_sendall(self, data)
260
+ # If we get here, all data was sent
261
+ _tee_capture(self, data, 'TX')
262
+ return None # sendall returns None on success
263
+
264
+ # Patch read/write (file interface)
265
+ def fast_read(self, len=1024, buffer=None):
266
+ """~15ns overhead tee wrapper"""
267
+ if _SF_DEBUG and not hasattr(self, '_read_logged'):
268
+ self._read_logged = True
269
+ print(f"[ssl_socket.py] fast_read: FIRST CALL on sock_id={id(self)}", log=False)
270
+ data = _original_read(self, len, buffer)
271
+ if buffer is not None:
272
+ # readinto mode: data is int (bytes read), actual data is in buffer
273
+ if data and isinstance(data, int) and data > 0:
274
+ # Extract bytes from buffer for capture
275
+ captured_data = bytes(buffer[:data])
276
+ _tee_capture(self, captured_data, 'RX')
277
+ elif data:
278
+ # Normal mode: data is bytes
279
+ _tee_capture(self, data, 'RX')
280
+ return data
281
+
282
+ def fast_write(self, data):
283
+ """~15ns overhead tee wrapper"""
284
+ if _SF_DEBUG and not hasattr(self, '_write_logged'):
285
+ self._write_logged = True
286
+ print(f"[ssl_socket.py] fast_write: FIRST CALL on sock_id={id(self)}, writing {len(data)} bytes", log=False)
287
+ result = _original_write(self, data)
288
+ if result > 0:
289
+ _tee_capture(self, data[:result], 'TX')
290
+ return result
291
+
292
+ # Apply patches
293
+ ssl.SSLSocket.recv = fast_recv
294
+ ssl.SSLSocket.send = fast_send
295
+ ssl.SSLSocket.sendall = fast_sendall
296
+ ssl.SSLSocket.read = fast_read
297
+ ssl.SSLSocket.write = fast_write
298
+
299
+ # Cache peername on connect to avoid repeated syscalls on hot path
300
+ _patch_ssl_connect()
301
+
302
+ # Start background processor
303
+ _start_background_processor()
304
+
305
+ _patched = True
306
+
307
+ if _SF_DEBUG:
308
+ print("[ssl_socket.py] ✓ SSL patching complete - all HTTPS traffic will be captured automatically", log=False)
309
+ print("[ssl_socket.py] Patched methods: recv, send, sendall, read, write", log=False)
310
+ print("[ssl_socket.py] requests, httpx, urllib3, aiohttp all use ssl.SSLSocket underneath", log=False)
311
+
312
+
313
+ def _patch_ssl_connect():
314
+ """Cache peername after SSL handshake to avoid syscalls on hot path"""
315
+ original_do_handshake = ssl.SSLSocket.do_handshake
316
+
317
+ def cached_do_handshake(self):
318
+ result = original_do_handshake(self)
319
+ try:
320
+ self._peername_cache = self.getpeername()
321
+ except Exception:
322
+ pass
323
+ return result
324
+
325
+ ssl.SSLSocket.do_handshake = cached_do_handshake
326
+
327
+
328
+ # ============================================================================
329
+ # BACKGROUND PROCESSING (Zero hot path impact)
330
+ # ============================================================================
331
+
332
+ _background_thread = None
333
+ _background_event = threading.Event()
334
+ _background_running = False
335
+ _sock_aggregators = {} # sock_id -> HTTPAggregator
336
+
337
+
338
+ class HTTPAggregator:
339
+ """Aggregates captured bytes into complete HTTP transactions"""
340
+
341
+ def __init__(self, sock_id):
342
+ self.sock_id = sock_id
343
+ self.tx_buffer = bytearray()
344
+ self.rx_buffer = bytearray()
345
+ self.current_transaction = None
346
+ self.last_activity = time.time()
347
+ self.peername = None
348
+ self._first_feed_logged = False # Debug flag
349
+
350
+ def feed(self, data, direction, peername=None):
351
+ """Feed captured data, parse when complete"""
352
+ # Early exit if capture is disabled globally
353
+ if not _CAPTURE_ENABLED:
354
+ return
355
+
356
+ # Debug: Log first feed for this socket
357
+ if _SF_DEBUG and not self._first_feed_logged:
358
+ self._first_feed_logged = True
359
+ print(f"[ssl_socket.py] HTTPAggregator.feed: FIRST FEED sock_id={self.sock_id}, direction={direction}, size={len(data)} bytes", log=False)
360
+
361
+ if peername:
362
+ self.peername = peername
363
+
364
+ if direction == 'TX':
365
+ # Only buffer request data if we're capturing headers or body
366
+ if _CAPTURE_REQUEST_HEADERS or _CAPTURE_REQUEST_BODY:
367
+ self.tx_buffer.extend(data)
368
+ if _SF_DEBUG:
369
+ print(f"[ssl_socket.py] HTTPAggregator.feed: TX buffer now {len(self.tx_buffer)} bytes, calling _try_parse_request()", log=False)
370
+ self._try_parse_request()
371
+ else: # RX
372
+ # Only buffer response data if we're capturing headers or body
373
+ if _CAPTURE_RESPONSE_HEADERS or _CAPTURE_RESPONSE_BODY:
374
+ self.rx_buffer.extend(data)
375
+ if _SF_DEBUG:
376
+ print(f"[ssl_socket.py] HTTPAggregator.feed: RX buffer now {len(self.rx_buffer)} bytes, calling _try_parse_response()", log=False)
377
+ self._try_parse_response()
378
+
379
+ self.last_activity = time.time()
380
+
381
+ def _try_parse_request(self):
382
+ """Try to parse complete HTTP request from buffer"""
383
+ # Look for \r\n\r\n (end of headers)
384
+ headers_end = self.tx_buffer.find(b'\r\n\r\n')
385
+ if headers_end == -1:
386
+ if _SF_DEBUG and len(self.tx_buffer) > 0:
387
+ print(f"[ssl_socket.py] _try_parse_request: Incomplete headers (buffer={len(self.tx_buffer)} bytes, waiting for \\r\\n\\r\\n)", log=False)
388
+ return # Incomplete headers
389
+
390
+ try:
391
+ headers = self.tx_buffer[:headers_end].decode('latin-1', errors='ignore')
392
+ except Exception:
393
+ return
394
+
395
+ if _SF_DEBUG:
396
+ preview = headers if len(headers) < 1000 else headers[:1000] + "...<truncated>"
397
+ print(f"[ssl_socket.py] RAW REQUEST HEADERS:\n{preview}", log=False)
398
+
399
+ lines = headers.split('\r\n')
400
+
401
+ if not lines:
402
+ return
403
+
404
+ # Parse request line
405
+ request_line = lines[0].split(' ', 2)
406
+ if len(request_line) < 3:
407
+ return
408
+
409
+ method, target, version = request_line
410
+
411
+ # Parse headers
412
+ host = None
413
+ content_length = 0
414
+ for line in lines[1:]:
415
+ if ':' in line:
416
+ key, value = line.split(':', 1)
417
+ key = key.strip().lower()
418
+ value = value.strip()
419
+ if key == 'host':
420
+ host = value
421
+ elif key == 'content-length':
422
+ try:
423
+ content_length = int(value)
424
+ except ValueError:
425
+ pass
426
+
427
+ # Use peername as fallback for host
428
+ if not host and self.peername:
429
+ host = f"{self.peername[0]}:{self.peername[1]}"
430
+
431
+ # Check if body complete
432
+ body_start = headers_end + 4
433
+ body_available = len(self.tx_buffer) - body_start
434
+
435
+ # Capture up to configured limit
436
+ body_capture_limit = MAX_REQUEST_BODY_CAPTURE if _CAPTURE_REQUEST_BODY else 0
437
+ expected_body_size = min(content_length, body_capture_limit) if _CAPTURE_REQUEST_BODY else 0
438
+
439
+ if body_available >= expected_body_size:
440
+ # Request complete!
441
+ body = self.tx_buffer[body_start:body_start + expected_body_size] if _CAPTURE_REQUEST_BODY else b''
442
+
443
+ self.current_transaction = {
444
+ 'method': method,
445
+ 'target': target,
446
+ 'host': host or 'unknown',
447
+ 'req_headers': headers if _CAPTURE_REQUEST_HEADERS else '',
448
+ 'req_body': bytes(body),
449
+ 't_start': time.perf_counter_ns()
450
+ }
451
+
452
+ if _SF_DEBUG:
453
+ print(f"[ssl_socket.py] _try_parse_request: REQUEST COMPLETE - {method} {host}{target}, waiting for response", log=False)
454
+ print(f"[ssl_socket.py] _try_parse_request: Captured req_headers_len={len(headers) if _CAPTURE_REQUEST_HEADERS else 0}, req_body_len={len(body)}", log=False)
455
+ if _CAPTURE_REQUEST_HEADERS and len(headers) > 0:
456
+ header_lines = headers.split('\r\n')[:5] # First 5 lines
457
+ print(f"[ssl_socket.py] _try_parse_request: First headers lines: {header_lines}", log=False)
458
+
459
+ # Clear buffer
460
+ self.tx_buffer.clear()
461
+
462
+ def _try_parse_response(self):
463
+ """Try to parse complete HTTP response from buffer"""
464
+ if not self.current_transaction:
465
+ if _SF_DEBUG and len(self.rx_buffer) > 0:
466
+ print(f"[ssl_socket.py] _try_parse_response: No current transaction (received response without request?), buffer={len(self.rx_buffer)} bytes", log=False)
467
+ return # No request to match
468
+
469
+ # Look for \r\n\r\n
470
+ headers_end = self.rx_buffer.find(b'\r\n\r\n')
471
+ if headers_end == -1:
472
+ if _SF_DEBUG and len(self.rx_buffer) > 0:
473
+ print(f"[ssl_socket.py] _try_parse_response: Incomplete headers (buffer={len(self.rx_buffer)} bytes, waiting for \\r\\n\\r\\n)", log=False)
474
+ return
475
+
476
+ try:
477
+ headers = self.rx_buffer[:headers_end].decode('latin-1', errors='ignore')
478
+ except Exception:
479
+ return
480
+
481
+ lines = headers.split('\r\n')
482
+
483
+ if not lines:
484
+ return
485
+
486
+ # Parse status line
487
+ status_line = lines[0].split(' ', 2)
488
+ status = 0
489
+ if len(status_line) >= 2:
490
+ try:
491
+ status = int(status_line[1])
492
+ except ValueError:
493
+ pass
494
+
495
+ # Parse content-length
496
+ content_length = 0
497
+ for line in lines[1:]:
498
+ if ':' in line:
499
+ key, value = line.split(':', 1)
500
+ if key.strip().lower() == 'content-length':
501
+ try:
502
+ content_length = int(value.strip())
503
+ except ValueError:
504
+ pass
505
+
506
+ # Check if body complete (capture up to configured limit)
507
+ body_start = headers_end + 4
508
+ body_available = len(self.rx_buffer) - body_start
509
+
510
+ # Capture up to configured limit
511
+ body_capture_limit = MAX_RESPONSE_BODY_CAPTURE if _CAPTURE_RESPONSE_BODY else 0
512
+ expected_body_size = min(content_length, body_capture_limit) if _CAPTURE_RESPONSE_BODY else 0
513
+
514
+ if body_available >= expected_body_size:
515
+ # Response complete!
516
+ body = self.rx_buffer[body_start:body_start + expected_body_size] if _CAPTURE_RESPONSE_BODY else b''
517
+
518
+ self.current_transaction.update({
519
+ 'status': status,
520
+ 'resp_headers': headers if _CAPTURE_RESPONSE_HEADERS else '',
521
+ 'resp_body': bytes(body),
522
+ 't_end': time.perf_counter_ns()
523
+ })
524
+
525
+ if _SF_DEBUG:
526
+ method = self.current_transaction.get('method', 'UNKNOWN')
527
+ host = self.current_transaction.get('host', 'unknown')
528
+ target = self.current_transaction.get('target', '/')
529
+ print(f"[ssl_socket.py] _try_parse_response: RESPONSE COMPLETE - {method} {host}{target} -> status={status}, pushing to ring", log=False)
530
+ print(f"[ssl_socket.py] _try_parse_response: Captured resp_headers_len={len(headers) if _CAPTURE_RESPONSE_HEADERS else 0}, resp_body_len={len(body)}", log=False)
531
+ if _CAPTURE_RESPONSE_HEADERS and len(headers) > 0:
532
+ header_lines = headers.split('\r\n')[:5] # First 5 lines
533
+ print(f"[ssl_socket.py] _try_parse_response: First headers lines: {header_lines}", log=False)
534
+ if len(body) > 0:
535
+ print(f"[ssl_socket.py] _try_parse_response: Body preview: {body[:100]}", log=False)
536
+
537
+ # Push to C ring
538
+ _push_transaction_to_c_ring(self.current_transaction)
539
+
540
+ # Clear state
541
+ self.current_transaction = None
542
+ self.rx_buffer.clear()
543
+
544
+
545
+ def _background_processor():
546
+ """Background thread that aggregates and pushes to C ring"""
547
+ global _background_running
548
+ _background_running = True
549
+
550
+ if _SF_DEBUG:
551
+ print("[ssl_socket.py] Background processor started", log=False)
552
+
553
+ items_processed = 0 # Debug counter
554
+
555
+ while _background_running:
556
+ _background_event.wait(timeout=0.01) # 10ms or on signal
557
+ _background_event.clear()
558
+
559
+ # Drain all capture queues
560
+ batch_count = 0
561
+ for tid, queue in list(_capture_queues.items()):
562
+ while queue:
563
+ try:
564
+ item = queue.popleft()
565
+ sock_id = item['sock_id']
566
+ batch_count += 1
567
+
568
+ # Get or create aggregator for this socket
569
+ if sock_id not in _sock_aggregators:
570
+ _sock_aggregators[sock_id] = HTTPAggregator(sock_id)
571
+ if _SF_DEBUG:
572
+ print(f"[ssl_socket.py] _background_processor: Created new aggregator for sock_id={sock_id}", log=False)
573
+
574
+ agg = _sock_aggregators[sock_id]
575
+ agg.feed(item['data'], item['direction'], item.get('peername'))
576
+
577
+ except Exception as e:
578
+ if _SF_DEBUG:
579
+ print(f"[ssl_socket.py] Background processing error: {e}", log=False)
580
+
581
+ # Debug: Log processing activity
582
+ if _SF_DEBUG and batch_count > 0:
583
+ items_processed += batch_count
584
+ print(f"[ssl_socket.py] _background_processor: Processed {batch_count} items (total: {items_processed})", log=False)
585
+
586
+ # Cleanup stale aggregators (>30s idle)
587
+ now = time.time()
588
+ stale = [sid for sid, agg in _sock_aggregators.items()
589
+ if now - agg.last_activity > 30]
590
+ for sid in stale:
591
+ del _sock_aggregators[sid]
592
+
593
+
594
+ def _start_background_processor():
595
+ """Start background processor thread"""
596
+ global _background_thread
597
+ if _background_thread is None:
598
+ _background_thread = threading.Thread(
599
+ target=_background_processor,
600
+ daemon=True,
601
+ name='ssl_capture_processor'
602
+ )
603
+ _background_thread.start()
604
+
605
+
606
+ # ============================================================================
607
+ # C RING BRIDGE (Called from background thread only)
608
+ # ============================================================================
609
+
610
+ _c_lib = None
611
+
612
+
613
+ def _init_c_bridge():
614
+ """Initialize ctypes bridge to C ring buffer"""
615
+ global _c_lib
616
+
617
+ if not _C_RING_AVAILABLE:
618
+ if _SF_DEBUG:
619
+ print("[ssl_socket.py] C ring not available (SF_SSL_PYTHON_MODE not set)", log=False)
620
+ return False
621
+
622
+ try:
623
+ # Try to load the library
624
+ import sf_veritas
625
+ lib_path = os.path.join(os.path.dirname(sf_veritas.__file__), 'libsfnettee.so')
626
+
627
+ if not os.path.exists(lib_path):
628
+ if _SF_DEBUG:
629
+ print(f"[ssl_socket.py] libsfnettee.so not found at {lib_path}", log=False)
630
+ return False
631
+
632
+ _c_lib = ctypes.CDLL(lib_path)
633
+
634
+ # Define function signature
635
+ _c_lib.sf_ring_push_from_python.argtypes = [
636
+ ctypes.c_char_p, # req_method
637
+ ctypes.c_char_p, # req_target
638
+ ctypes.c_char_p, # req_host
639
+ ctypes.c_char_p, # req_headers
640
+ ctypes.c_char_p, # req_body
641
+ ctypes.c_size_t, # req_body_len
642
+ ctypes.c_char_p, # resp_headers
643
+ ctypes.c_char_p, # resp_body
644
+ ctypes.c_size_t, # resp_body_len
645
+ ctypes.c_int, # resp_status
646
+ ctypes.c_uint64, # t_start_ns
647
+ ctypes.c_uint64, # t_end_ns
648
+ ctypes.c_char_p, # parent_trace_id
649
+ ctypes.c_int, # is_ssl
650
+ ]
651
+ _c_lib.sf_ring_push_from_python.restype = ctypes.c_int
652
+
653
+ if _SF_DEBUG:
654
+ print("[ssl_socket.py] ✓ C ring bridge initialized successfully", log=False)
655
+
656
+ return True
657
+
658
+ except Exception as e:
659
+ if _SF_DEBUG:
660
+ print(f"[ssl_socket.py] Failed to init C bridge: {e}", log=False)
661
+ return False
662
+
663
+
664
+ def _debug_log_sf_header_anomalies(raw_headers: str) -> None:
665
+ """Log duplicate or mismatched Sailfish headers when SF_DEBUG enabled."""
666
+ if not (_SF_DEBUG and raw_headers):
667
+ return
668
+
669
+ sf3_values = []
670
+ sf4_values = []
671
+ for line in raw_headers.split('\r\n'):
672
+ if not line or ':' not in line:
673
+ continue
674
+ key, value = line.split(':', 1)
675
+ key_strip = key.strip()
676
+ value_strip = value.strip()
677
+ key_lower = key_strip.lower()
678
+ if key_lower == 'x-sf3-rid':
679
+ sf3_values.append((key_strip, value_strip))
680
+ elif key_lower == 'x-sf4-prid':
681
+ sf4_values.append((key_strip, value_strip))
682
+
683
+ if len(sf3_values) > 1:
684
+ print(f"[ssl_socket.py] ⚠️ Multiple X-Sf3-Rid headers detected: {sf3_values}", log=False)
685
+ if len(sf4_values) > 1:
686
+ print(f"[ssl_socket.py] ⚠️ Multiple X-Sf4-Prid headers detected: {sf4_values}", log=False)
687
+ if sf3_values and sf4_values and sf3_values[0][1] == sf4_values[0][1]:
688
+ print(
689
+ "[ssl_socket.py] ⚠️ X-Sf3-Rid matches X-Sf4-Prid "
690
+ f"(possible parent propagation) sf3={sf3_values} sf4={sf4_values}",
691
+ log=False,
692
+ )
693
+
694
+
695
+ def _push_transaction_to_c_ring(transaction: Dict[str, Any]):
696
+ """
697
+ Emit collectNetworkRequest mutation via record_network_request().
698
+ Respects SF_NETWORKREQUEST_CAPTURE_* environment variables.
699
+
700
+ NOTE: This now uses record_network_request() instead of sf_ring_push_from_python()
701
+ to emit the standard collectNetworkRequest mutation like other library patches.
702
+ """
703
+ # Early exit if capture is disabled
704
+ if not _CAPTURE_ENABLED:
705
+ if _SF_DEBUG:
706
+ print(f"[ssl_socket.py] _push_transaction_to_c_ring: capture disabled, skipping", log=False)
707
+ return
708
+
709
+ if _SF_DEBUG:
710
+ method = transaction.get('method', 'UNKNOWN')
711
+ host = transaction.get('host', 'unknown')
712
+ target = transaction.get('target', '/')
713
+ status = transaction.get('status', 0)
714
+ print(f"[ssl_socket.py] _push_transaction_to_c_ring: DATA RECEIVED - {method} {host}{target} -> status={status}", log=False)
715
+
716
+ try:
717
+ # Extract trace_id from X-Sf3-Rid header (injected by outbound header manager)
718
+ trace_id = ""
719
+ req_headers_str = transaction.get('req_headers', '')
720
+ if req_headers_str:
721
+ _debug_log_sf_header_anomalies(req_headers_str)
722
+ # Parse X-Sf3-Rid header from raw HTTP headers
723
+ for line in req_headers_str.split('\r\n'):
724
+ if ':' in line:
725
+ key, value = line.split(':', 1)
726
+ if key.strip().lower() == 'x-sf3-rid':
727
+ trace_id = value.strip()
728
+ break
729
+
730
+ # Build full URL (SSL socket only captures HTTPS)
731
+ host = transaction.get('host', 'unknown')
732
+ target = transaction.get('target', '/')
733
+ url = f"https://{host}{target}"
734
+
735
+ # Convert headers from raw HTTP format to JSON dict
736
+ import json
737
+ try:
738
+ import orjson
739
+ HAS_ORJSON = True
740
+ except ImportError:
741
+ HAS_ORJSON = False
742
+
743
+ def parse_http_headers(headers_str: str) -> bytes:
744
+ """Parse raw HTTP headers into JSON dict."""
745
+ if not headers_str:
746
+ return b"{}"
747
+ headers_dict = {}
748
+ for line in headers_str.split('\r\n')[1:]: # Skip request/status line
749
+ if ':' in line:
750
+ key, value = line.split(':', 1)
751
+ headers_dict[key.strip()] = value.strip()
752
+ if HAS_ORJSON:
753
+ return orjson.dumps(headers_dict)
754
+ else:
755
+ return json.dumps(headers_dict).encode('utf-8')
756
+
757
+ # Prepare data based on capture flags
758
+ req_headers_json = parse_http_headers(req_headers_str) if _CAPTURE_REQUEST_HEADERS else b'{}'
759
+ req_body = transaction.get('req_body', b'') if _CAPTURE_REQUEST_BODY else b''
760
+
761
+ resp_headers_str = transaction.get('resp_headers', '')
762
+ resp_headers_json = parse_http_headers(resp_headers_str) if _CAPTURE_RESPONSE_HEADERS else b'{}'
763
+ resp_body = transaction.get('resp_body', b'') if _CAPTURE_RESPONSE_BODY else b''
764
+
765
+ # Enforce size limits (already enforced during parsing, but double-check)
766
+ if len(req_body) > MAX_REQUEST_BODY_CAPTURE:
767
+ req_body = req_body[:MAX_REQUEST_BODY_CAPTURE]
768
+ if len(resp_body) > MAX_RESPONSE_BODY_CAPTURE:
769
+ resp_body = resp_body[:MAX_RESPONSE_BODY_CAPTURE]
770
+
771
+ # Convert timestamps from nanoseconds to milliseconds
772
+ timestamp_start = transaction['t_start'] // 1_000_000 # ns to ms
773
+ timestamp_end = transaction.get('t_end', transaction['t_start']) // 1_000_000 # ns to ms
774
+
775
+ # Get status and determine success
776
+ status = transaction.get('status', 0)
777
+ success = status > 0 and status < 400
778
+
779
+ if _SF_DEBUG:
780
+ print(f"[ssl_socket.py] PREPARING TO SEND collectNetworkRequest:", log=False)
781
+ print(f"[ssl_socket.py] url={url} (type={type(url).__name__})", log=False)
782
+ print(f"[ssl_socket.py] method={transaction['method']} (type={type(transaction['method']).__name__})", log=False)
783
+ print(f"[ssl_socket.py] status_code={status}", log=False)
784
+ print(f"[ssl_socket.py] success={success}", log=False)
785
+ print(f"[ssl_socket.py] trace_id={trace_id}", log=False)
786
+ print(f"[ssl_socket.py] request_headers_size={len(req_headers_json)} bytes", log=False)
787
+ print(f"[ssl_socket.py] request_body_size={len(req_body)} bytes", log=False)
788
+ print(f"[ssl_socket.py] response_headers_size={len(resp_headers_json)} bytes", log=False)
789
+ print(f"[ssl_socket.py] response_body_size={len(resp_body)} bytes", log=False)
790
+ print(f"[ssl_socket.py] timestamp_start={timestamp_start}ms", log=False)
791
+ print(f"[ssl_socket.py] timestamp_end={timestamp_end}ms", log=False)
792
+ # Show first 200 chars of headers/body for debugging
793
+ if len(req_headers_json) > 2:
794
+ print(f"[ssl_socket.py] req_headers_preview={req_headers_json[:200]}", log=False)
795
+ if len(resp_headers_json) > 2:
796
+ print(f"[ssl_socket.py] resp_headers_preview={resp_headers_json[:200]}", log=False)
797
+ if len(req_body) > 0:
798
+ print(f"[ssl_socket.py] req_body_preview={req_body[:100]}", log=False)
799
+ if len(resp_body) > 0:
800
+ print(f"[ssl_socket.py] resp_body_preview={resp_body[:100]}", log=False)
801
+
802
+ # Emit collectNetworkRequest mutation
803
+ record_network_request(
804
+ trace_id=trace_id,
805
+ url=url,
806
+ method=transaction['method'],
807
+ status_code=status,
808
+ success=success,
809
+ timestamp_start=timestamp_start,
810
+ timestamp_end=timestamp_end,
811
+ request_data=req_body,
812
+ response_data=resp_body,
813
+ request_headers=req_headers_json,
814
+ response_headers=resp_headers_json,
815
+ )
816
+
817
+ if _SF_DEBUG:
818
+ print(f"[ssl_socket.py] ✓ SUCCESSFULLY SENT collectNetworkRequest for {transaction['method']} {url} (status={status})", log=False)
819
+
820
+ except Exception as e:
821
+ if _SF_DEBUG:
822
+ print(f"[ssl_socket.py] Failed to emit collectNetworkRequest: {e}", log=False)