sf-veritas 0.10.3__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.

Potentially problematic release.


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

Files changed (132) hide show
  1. sf_veritas/__init__.py +20 -0
  2. sf_veritas/_sffastlog.c +889 -0
  3. sf_veritas/_sffastlog.cpython-314-x86_64-linux-gnu.so +0 -0
  4. sf_veritas/_sffastnet.c +924 -0
  5. sf_veritas/_sffastnet.cpython-314-x86_64-linux-gnu.so +0 -0
  6. sf_veritas/_sffastnetworkrequest.c +730 -0
  7. sf_veritas/_sffastnetworkrequest.cpython-314-x86_64-linux-gnu.so +0 -0
  8. sf_veritas/_sffuncspan.c +2155 -0
  9. sf_veritas/_sffuncspan.cpython-314-x86_64-linux-gnu.so +0 -0
  10. sf_veritas/_sffuncspan_config.c +617 -0
  11. sf_veritas/_sffuncspan_config.cpython-314-x86_64-linux-gnu.so +0 -0
  12. sf_veritas/_sfheadercheck.c +341 -0
  13. sf_veritas/_sfheadercheck.cpython-314-x86_64-linux-gnu.so +0 -0
  14. sf_veritas/_sfnetworkhop.c +1451 -0
  15. sf_veritas/_sfnetworkhop.cpython-314-x86_64-linux-gnu.so +0 -0
  16. sf_veritas/_sfservice.c +1175 -0
  17. sf_veritas/_sfservice.cpython-314-x86_64-linux-gnu.so +0 -0
  18. sf_veritas/_sfteepreload.c +5167 -0
  19. sf_veritas/app_config.py +49 -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/concurrent_futures.py +19 -0
  49. sf_veritas/patches/constants.py +1 -0
  50. sf_veritas/patches/exceptions.py +82 -0
  51. sf_veritas/patches/multiprocessing.py +32 -0
  52. sf_veritas/patches/network_libraries/__init__.py +76 -0
  53. sf_veritas/patches/network_libraries/aiohttp.py +281 -0
  54. sf_veritas/patches/network_libraries/curl_cffi.py +363 -0
  55. sf_veritas/patches/network_libraries/http_client.py +419 -0
  56. sf_veritas/patches/network_libraries/httpcore.py +515 -0
  57. sf_veritas/patches/network_libraries/httplib2.py +204 -0
  58. sf_veritas/patches/network_libraries/httpx.py +515 -0
  59. sf_veritas/patches/network_libraries/niquests.py +211 -0
  60. sf_veritas/patches/network_libraries/pycurl.py +385 -0
  61. sf_veritas/patches/network_libraries/requests.py +633 -0
  62. sf_veritas/patches/network_libraries/tornado.py +341 -0
  63. sf_veritas/patches/network_libraries/treq.py +270 -0
  64. sf_veritas/patches/network_libraries/urllib_request.py +468 -0
  65. sf_veritas/patches/network_libraries/utils.py +398 -0
  66. sf_veritas/patches/os.py +17 -0
  67. sf_veritas/patches/threading.py +218 -0
  68. sf_veritas/patches/web_frameworks/__init__.py +54 -0
  69. sf_veritas/patches/web_frameworks/aiohttp.py +793 -0
  70. sf_veritas/patches/web_frameworks/async_websocket_consumer.py +317 -0
  71. sf_veritas/patches/web_frameworks/blacksheep.py +527 -0
  72. sf_veritas/patches/web_frameworks/bottle.py +502 -0
  73. sf_veritas/patches/web_frameworks/cherrypy.py +678 -0
  74. sf_veritas/patches/web_frameworks/cors_utils.py +122 -0
  75. sf_veritas/patches/web_frameworks/django.py +944 -0
  76. sf_veritas/patches/web_frameworks/eve.py +395 -0
  77. sf_veritas/patches/web_frameworks/falcon.py +926 -0
  78. sf_veritas/patches/web_frameworks/fastapi.py +724 -0
  79. sf_veritas/patches/web_frameworks/flask.py +520 -0
  80. sf_veritas/patches/web_frameworks/klein.py +501 -0
  81. sf_veritas/patches/web_frameworks/litestar.py +551 -0
  82. sf_veritas/patches/web_frameworks/pyramid.py +428 -0
  83. sf_veritas/patches/web_frameworks/quart.py +824 -0
  84. sf_veritas/patches/web_frameworks/robyn.py +697 -0
  85. sf_veritas/patches/web_frameworks/sanic.py +857 -0
  86. sf_veritas/patches/web_frameworks/starlette.py +723 -0
  87. sf_veritas/patches/web_frameworks/strawberry.py +813 -0
  88. sf_veritas/patches/web_frameworks/tornado.py +481 -0
  89. sf_veritas/patches/web_frameworks/utils.py +91 -0
  90. sf_veritas/print_override.py +13 -0
  91. sf_veritas/regular_data_transmitter.py +409 -0
  92. sf_veritas/request_interceptor.py +401 -0
  93. sf_veritas/request_utils.py +550 -0
  94. sf_veritas/server_status.py +1 -0
  95. sf_veritas/shutdown_flag.py +11 -0
  96. sf_veritas/subprocess_startup.py +3 -0
  97. sf_veritas/test_cli.py +145 -0
  98. sf_veritas/thread_local.py +970 -0
  99. sf_veritas/timeutil.py +114 -0
  100. sf_veritas/transmit_exception_to_sailfish.py +28 -0
  101. sf_veritas/transmitter.py +132 -0
  102. sf_veritas/types.py +47 -0
  103. sf_veritas/unified_interceptor.py +1580 -0
  104. sf_veritas/utils.py +39 -0
  105. sf_veritas-0.10.3.dist-info/METADATA +97 -0
  106. sf_veritas-0.10.3.dist-info/RECORD +132 -0
  107. sf_veritas-0.10.3.dist-info/WHEEL +5 -0
  108. sf_veritas-0.10.3.dist-info/entry_points.txt +2 -0
  109. sf_veritas-0.10.3.dist-info/top_level.txt +1 -0
  110. sf_veritas.libs/libbrotlicommon-6ce2a53c.so.1.0.6 +0 -0
  111. sf_veritas.libs/libbrotlidec-811d1be3.so.1.0.6 +0 -0
  112. sf_veritas.libs/libcom_err-730ca923.so.2.1 +0 -0
  113. sf_veritas.libs/libcrypt-52aca757.so.1.1.0 +0 -0
  114. sf_veritas.libs/libcrypto-bdaed0ea.so.1.1.1k +0 -0
  115. sf_veritas.libs/libcurl-eaa3cf66.so.4.5.0 +0 -0
  116. sf_veritas.libs/libgssapi_krb5-323bbd21.so.2.2 +0 -0
  117. sf_veritas.libs/libidn2-2f4a5893.so.0.3.6 +0 -0
  118. sf_veritas.libs/libk5crypto-9a74ff38.so.3.1 +0 -0
  119. sf_veritas.libs/libkeyutils-2777d33d.so.1.6 +0 -0
  120. sf_veritas.libs/libkrb5-a55300e8.so.3.3 +0 -0
  121. sf_veritas.libs/libkrb5support-e6594cfc.so.0.1 +0 -0
  122. sf_veritas.libs/liblber-2-d20824ef.4.so.2.10.9 +0 -0
  123. sf_veritas.libs/libldap-2-cea2a960.4.so.2.10.9 +0 -0
  124. sf_veritas.libs/libnghttp2-39367a22.so.14.17.0 +0 -0
  125. sf_veritas.libs/libpcre2-8-516f4c9d.so.0.7.1 +0 -0
  126. sf_veritas.libs/libpsl-99becdd3.so.5.3.1 +0 -0
  127. sf_veritas.libs/libsasl2-7de4d792.so.3.0.0 +0 -0
  128. sf_veritas.libs/libselinux-d0805dcb.so.1 +0 -0
  129. sf_veritas.libs/libssh-c11d285b.so.4.8.7 +0 -0
  130. sf_veritas.libs/libssl-60250281.so.1.1.1k +0 -0
  131. sf_veritas.libs/libunistring-05abdd40.so.2.1.0 +0 -0
  132. sf_veritas.libs/libuuid-95b83d40.so.1.3.0 +0 -0
@@ -0,0 +1,341 @@
1
+ """
2
+ Monkey-patch Tornado's HTTP clients so that
3
+
4
+ • Every outbound request carries SAILFISH_TRACING_HEADER
5
+ (unless the destination host is excluded).
6
+ • Every request – success or failure – triggers record_network_request(…).
7
+
8
+ Covers
9
+ • tornado.httpclient.AsyncHTTPClient.fetch (await-able)
10
+ • tornado.httpclient.HTTPClient.fetch (blocking/sync)
11
+ Safe to call repeatedly; patches only once per process.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import time
18
+ from typing import List, Optional, Tuple
19
+
20
+ from ...thread_local import trace_id_ctx
21
+
22
+ try:
23
+ import wrapt
24
+
25
+ HAS_WRAPT = True
26
+ except ImportError:
27
+ HAS_WRAPT = False
28
+
29
+
30
+ # JSON serialization - try fast orjson first, fallback to stdlib json
31
+ try:
32
+ import orjson
33
+
34
+ HAS_ORJSON = True
35
+ except ImportError:
36
+ import json
37
+
38
+ HAS_ORJSON = False
39
+
40
+ from .utils import (
41
+ init_fast_header_check,
42
+ inject_headers_ultrafast,
43
+ record_network_request,
44
+ )
45
+
46
+
47
+ def _tee_preload_active() -> bool:
48
+ """Detect if LD_PRELOAD tee is active (same logic as http_client.py)."""
49
+ if os.getenv("SF_TEE_PRELOAD_ONLY", "0") == "1":
50
+ return True
51
+ ld = os.getenv("LD_PRELOAD", "")
52
+ return "libsfnettee.so" in ld or "_sfteepreload" in ld
53
+
54
+
55
+ def patch_tornado(domains_to_not_propagate_headers_to: Optional[List[str]] = None):
56
+ try:
57
+ # Tornado is optional; exit silently if missing
58
+ from tornado.httpclient import AsyncHTTPClient, HTTPClient, HTTPRequest
59
+ except ImportError:
60
+ return
61
+
62
+ exclude: List[str] = domains_to_not_propagate_headers_to or []
63
+ preload_active = _tee_preload_active()
64
+
65
+ # Initialize C extension for ultra-fast header checking (if available)
66
+ if preload_active:
67
+ init_fast_header_check(exclude)
68
+
69
+ # ------------------------------------------------------------------ #
70
+ # Helpers shared by sync & async wrappers
71
+ # ------------------------------------------------------------------ #
72
+ def _resolve(
73
+ req_or_url, kwargs
74
+ ) -> Tuple[str, str, dict]: # → (url, METHOD, headers_dict)
75
+ """
76
+ Handle both call styles:
77
+
78
+ client.fetch("https://foo", method="POST", headers={...})
79
+ client.fetch(HTTPRequest(...))
80
+
81
+ Always returns a mutable *headers* dict.
82
+ """
83
+ if isinstance(req_or_url, HTTPRequest):
84
+ url = req_or_url.url
85
+ method = (req_or_url.method or "GET").upper()
86
+ hdrs = dict(req_or_url.headers or {})
87
+ else:
88
+ url = str(req_or_url)
89
+ method = kwargs.get("method", "GET").upper()
90
+ hdrs = dict(kwargs.get("headers", {}) or {})
91
+ return url, method, hdrs
92
+
93
+ def _inject(
94
+ req_or_url, kwargs, hdrs: dict
95
+ ): # mutate request object *or* kwargs to carry hdrs
96
+ if isinstance(req_or_url, HTTPRequest):
97
+ req_or_url.headers = hdrs
98
+ else:
99
+ kwargs["headers"] = hdrs
100
+ return req_or_url, kwargs
101
+
102
+ # Unified _prepare function for both fast and slow paths
103
+ def _prepare(url: str, hdrs: dict):
104
+ """Return (trace_id, merged_headers, start_ms). Ultra-fast header injection."""
105
+ # Make a copy for mutation
106
+ out = dict(hdrs)
107
+
108
+ # ULTRA-FAST: inject_headers_ultrafast does domain filtering + header injection (~100ns)
109
+ inject_headers_ultrafast(out, url, exclude)
110
+
111
+ # Get trace_id for capture (only used in slow path)
112
+ if not preload_active:
113
+ trace_id = trace_id_ctx.get(None) or ""
114
+ else:
115
+ trace_id = "" # Not needed for fast path
116
+
117
+ return trace_id, out, int(time.time() * 1_000)
118
+
119
+ # ------------------------------------------------------------------ #
120
+ # AsyncHTTPClient.fetch wrapper
121
+ # ------------------------------------------------------------------ #
122
+ if preload_active:
123
+ # ========== ULTRA-FAST PATH: When LD_PRELOAD is active ==========
124
+ if HAS_WRAPT:
125
+ # FASTEST: Use wrapt directly (OTEL-style for minimal overhead)
126
+ async def instrumented_async_fetch(wrapped, instance, args, kwargs):
127
+ """Ultra-fast header injection using C extension via wrapt."""
128
+ req_or_url = args[0] if len(args) > 0 else kwargs.get("request", "")
129
+ url, method, hdrs_cur = _resolve(req_or_url, kwargs)
130
+ trace_id, hdrs_new, t0 = _prepare(url, hdrs_cur)
131
+
132
+ # Inject headers back into args/kwargs
133
+ if len(args) > 0:
134
+ args = (_inject(args[0], kwargs, hdrs_new)[0],) + args[1:]
135
+ else:
136
+ req_or_url, kwargs = _inject(req_or_url, kwargs, hdrs_new)
137
+
138
+ # NO capture, NO timing, NO record - immediate return!
139
+ return await wrapped(*args, **kwargs)
140
+
141
+ wrapt.wrap_function_wrapper(
142
+ "tornado.httpclient", "AsyncHTTPClient.fetch", instrumented_async_fetch
143
+ )
144
+ else:
145
+ # Fallback: Direct patching if wrapt not available
146
+ original_async_fetch = AsyncHTTPClient.fetch
147
+
148
+ async def patched_async_fetch(self, req_or_url, *args, **kwargs):
149
+ url, method, hdrs_cur = _resolve(req_or_url, kwargs)
150
+ trace_id, hdrs_new, t0 = _prepare(url, hdrs_cur)
151
+ req_or_url, kwargs = _inject(req_or_url, kwargs, hdrs_new)
152
+
153
+ # NO capture, NO timing, NO record - immediate return!
154
+ return await original_async_fetch(self, req_or_url, *args, **kwargs)
155
+
156
+ AsyncHTTPClient.fetch = patched_async_fetch # type: ignore[assignment]
157
+
158
+ else:
159
+ # ========== FULL CAPTURE PATH: When LD_PRELOAD is NOT active ==========
160
+ async def patched_async_fetch(self, req_or_url, *args, **kwargs):
161
+ url, method, hdrs_cur = _resolve(req_or_url, kwargs)
162
+ trace_id, hdrs_new, t0 = _prepare(url, hdrs_cur)
163
+ req_or_url, kwargs = _inject(req_or_url, kwargs, hdrs_new)
164
+
165
+ # Capture request data
166
+ req_data = b""
167
+ req_headers = b""
168
+ try:
169
+ body = None
170
+ if isinstance(req_or_url, HTTPRequest):
171
+ body = req_or_url.body
172
+ else:
173
+ body = kwargs.get("body")
174
+
175
+ if body:
176
+ if isinstance(body, bytes):
177
+ req_data = body
178
+ elif isinstance(body, str):
179
+ req_data = body.encode("utf-8")
180
+
181
+ # Capture request headers
182
+ if HAS_ORJSON:
183
+ req_headers = orjson.dumps({str(k): str(v) for k, v in hdrs_new.items()})
184
+ else:
185
+ req_headers = json.dumps({str(k): str(v) for k, v in hdrs_new.items()}).encode("utf-8")
186
+ except Exception: # noqa: BLE001
187
+ pass
188
+
189
+ status, success, err = 0, False, None
190
+ resp_data = b""
191
+ resp_headers = b""
192
+ try:
193
+ resp = await original_async_fetch(self, req_or_url, *args, **kwargs)
194
+ status = getattr(resp, "code", 0)
195
+ success = status < 400
196
+
197
+ # Capture response data
198
+ try:
199
+ resp_data = getattr(resp, "body", b"")
200
+ # Capture response headers
201
+ if HAS_ORJSON:
202
+ resp_headers = orjson.dumps({str(k): str(v) for k, v in resp.headers.items()})
203
+ else:
204
+ resp_headers = json.dumps({str(k): str(v) for k, v in resp.headers.items()}).encode("utf-8")
205
+ except Exception: # noqa: BLE001
206
+ pass
207
+
208
+ return resp
209
+ except Exception as exc: # noqa: BLE001
210
+ err = str(exc)[:255]
211
+ raise
212
+ finally:
213
+ record_network_request(
214
+ trace_id,
215
+ url,
216
+ method,
217
+ status,
218
+ success,
219
+ err,
220
+ timestamp_start=t0,
221
+ timestamp_end=int(time.time() * 1_000),
222
+ request_data=req_data,
223
+ response_data=resp_data,
224
+ request_headers=req_headers,
225
+ response_headers=resp_headers,
226
+ )
227
+
228
+ if not HAS_WRAPT:
229
+ AsyncHTTPClient.fetch = patched_async_fetch # type: ignore[assignment]
230
+
231
+ # ------------------------------------------------------------------ #
232
+ # HTTPClient.fetch wrapper (blocking)
233
+ # ------------------------------------------------------------------ #
234
+ if preload_active:
235
+ # ========== ULTRA-FAST PATH: When LD_PRELOAD is active ==========
236
+ if HAS_WRAPT:
237
+ # FASTEST: Use wrapt directly (OTEL-style for minimal overhead)
238
+ def instrumented_sync_fetch(wrapped, instance, args, kwargs):
239
+ """Ultra-fast header injection using C extension via wrapt."""
240
+ req_or_url = args[0] if len(args) > 0 else kwargs.get("request", "")
241
+ url, method, hdrs_cur = _resolve(req_or_url, kwargs)
242
+ trace_id, hdrs_new, t0 = _prepare(url, hdrs_cur)
243
+
244
+ # Inject headers back into args/kwargs
245
+ if len(args) > 0:
246
+ args = (_inject(args[0], kwargs, hdrs_new)[0],) + args[1:]
247
+ else:
248
+ req_or_url, kwargs = _inject(req_or_url, kwargs, hdrs_new)
249
+
250
+ # NO capture, NO timing, NO record - immediate return!
251
+ return wrapped(*args, **kwargs)
252
+
253
+ wrapt.wrap_function_wrapper(
254
+ "tornado.httpclient", "HTTPClient.fetch", instrumented_sync_fetch
255
+ )
256
+ else:
257
+ # Fallback: Direct patching if wrapt not available
258
+ original_sync_fetch = HTTPClient.fetch
259
+
260
+ def patched_sync_fetch(self, req_or_url, *args, **kwargs):
261
+ url, method, hdrs_cur = _resolve(req_or_url, kwargs)
262
+ trace_id, hdrs_new, t0 = _prepare(url, hdrs_cur)
263
+ req_or_url, kwargs = _inject(req_or_url, kwargs, hdrs_new)
264
+
265
+ # NO capture, NO timing, NO record - immediate return!
266
+ return original_sync_fetch(self, req_or_url, *args, **kwargs)
267
+
268
+ HTTPClient.fetch = patched_sync_fetch # type: ignore[assignment]
269
+
270
+ else:
271
+ # ========== FULL CAPTURE PATH: When LD_PRELOAD is NOT active ==========
272
+ def patched_sync_fetch(self, req_or_url, *args, **kwargs):
273
+ url, method, hdrs_cur = _resolve(req_or_url, kwargs)
274
+ trace_id, hdrs_new, t0 = _prepare(url, hdrs_cur)
275
+ req_or_url, kwargs = _inject(req_or_url, kwargs, hdrs_new)
276
+
277
+ # Capture request data
278
+ req_data = b""
279
+ req_headers = b""
280
+ try:
281
+ body = None
282
+ if isinstance(req_or_url, HTTPRequest):
283
+ body = req_or_url.body
284
+ else:
285
+ body = kwargs.get("body")
286
+
287
+ if body:
288
+ if isinstance(body, bytes):
289
+ req_data = body
290
+ elif isinstance(body, str):
291
+ req_data = body.encode("utf-8")
292
+
293
+ # Capture request headers
294
+ if HAS_ORJSON:
295
+ req_headers = orjson.dumps({str(k): str(v) for k, v in hdrs_new.items()})
296
+ else:
297
+ req_headers = json.dumps({str(k): str(v) for k, v in hdrs_new.items()}).encode("utf-8")
298
+ except Exception: # noqa: BLE001
299
+ pass
300
+
301
+ status, success, err = 0, False, None
302
+ resp_data = b""
303
+ resp_headers = b""
304
+ try:
305
+ resp = original_sync_fetch(self, req_or_url, *args, **kwargs)
306
+ status = getattr(resp, "code", 0)
307
+ success = status < 400
308
+
309
+ # Capture response data
310
+ try:
311
+ resp_data = getattr(resp, "body", b"")
312
+ # Capture response headers
313
+ if HAS_ORJSON:
314
+ resp_headers = orjson.dumps({str(k): str(v) for k, v in resp.headers.items()})
315
+ else:
316
+ resp_headers = json.dumps({str(k): str(v) for k, v in resp.headers.items()}).encode("utf-8")
317
+ except Exception: # noqa: BLE001
318
+ pass
319
+
320
+ return resp
321
+ except Exception as exc: # noqa: BLE001
322
+ err = str(exc)[:255]
323
+ raise
324
+ finally:
325
+ record_network_request(
326
+ trace_id,
327
+ url,
328
+ method,
329
+ status,
330
+ success,
331
+ err,
332
+ timestamp_start=t0,
333
+ timestamp_end=int(time.time() * 1_000),
334
+ request_data=req_data,
335
+ response_data=resp_data,
336
+ request_headers=req_headers,
337
+ response_headers=resp_headers,
338
+ )
339
+
340
+ if not HAS_WRAPT:
341
+ HTTPClient.fetch = patched_sync_fetch # type: ignore[assignment]
@@ -0,0 +1,270 @@
1
+ """
2
+ Header propagation + network-recording patch for **Treq**.
3
+
4
+ • Propagates SAILFISH_TRACING_HEADER (unless excluded destination).
5
+ • Records every outbound request via record_network_request(…).
6
+
7
+ It also guarantees that Twisted's reactor is *running*:
8
+
9
+ 1. Prefer installing the asyncio reactor early.
10
+ 2. If a different reactor is already installed, start it in a background thread
11
+ (if it isn't running yet), so Deferreds produced by treq will fire.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ import os
18
+ import threading
19
+ import time
20
+ from typing import List, Optional
21
+
22
+ from ...thread_local import trace_id_ctx
23
+
24
+ try:
25
+ import wrapt
26
+
27
+ HAS_WRAPT = True
28
+ except ImportError:
29
+ HAS_WRAPT = False
30
+
31
+ # JSON serialization - try fast orjson first, fallback to stdlib json
32
+ try:
33
+ import orjson
34
+
35
+ HAS_ORJSON = True
36
+ except ImportError:
37
+ import json
38
+
39
+ HAS_ORJSON = False
40
+
41
+ from ..constants import supported_network_verbs as verbs
42
+ from .utils import (
43
+ init_fast_header_check,
44
+ inject_headers_ultrafast,
45
+ record_network_request,
46
+ )
47
+
48
+
49
+ def _tee_preload_active() -> bool:
50
+ """Detect if LD_PRELOAD tee is active (same logic as http_client.py)."""
51
+ if os.getenv("SF_TEE_PRELOAD_ONLY", "0") == "1":
52
+ return True
53
+ ld = os.getenv("LD_PRELOAD", "")
54
+ return "libsfnettee.so" in ld or "_sfteepreload" in ld
55
+
56
+
57
+ def _ensure_reactor_running() -> None:
58
+ """
59
+ • Try to replace Twisted's default reactor with the asyncio one.
60
+ • If that fails because a reactor is already installed, make sure the
61
+ existing reactor is *running* (start it in a daemon thread if needed).
62
+
63
+ NOTE: If the application has its own reactor startup logic, it should
64
+ start the reactor BEFORE importing treq to avoid conflicts.
65
+ """
66
+ # Twisted import must be inside this function to avoid premature reactor load
67
+ from twisted.internet import reactor
68
+
69
+ # If reactor is already running, nothing to do
70
+ if reactor.running:
71
+ return
72
+
73
+ try:
74
+ from twisted.internet import asyncioreactor
75
+
76
+ # Already an asyncio reactor? -> nothing to do
77
+ if reactor.__class__.__module__ == "twisted.internet.asyncioreactor":
78
+ return
79
+
80
+ # Try upgrade to asyncio-reactor (will raise if another reactor in use)
81
+ asyncioreactor.install(asyncio.get_event_loop()) # type: ignore[arg-type]
82
+ return
83
+ except Exception:
84
+ # Could not swap reactors (already installed). Check if we need to start it.
85
+ # Use a more robust check to avoid race conditions
86
+ if not reactor.running and not getattr(reactor, "_started", False):
87
+
88
+ def _safe_reactor_run():
89
+ try:
90
+ reactor.run(installSignalHandlers=False)
91
+ except Exception:
92
+ # Reactor already running (race condition) - silently ignore
93
+ pass
94
+
95
+ threading.Thread(
96
+ target=_safe_reactor_run,
97
+ daemon=True,
98
+ ).start()
99
+ # Give reactor a moment to start
100
+
101
+ time.sleep(0.01)
102
+
103
+
104
+ def patch_treq(domains_to_not_propagate_headers_to: Optional[List[str]] = None):
105
+ try:
106
+ # Ensure a live reactor *before* importing treq
107
+ _ensure_reactor_running()
108
+
109
+ import treq
110
+ except ImportError:
111
+ return # treq is not installed; nothing to patch
112
+
113
+ exclude = domains_to_not_propagate_headers_to or []
114
+ preload_active = _tee_preload_active()
115
+
116
+ # Initialize C extension for ultra-fast header checking (if available)
117
+ if preload_active:
118
+ init_fast_header_check(exclude)
119
+
120
+ orig_request = treq.request
121
+
122
+ # ------------------------------------------------------------------ #
123
+ if preload_active:
124
+ # ========== ULTRA-FAST PATH: When LD_PRELOAD is active ==========
125
+ if HAS_WRAPT:
126
+
127
+ def instrumented_request(wrapped, instance, args, kwargs):
128
+ """Ultra-fast header injection using inject_headers_ultrafast() via wrapt."""
129
+ # args = (method, url, ...), kwargs = {...}
130
+ url = args[1] if len(args) > 1 else kwargs.get("url", "")
131
+
132
+ # Get or create headers dict
133
+ hdrs = dict(kwargs.pop("headers", {}) or {})
134
+
135
+ # ULTRA-FAST: inject_headers_ultrafast does domain filtering + header injection (~100ns)
136
+ inject_headers_ultrafast(hdrs, url, exclude)
137
+
138
+ kwargs["headers"] = hdrs
139
+
140
+ # Immediately call original and return - NO timing, NO capture!
141
+ return wrapped(*args, **kwargs)
142
+
143
+ wrapt.wrap_function_wrapper("treq", "request", instrumented_request)
144
+ else:
145
+ # Fallback: Direct patching if wrapt not available
146
+ def patched_request_fast(method: str, url: str, **kwargs):
147
+ """Ultra-fast header injection without wrapt."""
148
+ # Get or create headers dict
149
+ hdrs = dict(kwargs.pop("headers", {}) or {})
150
+
151
+ # ULTRA-FAST: inject_headers_ultrafast does domain filtering + header injection (~100ns)
152
+ inject_headers_ultrafast(hdrs, url, exclude)
153
+
154
+ kwargs["headers"] = hdrs
155
+
156
+ # Immediately call original and return - NO timing, NO capture!
157
+ return orig_request(method, url, **kwargs)
158
+
159
+ treq.request = patched_request_fast # type: ignore[assignment]
160
+ else:
161
+ # ========== FULL CAPTURE PATH: When LD_PRELOAD is NOT active ==========
162
+ def patched_request(method: str, url: str, **kwargs):
163
+ # -------- header injection
164
+ hdrs = dict(kwargs.pop("headers", {}) or {})
165
+
166
+ # ULTRA-FAST: inject_headers_ultrafast does domain filtering + header injection (~100ns)
167
+ inject_headers_ultrafast(hdrs, url, exclude)
168
+
169
+ kwargs["headers"] = hdrs
170
+
171
+ # Get trace_id for capture
172
+ trace_id = trace_id_ctx.get(None) or ""
173
+
174
+ # Capture request data
175
+ req_data = b""
176
+ req_headers = b""
177
+ try:
178
+ if "json" in kwargs:
179
+ if HAS_ORJSON:
180
+ req_data = orjson.dumps(kwargs["json"])
181
+ else:
182
+ req_data = json.dumps(kwargs["json"]).encode("utf-8")
183
+ elif "data" in kwargs:
184
+ data = kwargs["data"]
185
+ if isinstance(data, bytes):
186
+ req_data = data
187
+ elif isinstance(data, str):
188
+ req_data = data.encode("utf-8")
189
+
190
+ # Capture request headers
191
+ if HAS_ORJSON:
192
+ req_headers = orjson.dumps({str(k): str(v) for k, v in hdrs.items()})
193
+ else:
194
+ req_headers = json.dumps({str(k): str(v) for k, v in hdrs.items()}).encode("utf-8")
195
+ except Exception: # noqa: BLE001
196
+ pass
197
+
198
+ t0 = int(time.time() * 1_000)
199
+ d = orig_request(method, url, **kwargs) # Deferred
200
+
201
+ # -------- record on success
202
+ def _ok(resp):
203
+ status = getattr(resp, "code", 0)
204
+ resp_headers = b""
205
+
206
+ # Capture response headers
207
+ try:
208
+ if HAS_ORJSON:
209
+ resp_headers = orjson.dumps(
210
+ dict(resp.headers.getAllRawHeaders())
211
+ )
212
+ else:
213
+ resp_headers = json.dumps(
214
+ dict(resp.headers.getAllRawHeaders())
215
+ ).encode("utf-8")
216
+ except Exception: # noqa: BLE001
217
+ pass
218
+
219
+ # For treq, capturing response body is complex (async via Deferred)
220
+ # We'll record without body to keep it simple and non-blocking
221
+ # Only capture if LD_PRELOAD is NOT active (avoid duplicates)
222
+ if not preload_active:
223
+ record_network_request(
224
+ trace_id,
225
+ url,
226
+ method.upper(),
227
+ status,
228
+ status < 400,
229
+ None,
230
+ timestamp_start=t0,
231
+ timestamp_end=int(time.time() * 1_000),
232
+ request_data=req_data,
233
+ request_headers=req_headers,
234
+ response_headers=resp_headers,
235
+ )
236
+ return resp
237
+
238
+ # -------- record on failure
239
+ def _err(f):
240
+ # Only capture if LD_PRELOAD is NOT active (avoid duplicates)
241
+ if not preload_active:
242
+ record_network_request(
243
+ trace_id,
244
+ url,
245
+ method.upper(),
246
+ 0,
247
+ False,
248
+ str(f.value)[:255],
249
+ timestamp_start=t0,
250
+ timestamp_end=int(time.time() * 1_000),
251
+ request_data=req_data,
252
+ request_headers=req_headers,
253
+ )
254
+ return f
255
+
256
+ d.addCallbacks(_ok, _err)
257
+ return d
258
+
259
+ treq.request = patched_request # type: ignore[assignment]
260
+
261
+ # Convenience verbs → reuse patched_request
262
+ def _verb_factory(v: str):
263
+ def _verb(url, **k):
264
+ return treq.request(v.upper(), url, **k)
265
+
266
+ _verb.__name__ = v
267
+ return _verb
268
+
269
+ for verb in verbs:
270
+ setattr(treq, verb, _verb_factory(verb))