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,557 @@
1
+ """
2
+ Patch httpx to inject tracing headers and capture network requests using event hooks.
3
+
4
+ • For every outbound request, propagate the SAILFISH_TRACING_HEADER + FUNCSPAN_OVERRIDE_HEADER
5
+ unless the destination host is in `domains_to_not_propagate_headers_to`.
6
+ • Fire NetworkRequestTransmitter via utils.record_network_request
7
+ so we always capture (url, status, timings, success, error).
8
+ • When LD_PRELOAD is active, ONLY inject headers (skip capture - socket layer handles it).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import os
15
+ import threading
16
+ import time
17
+ from typing import Dict, List, Optional, Tuple
18
+
19
+ try:
20
+ import wrapt
21
+
22
+ HAS_WRAPT = True
23
+ except ImportError:
24
+ HAS_WRAPT = False
25
+
26
+ from ...constants import FUNCSPAN_OVERRIDE_HEADER, SAILFISH_TRACING_HEADER
27
+ from ...thread_local import (
28
+ activate_reentrancy_guards_exception,
29
+ activate_reentrancy_guards_logging,
30
+ activate_reentrancy_guards_print,
31
+ get_funcspan_override,
32
+ trace_id_ctx,
33
+ )
34
+ from .utils import (
35
+ get_trace_and_should_propagate,
36
+ get_trace_and_should_propagate_fast,
37
+ init_fast_header_check,
38
+ inject_headers_ultrafast,
39
+ is_ssl_socket_active,
40
+ record_network_request,
41
+ )
42
+ from .._patch_tracker import is_already_patched, mark_as_patched
43
+
44
+ # JSON serialization - try fast orjson first, fallback to stdlib json
45
+ try:
46
+ import orjson
47
+
48
+ HAS_ORJSON = True
49
+ except ImportError:
50
+ import json
51
+
52
+ HAS_ORJSON = False
53
+
54
+ ###############################################################################
55
+ # Internal helpers
56
+ ###############################################################################
57
+
58
+ # header names used for re-entrancy guards
59
+ REENTRANCY_GUARD_LOGGING_PREACTIVE = "reentrancy_guard_logging_preactive"
60
+ REENTRANCY_GUARD_PRINT_PREACTIVE = "reentrancy_guard_print_preactive"
61
+ REENTRANCY_GUARD_EXCEPTIONS_PREACTIVE = "reentrancy_guard_exception_preactive"
62
+
63
+
64
+ def _tee_preload_active() -> bool:
65
+ """Detect if LD_PRELOAD tee is active (same logic as requests.py)."""
66
+ if os.getenv("SF_TEE_PRELOAD_ONLY", "0") == "1":
67
+ return True
68
+ ld = os.getenv("LD_PRELOAD", "")
69
+ return "libsfnettee.so" in ld or "_sfteepreload" in ld
70
+
71
+
72
+ def _activate_rg(headers: Dict[str, str]) -> None:
73
+ """Turn the three 'preactive' guard flags ON for downstream hops."""
74
+ headers[REENTRANCY_GUARD_LOGGING_PREACTIVE] = "true"
75
+ headers[REENTRANCY_GUARD_PRINT_PREACTIVE] = "true"
76
+ headers[REENTRANCY_GUARD_EXCEPTIONS_PREACTIVE] = "true"
77
+
78
+
79
+ def _check_rg(headers: Dict[str, str]) -> None:
80
+ """If any pre-active guard present, switch the corresponding guard on."""
81
+ if headers.get(REENTRANCY_GUARD_LOGGING_PREACTIVE, "false").lower() == "true":
82
+ activate_reentrancy_guards_logging()
83
+ if headers.get(REENTRANCY_GUARD_PRINT_PREACTIVE, "false").lower() == "true":
84
+ activate_reentrancy_guards_print()
85
+ if headers.get(REENTRANCY_GUARD_EXCEPTIONS_PREACTIVE, "false").lower() == "true":
86
+ activate_reentrancy_guards_exception()
87
+
88
+
89
+ def _prepare(
90
+ url: str,
91
+ domains_to_skip: List[str],
92
+ headers: Dict[str, str],
93
+ ) -> Tuple[str, Dict[str, str], int]:
94
+ """
95
+ Inject trace header + funcspan override header (unless excluded) and return:
96
+ trace_id, merged_headers, timestamp_ms
97
+
98
+ ULTRA-FAST: <20ns overhead for header injection.
99
+ """
100
+ trace_id, propagate = get_trace_and_should_propagate(url, domains_to_skip)
101
+ hdrs: Dict[str, str] = dict(headers or {})
102
+ _check_rg(hdrs)
103
+ if propagate:
104
+ hdrs[SAILFISH_TRACING_HEADER] = trace_id
105
+
106
+ # Inject funcspan override header if present (ContextVar lookup ~8ns)
107
+ try:
108
+ funcspan_override = get_funcspan_override()
109
+ if funcspan_override is not None:
110
+ hdrs[FUNCSPAN_OVERRIDE_HEADER] = funcspan_override
111
+ except Exception:
112
+ pass
113
+
114
+ _activate_rg(hdrs)
115
+ return trace_id, hdrs, int(time.time() * 1_000)
116
+
117
+
118
+ def _capture_request_data(request) -> bytes:
119
+ """Capture request body data as bytes."""
120
+ req_data = b""
121
+ try:
122
+ # Check if content is available
123
+ if hasattr(request, "content"):
124
+ content = request.content
125
+ if isinstance(content, bytes):
126
+ req_data = content
127
+ elif isinstance(content, str):
128
+ req_data = content.encode("utf-8")
129
+ except Exception: # noqa: BLE001
130
+ pass
131
+ return req_data
132
+
133
+
134
+ def _capture_and_record(
135
+ trace_id: str,
136
+ url: str,
137
+ method: str,
138
+ status: int,
139
+ success: bool,
140
+ err: str | None,
141
+ t0: int,
142
+ t1: int,
143
+ req_data: bytes,
144
+ req_headers: bytes,
145
+ resp_data: bytes,
146
+ resp_headers: bytes,
147
+ ) -> None:
148
+ """Schedule capture and record in background thread AFTER response is returned to user."""
149
+
150
+ def _do_record():
151
+ record_network_request(
152
+ trace_id,
153
+ url,
154
+ method,
155
+ status,
156
+ success,
157
+ err,
158
+ timestamp_start=t0,
159
+ timestamp_end=t1,
160
+ request_data=req_data,
161
+ response_data=resp_data,
162
+ request_headers=req_headers,
163
+ response_headers=resp_headers,
164
+ )
165
+
166
+ threading.Thread(target=_do_record, daemon=True).start()
167
+
168
+
169
+ ###############################################################################
170
+ # Event hook factories
171
+ ###############################################################################
172
+
173
+
174
+ def _make_request_hook(domains_to_skip: List[str], preload_active: bool):
175
+ """Create a sync request hook that injects headers before request is sent."""
176
+
177
+ if preload_active:
178
+ # ========== ULTRA-FAST PATH: When LD_PRELOAD is active ==========
179
+ def request_hook(request):
180
+ """Inject tracing headers into outbound request (ultra-fast C extension)."""
181
+ try:
182
+ url = str(request.url)
183
+ # CRITICAL: Skip if already injected (prevents double injection)
184
+ if SAILFISH_TRACING_HEADER not in request.headers:
185
+ # ULTRA-FAST: Thread-local cache + direct ContextVar.get() (<100ns!)
186
+ inject_headers_ultrafast(request.headers, url, domains_to_skip)
187
+ except Exception: # noqa: BLE001
188
+ pass # Fail silently to not break requests
189
+
190
+ else:
191
+ # ========== FULL CAPTURE PATH: When LD_PRELOAD is NOT active ==========
192
+ def request_hook(request):
193
+ """Inject tracing headers into outbound request (optimized - no debug logging)."""
194
+ try:
195
+ url = str(request.url)
196
+ trace_id, hdrs, t0 = _prepare(
197
+ url, domains_to_skip, dict(request.headers)
198
+ )
199
+
200
+ # Update request headers
201
+ request.headers.update(hdrs)
202
+
203
+ # Store metadata on request for response hook to use
204
+ # httpx Request objects always have an extensions dict
205
+ request.extensions["sf_trace_id"] = trace_id
206
+ request.extensions["sf_timestamp_start"] = t0
207
+
208
+ # Capture request data
209
+ request.extensions["sf_request_data"] = _capture_request_data(request)
210
+
211
+ # Capture request headers
212
+ if HAS_ORJSON:
213
+ request.extensions["sf_request_headers"] = orjson.dumps(
214
+ dict(request.headers)
215
+ )
216
+ else:
217
+ request.extensions["sf_request_headers"] = json.dumps(
218
+ dict(request.headers)
219
+ ).encode("utf-8")
220
+ except Exception: # noqa: BLE001
221
+ pass # Fail silently to not break requests
222
+
223
+ return request_hook
224
+
225
+
226
+ def _make_async_request_hook(domains_to_skip: List[str], preload_active: bool):
227
+ """Create an async request hook that injects headers before request is sent."""
228
+
229
+ if preload_active:
230
+ # ========== ULTRA-FAST PATH: When LD_PRELOAD is active ==========
231
+ async def async_request_hook(request):
232
+ """Inject tracing headers into outbound request (ultra-fast C extension)."""
233
+ # Get trace ID and check if we should propagate
234
+ url = str(request.url)
235
+ # CRITICAL: Skip if already injected (prevents double injection)
236
+ if SAILFISH_TRACING_HEADER not in request.headers:
237
+ # ULTRA-FAST: Thread-local cache + direct ContextVar.get() (<100ns!)
238
+ inject_headers_ultrafast(request.headers, url, domains_to_skip)
239
+
240
+ else:
241
+ # ========== FULL CAPTURE PATH: When LD_PRELOAD is NOT active ==========
242
+ async def async_request_hook(request):
243
+ """Inject tracing headers into outbound request (optimized - no debug logging)."""
244
+ # Get trace ID and timing
245
+ url = str(request.url)
246
+ trace_id = trace_id_ctx.get(None) or ""
247
+ t0 = int(time.time() * 1_000)
248
+
249
+ # Check and activate re-entrancy guards from incoming headers (avoid dict copy)
250
+ req_headers = request.headers
251
+ if (
252
+ req_headers.get(REENTRANCY_GUARD_LOGGING_PREACTIVE, "false").lower()
253
+ == "true"
254
+ ):
255
+ activate_reentrancy_guards_logging()
256
+ if (
257
+ req_headers.get(REENTRANCY_GUARD_PRINT_PREACTIVE, "false").lower()
258
+ == "true"
259
+ ):
260
+ activate_reentrancy_guards_print()
261
+ if (
262
+ req_headers.get(REENTRANCY_GUARD_EXCEPTIONS_PREACTIVE, "false").lower()
263
+ == "true"
264
+ ):
265
+ activate_reentrancy_guards_exception()
266
+
267
+ # CRITICAL: Skip if already injected (prevents double injection)
268
+ if SAILFISH_TRACING_HEADER not in req_headers:
269
+ # ULTRA-FAST: Thread-local cache + direct ContextVar.get() (<100ns!)
270
+ inject_headers_ultrafast(req_headers, url, domains_to_skip)
271
+
272
+ # Activate re-entrancy guards for downstream (inject into request)
273
+ req_headers[REENTRANCY_GUARD_LOGGING_PREACTIVE] = "true"
274
+ req_headers[REENTRANCY_GUARD_PRINT_PREACTIVE] = "true"
275
+ req_headers[REENTRANCY_GUARD_EXCEPTIONS_PREACTIVE] = "true"
276
+
277
+ # Store metadata on request for response hook to use
278
+ request.extensions["sf_trace_id"] = trace_id
279
+ request.extensions["sf_timestamp_start"] = t0
280
+
281
+ # Capture request data
282
+ request.extensions["sf_request_data"] = _capture_request_data(request)
283
+
284
+ # Capture request headers (AFTER injection)
285
+ if HAS_ORJSON:
286
+ request.extensions["sf_request_headers"] = orjson.dumps(
287
+ dict(req_headers)
288
+ )
289
+ else:
290
+ request.extensions["sf_request_headers"] = json.dumps(
291
+ dict(req_headers)
292
+ ).encode("utf-8")
293
+
294
+ return async_request_hook
295
+
296
+
297
+ def _make_response_hook(preload_active: bool):
298
+ """Create a response hook that captures and records response data."""
299
+
300
+ def response_hook(response):
301
+ """Capture response data and record the network request."""
302
+ # Skip recording if LD_PRELOAD is active (socket layer already captured it)
303
+ if preload_active:
304
+ return
305
+
306
+ # Skip capture for HTTPS when ssl_socket.py is active (avoids double-capture)
307
+ url = str(response.request.url)
308
+ is_https = url.startswith("https://")
309
+ if is_https and is_ssl_socket_active():
310
+ return
311
+
312
+ # Extract metadata from request
313
+ request = response.request
314
+ trace_id = request.extensions.get("sf_trace_id", "")
315
+ t0 = request.extensions.get("sf_timestamp_start", 0)
316
+ req_data = request.extensions.get("sf_request_data", b"")
317
+ req_headers = request.extensions.get("sf_request_headers", b"")
318
+
319
+ # Capture response data
320
+ url = str(request.url)
321
+ method = str(request.method).upper()
322
+ status = response.status_code
323
+ success = status < 400
324
+ t1 = int(time.time() * 1_000)
325
+
326
+ resp_data = b""
327
+ resp_headers = b""
328
+
329
+ try:
330
+ # Capture response body - check if already consumed/materialized
331
+ # Don't force materialization of streaming responses
332
+ if hasattr(response, '_content') and response._content is not None:
333
+ # Response body already materialized (non-streaming or already read)
334
+ resp_data = response._content
335
+ elif hasattr(response, 'is_stream_consumed') and not response.is_stream_consumed:
336
+ # Streaming response not yet consumed - don't materialize
337
+ # We'll capture what we can without breaking streaming behavior
338
+ resp_data = b""
339
+ else:
340
+ # Safe to access .content (either not streaming or already consumed)
341
+ resp_data = response.content
342
+
343
+ # Capture response headers
344
+ if HAS_ORJSON:
345
+ resp_headers = orjson.dumps({str(k): str(v) for k, v in response.headers.items()})
346
+ else:
347
+ resp_headers = json.dumps({str(k): str(v) for k, v in response.headers.items()}).encode("utf-8")
348
+ except Exception: # noqa: BLE001
349
+ pass
350
+
351
+ # Record in background thread
352
+ _capture_and_record(
353
+ trace_id,
354
+ url,
355
+ method,
356
+ status,
357
+ success,
358
+ None,
359
+ t0,
360
+ t1,
361
+ req_data,
362
+ req_headers,
363
+ resp_data,
364
+ resp_headers,
365
+ )
366
+
367
+ return response_hook
368
+
369
+
370
+ def _make_async_response_hook(preload_active: bool):
371
+ """Create an async response hook that captures and records response data."""
372
+
373
+ async def async_response_hook(response):
374
+ """Capture response data and record the network request (optimized - no debug logging)."""
375
+ # Skip recording if LD_PRELOAD is active (socket layer already captured it)
376
+ if preload_active:
377
+ return
378
+
379
+ # Skip capture for HTTPS when ssl_socket.py is active (avoids double-capture)
380
+ url = str(response.request.url)
381
+ is_https = url.startswith("https://")
382
+ if is_https and is_ssl_socket_active():
383
+ return
384
+
385
+ try:
386
+ # Extract metadata from request
387
+ request = response.request
388
+ trace_id = request.extensions.get("sf_trace_id", "")
389
+ t0 = request.extensions.get("sf_timestamp_start", 0)
390
+ req_data = request.extensions.get("sf_request_data", b"")
391
+ req_headers = request.extensions.get("sf_request_headers", b"")
392
+
393
+ # Capture response data
394
+ url = str(request.url)
395
+ method = str(request.method).upper()
396
+ status = response.status_code
397
+ success = status < 400
398
+ t1 = int(time.time() * 1_000)
399
+
400
+ resp_data = b""
401
+ resp_headers = b""
402
+
403
+ try:
404
+ # Capture response body - check if already consumed/materialized
405
+ # Don't force materialization of streaming responses
406
+ if hasattr(response, '_content') and response._content is not None:
407
+ # Response body already materialized (non-streaming or already read)
408
+ resp_data = response._content
409
+ elif hasattr(response, 'is_stream_consumed') and not response.is_stream_consumed:
410
+ # Streaming response not yet consumed - don't materialize
411
+ # We'll capture what we can without breaking streaming behavior
412
+ resp_data = b""
413
+ else:
414
+ # Safe to read (either not streaming or already consumed)
415
+ # For non-streaming async responses, read the body
416
+ try:
417
+ await response.aread()
418
+ resp_data = response.content
419
+ except asyncio.CancelledError:
420
+ raise # CRITICAL: Must re-raise CancelledError immediately
421
+ except Exception:
422
+ resp_data = b""
423
+
424
+ # Capture response headers
425
+ if HAS_ORJSON:
426
+ resp_headers = orjson.dumps({str(k): str(v) for k, v in response.headers.items()})
427
+ else:
428
+ resp_headers = json.dumps({str(k): str(v) for k, v in response.headers.items()}).encode("utf-8")
429
+ except Exception: # noqa: BLE001
430
+ pass
431
+
432
+ # Record in background thread
433
+ _capture_and_record(
434
+ trace_id,
435
+ url,
436
+ method,
437
+ status,
438
+ success,
439
+ None,
440
+ t0,
441
+ t1,
442
+ req_data,
443
+ req_headers,
444
+ resp_data,
445
+ resp_headers,
446
+ )
447
+ except Exception: # noqa: BLE001
448
+ pass # Silently fail to not break requests
449
+
450
+ return async_response_hook
451
+
452
+
453
+ ###############################################################################
454
+ # Top-level patch function
455
+ ###############################################################################
456
+
457
+
458
+ def patch_httpx(domains_to_not_propagate_headers_to: Optional[List[str]] = None):
459
+ """
460
+ Patch httpx to inject SAILFISH_TRACING_HEADER into all outbound requests
461
+ using event hooks. Safe to call even if httpx is not installed.
462
+
463
+ When LD_PRELOAD is active:
464
+ - ALWAYS inject headers (trace_id + funcspan_override)
465
+ - SKIP capture/emission (LD_PRELOAD handles at socket layer)
466
+ - Uses ultra-fast C extension for <10ns overhead
467
+ """
468
+ # Idempotency guard: prevent double-patching (handles forks, reloading)
469
+ if is_already_patched("httpx"):
470
+ return
471
+ mark_as_patched("httpx")
472
+
473
+ try:
474
+ import httpx
475
+ except ImportError:
476
+ return # No httpx installed—nothing to patch
477
+
478
+ domains = domains_to_not_propagate_headers_to or []
479
+ preload_active = _tee_preload_active()
480
+
481
+ # Initialize C extension for ultra-fast header checking (if available)
482
+ if preload_active:
483
+ init_fast_header_check(domains)
484
+
485
+ # Create hooks
486
+ sync_request_hook = _make_request_hook(domains, preload_active)
487
+ async_request_hook = _make_async_request_hook(domains, preload_active)
488
+ sync_response_hook = _make_response_hook(preload_active)
489
+ async_response_hook = _make_async_response_hook(preload_active)
490
+
491
+ # Patch Client.__init__ to attach sync hooks
492
+ if HAS_WRAPT:
493
+
494
+ def instrumented_client_init(wrapped, instance, args, kwargs):
495
+ """Ultra-fast hook injection using wrapt."""
496
+ # Get existing event_hooks or create empty dict
497
+ event_hooks = kwargs.get("event_hooks") or {}
498
+
499
+ # Add our sync hooks to the request and response lists
500
+ event_hooks.setdefault("request", []).append(sync_request_hook)
501
+ event_hooks.setdefault("response", []).append(sync_response_hook)
502
+
503
+ kwargs["event_hooks"] = event_hooks
504
+ return wrapped(*args, **kwargs)
505
+
506
+ wrapt.wrap_function_wrapper(
507
+ "httpx", "Client.__init__", instrumented_client_init
508
+ )
509
+ else:
510
+ original_client_init = httpx.Client.__init__
511
+
512
+ def patched_client_init(self, *args, **kwargs):
513
+ # Get existing event_hooks or create empty dict
514
+ event_hooks = kwargs.get("event_hooks") or {}
515
+
516
+ # Add our sync hooks to the request and response lists
517
+ event_hooks.setdefault("request", []).append(sync_request_hook)
518
+ event_hooks.setdefault("response", []).append(sync_response_hook)
519
+
520
+ kwargs["event_hooks"] = event_hooks
521
+ original_client_init(self, *args, **kwargs)
522
+
523
+ httpx.Client.__init__ = patched_client_init
524
+
525
+ # Patch AsyncClient.__init__ to attach async hooks
526
+ if HAS_WRAPT:
527
+
528
+ def instrumented_async_client_init(wrapped, instance, args, kwargs):
529
+ """Ultra-fast hook injection using wrapt."""
530
+ # Get existing event_hooks or create empty dict
531
+ event_hooks = kwargs.get("event_hooks") or {}
532
+
533
+ # Add our ASYNC hooks to the request and response lists
534
+ event_hooks.setdefault("request", []).append(async_request_hook)
535
+ event_hooks.setdefault("response", []).append(async_response_hook)
536
+
537
+ kwargs["event_hooks"] = event_hooks
538
+ return wrapped(*args, **kwargs)
539
+
540
+ wrapt.wrap_function_wrapper(
541
+ "httpx", "AsyncClient.__init__", instrumented_async_client_init
542
+ )
543
+ else:
544
+ original_async_client_init = httpx.AsyncClient.__init__
545
+
546
+ def patched_async_client_init(self, *args, **kwargs):
547
+ # Get existing event_hooks or create empty dict
548
+ event_hooks = kwargs.get("event_hooks") or {}
549
+
550
+ # Add our ASYNC hooks to the request and response lists
551
+ event_hooks.setdefault("request", []).append(async_request_hook)
552
+ event_hooks.setdefault("response", []).append(async_response_hook)
553
+
554
+ kwargs["event_hooks"] = event_hooks
555
+ original_async_client_init(self, *args, **kwargs)
556
+
557
+ httpx.AsyncClient.__init__ = patched_async_client_init