sf-veritas 0.10.5__cp313-cp313-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.

Potentially problematic release.


This version of sf-veritas might be problematic. Click here for more details.

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