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,398 @@
1
+ """
2
+ Shared helpers used by all network-patch modules.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ import time
9
+ import threading
10
+ from functools import lru_cache
11
+ from typing import List, Tuple, Optional
12
+ from urllib.parse import urlparse
13
+
14
+ from ... import app_config
15
+ from ...constants import SAILFISH_TRACING_HEADER, FUNCSPAN_OVERRIDE_HEADER
16
+ from ...env_vars import SF_DEBUG
17
+ from ...regular_data_transmitter import NetworkRequestTransmitter
18
+ from ...thread_local import (
19
+ get_or_set_sf_trace_id,
20
+ is_network_recording_suppressed,
21
+ trace_id_ctx,
22
+ funcspan_override_ctx,
23
+ get_outbound_headers_with_new_uuid,
24
+ get_funcspan_override,
25
+ )
26
+
27
+ # Try to import the C extension for ultra-fast network request recording
28
+ try:
29
+ from ... import _sffastnet
30
+ _FAST_NET_AVAILABLE = True
31
+ except ImportError:
32
+ _FAST_NET_AVAILABLE = False
33
+ _sffastnet = None
34
+
35
+ # Try to import the C extension for http.client patching (captures headers/bodies)
36
+ try:
37
+ from ... import _sffastnetworkrequest
38
+ _FAST_NETWORKREQUEST_AVAILABLE = True
39
+ except ImportError:
40
+ _FAST_NETWORKREQUEST_AVAILABLE = False
41
+ _sffastnetworkrequest = None
42
+
43
+ # Try to import the C extension for ultra-fast header checking (domain filtering)
44
+ try:
45
+ from ... import _sfheadercheck
46
+ _HAS_FAST_HEADER_CHECK = True
47
+ except ImportError:
48
+ _HAS_FAST_HEADER_CHECK = False
49
+ _sfheadercheck = None
50
+
51
+ _FAST_NET_INITIALIZED = False
52
+ _FAST_NETWORKREQUEST_INITIALIZED = False
53
+
54
+ # GraphQL mutation for network requests
55
+ _COLLECT_NETWORK_REQUEST_MUTATION = """
56
+ mutation collectNetworkRequest($data: NetworkRequestInput!) {
57
+ collectNetworkRequest(data: $data)
58
+ }
59
+ """
60
+
61
+ def init_fast_networkrequest_tracking():
62
+ """Initialize the C extension for http.client network request tracking (with body/header capture)."""
63
+ global _FAST_NETWORKREQUEST_INITIALIZED
64
+ if not _FAST_NETWORKREQUEST_AVAILABLE or _FAST_NETWORKREQUEST_INITIALIZED or not _sffastnetworkrequest:
65
+ return False
66
+
67
+ try:
68
+ http2 = 1 if os.getenv("SF_NBPOST_HTTP2", "0") == "1" else 0
69
+ ok = _sffastnetworkrequest.init_networkhop(
70
+ url=app_config._sailfish_graphql_endpoint,
71
+ query=_COLLECT_NETWORK_REQUEST_MUTATION,
72
+ api_key=app_config._sailfish_api_key,
73
+ service_uuid=app_config._service_uuid or "",
74
+ library=getattr(app_config, "library", "sf-veritas"),
75
+ version=getattr(app_config, "version", "0.0.0"),
76
+ http2=http2,
77
+ )
78
+ if ok:
79
+ _FAST_NETWORKREQUEST_INITIALIZED = True
80
+ if SF_DEBUG:
81
+ print("[_sffastnetworkrequest] initialized (libcurl sender with body/header capture)", log=False)
82
+ return True
83
+ except Exception as e:
84
+ if SF_DEBUG:
85
+ print(f"[_sffastnetworkrequest] init failed; falling back: {e}", log=False)
86
+
87
+ return False
88
+
89
+ def init_fast_network_tracking():
90
+ """Initialize the C extensions for network request tracking (both _sffastnet and _sffastnetworkrequest)."""
91
+ global _FAST_NET_INITIALIZED
92
+
93
+ # Initialize _sffastnet (generic network requests)
94
+ net_ok = False
95
+ if _FAST_NET_AVAILABLE and not _FAST_NET_INITIALIZED and _sffastnet:
96
+ try:
97
+ http2 = 1 if os.getenv("SF_NBPOST_HTTP2", "0") == "1" else 0
98
+ ok = _sffastnet.init(
99
+ url=app_config._sailfish_graphql_endpoint,
100
+ query=_COLLECT_NETWORK_REQUEST_MUTATION,
101
+ api_key=app_config._sailfish_api_key,
102
+ http2=http2,
103
+ )
104
+ if ok:
105
+ _FAST_NET_INITIALIZED = True
106
+ net_ok = True
107
+ if SF_DEBUG:
108
+ print("[_sffastnet] initialized (libcurl sender)", log=False)
109
+ except Exception as e:
110
+ if SF_DEBUG:
111
+ print(f"[_sffastnet] init failed; falling back: {e}", log=False)
112
+
113
+ # Initialize _sffastnetworkrequest (http.client with body/header capture)
114
+ netreq_ok = init_fast_networkrequest_tracking()
115
+
116
+ return net_ok or netreq_ok
117
+
118
+
119
+ ###############################################################################
120
+ # ULTRA-FAST Header Injection (<100ns target)
121
+ ###############################################################################
122
+
123
+ # Thread-local cache for ultra-fast header injection
124
+ _thread_local = threading.local()
125
+
126
+ # [DIAGNOSTICS] Global counters for tracking request success/failure
127
+ _request_attempt_counter = 0
128
+ _request_success_counter = 0
129
+ _request_failure_counter = 0
130
+ _counter_lock = threading.Lock()
131
+
132
+ def get_request_stats() -> dict:
133
+ """Get diagnostic statistics for request tracking."""
134
+ return {
135
+ "attempts": _request_attempt_counter,
136
+ "success": _request_success_counter,
137
+ "failures": _request_failure_counter,
138
+ "deficit": _request_attempt_counter - _request_success_counter - _request_failure_counter,
139
+ }
140
+
141
+ def print_request_stats() -> None:
142
+ """Print diagnostic statistics for request tracking."""
143
+ stats = get_request_stats()
144
+ print(f"\n[REQUEST_STATS] attempts={stats['attempts']} success={stats['success']} failures={stats['failures']} deficit={stats['deficit']}", log=False)
145
+ if stats["deficit"] > 0:
146
+ print(f"[REQUEST_STATS] ⚠️ WARNING: {stats['deficit']} requests neither succeeded nor failed - possible bug!", log=False)
147
+ if stats["failures"] > 0:
148
+ print(f"[REQUEST_STATS] ⚠️ WARNING: {stats['failures']} requests failed - check error logs above", log=False)
149
+
150
+ def track_request_result(success: bool, error: Optional[Exception] = None, url: str = "") -> None:
151
+ """Track whether a request succeeded or failed (only when SF_DEBUG is enabled)."""
152
+ if not SF_DEBUG:
153
+ return # Skip tracking entirely to avoid lock contention in production
154
+
155
+ global _request_success_counter, _request_failure_counter
156
+ with _counter_lock:
157
+ if success:
158
+ _request_success_counter += 1
159
+ else:
160
+ _request_failure_counter += 1
161
+ error_type = type(error).__name__ if error else "Unknown"
162
+ error_msg = str(error) if error else "Unknown error"
163
+ print(f"[track_request_result] ❌ REQUEST FAILED: {error_type}: {error_msg} (url={url})", log=False)
164
+
165
+ def inject_headers_ultrafast(headers_dict: dict, url: str, domains_to_skip: List[str]) -> None:
166
+ """
167
+ ULTRA-FAST header injection (~100ns average).
168
+
169
+ Injects X-Sf3-Rid and X-Sf3-FunctionSpanCaptureOverride headers directly into dict.
170
+ Uses pre-built headers from OutboundHeaderManager with background UUID4 generation.
171
+
172
+ Performance:
173
+ - Filtered domain: ~30ns (domain check only)
174
+ - Fast path (pre-generated): ~100ns (domain check + header injection)
175
+ - Slow path (generate on-demand): ~500ns (domain check + synchronous UUID4)
176
+
177
+ Args:
178
+ headers_dict: Dictionary to inject headers into (mutated in-place)
179
+ url: Destination URL for domain filtering
180
+ domains_to_skip: List of domains to skip header propagation
181
+ """
182
+ # [DIAGNOSTICS] Count request attempts (only when SF_DEBUG is enabled to avoid lock contention)
183
+ if SF_DEBUG:
184
+ global _request_attempt_counter
185
+ with _counter_lock:
186
+ _request_attempt_counter += 1
187
+ attempt_id = _request_attempt_counter
188
+ print(f"[inject_headers_ultrafast] 🚀 CALLED #{attempt_id} with url={url}, domains_to_skip={domains_to_skip}", log=False)
189
+
190
+ # FAST: Domain filtering check (LRU cached, ~20ns)
191
+ if domains_to_skip:
192
+ domain = extract_domain(url)
193
+ if domain in domains_to_skip:
194
+ if SF_DEBUG:
195
+ print(f"[inject_headers_ultrafast] ⛔ Skipped (domain filtered)", log=False)
196
+ return
197
+
198
+ # ULTRA-FAST: Get pre-built header with new UUID (~10-20ns with LD_PRELOAD)
199
+ # Measure ONLY the critical path (excluding debug prints)
200
+ if SF_DEBUG:
201
+ start_ns = time.perf_counter_ns()
202
+
203
+ outbound_headers = get_outbound_headers_with_new_uuid()
204
+
205
+ if SF_DEBUG:
206
+ get_headers_ns = time.perf_counter_ns() - start_ns
207
+
208
+ if SF_DEBUG:
209
+ print(f"[inject_headers_ultrafast] 📦 get_outbound_headers_with_new_uuid() returned: {outbound_headers} (took {get_headers_ns}ns)", log=False)
210
+
211
+ if outbound_headers:
212
+ # FAST: Dict update (~30ns)
213
+ if SF_DEBUG:
214
+ start_update_ns = time.perf_counter_ns()
215
+
216
+ headers_dict.update(outbound_headers)
217
+
218
+ if SF_DEBUG:
219
+ update_ns = time.perf_counter_ns() - start_update_ns
220
+ total_ns = get_headers_ns + update_ns
221
+ # Pre-generated = ContextVar lookup (10-50ns) + dict.get() (10-50ns) + dict.update() (20-50ns) = ~50-150ns typical
222
+ # Allow up to 1μs for variance (CPU scheduler, context switches, etc.)
223
+ status = "pre-generated" if total_ns < 1000 else "generated on-demand"
224
+ print(f"[inject_headers_ultrafast] ✅ Updated headers_dict. get={get_headers_ns}ns, update={update_ns}ns, total={total_ns}ns ({status})", log=False)
225
+ else:
226
+ if SF_DEBUG:
227
+ print(f"[inject_headers_ultrafast] ⚠️ No outbound headers returned (empty dict)", log=False)
228
+
229
+
230
+ ###############################################################################
231
+ # Domain-parsing utility (no external network / no tldextract needed)
232
+ ###############################################################################
233
+ @lru_cache(maxsize=256)
234
+ def extract_domain(url: str) -> str:
235
+ """
236
+ Return a canonical host name for header-propagation checks.
237
+
238
+ • Works entirely offline (std-lib only) – no remote download or file locks.
239
+ • Keeps sub-domains intact, just strips a leading “www.” and port numbers.
240
+
241
+ Examples
242
+ --------
243
+ >>> extract_domain("https://www.example.com:443/path")
244
+ 'example.com'
245
+ >>> extract_domain("https://api.foo.bar.example.co.uk/v1")
246
+ 'api.foo.bar.example.co.uk'
247
+ """
248
+ try:
249
+ host = urlparse(url).hostname or url
250
+ except Exception:
251
+ host = url # fall back to raw string on malformed URLs
252
+ if host.startswith("www."):
253
+ host = host[4:]
254
+ return host.lower()
255
+
256
+
257
+ ###############################################################################
258
+ # Header-propagation + network-recording helpers
259
+ ###############################################################################
260
+ def get_trace_and_should_propagate(
261
+ url: str,
262
+ domains_to_not_propagate: List[str],
263
+ ) -> Tuple[str, bool]:
264
+ """
265
+ Returns (trace_id, should_propagate?) for the given destination `url`.
266
+ """
267
+ _, trace_id = get_or_set_sf_trace_id()
268
+ domain = extract_domain(url)
269
+ allow_header = domain not in domains_to_not_propagate
270
+ return trace_id, allow_header
271
+
272
+
273
+ def init_fast_header_check(domains_to_not_propagate: List[str]) -> bool:
274
+ """
275
+ Initialize the C extension for ultra-fast header checking with skip list.
276
+
277
+ Should be called once at patch time to set up the domain filtering list.
278
+
279
+ Returns: True if C extension initialized successfully, False otherwise.
280
+ """
281
+ if _HAS_FAST_HEADER_CHECK and _sfheadercheck:
282
+ try:
283
+ _sfheadercheck.init_header_check(domains_to_not_propagate)
284
+ return True
285
+ except Exception:
286
+ return False
287
+ return False
288
+
289
+
290
+ def get_trace_and_should_propagate_fast(
291
+ url: str,
292
+ domains_to_not_propagate: List[str],
293
+ ) -> Tuple[str, bool, Optional[str]]:
294
+ """
295
+ Ultra-fast path using C extension for domain filtering.
296
+
297
+ Returns: (trace_id, should_propagate, funcspan_override)
298
+
299
+ Performance:
300
+ - Empty skip list: ~15ns (ContextVar reads only)
301
+ - With skip list: ~25ns (C domain parse + hash lookup + ContextVars)
302
+ - 10x faster than Python implementation (50-100ns)
303
+
304
+ Falls back to Python implementation if C extension not available.
305
+ """
306
+ if _HAS_FAST_HEADER_CHECK and _sfheadercheck:
307
+ # C extension handles domain filtering + ContextVar reads
308
+ # Returns: (should_inject: bool, trace_id: str, funcspan_override: str | None)
309
+ try:
310
+ should_inject, trace_id, funcspan_override = _sfheadercheck.should_inject_headers(url)
311
+ return trace_id, should_inject, funcspan_override
312
+ except Exception:
313
+ # Fall back to Python on any error
314
+ pass
315
+
316
+ # Fallback to Python implementation
317
+ trace_id, allow = get_trace_and_should_propagate(url, domains_to_not_propagate)
318
+ funcspan_override = get_funcspan_override()
319
+ return trace_id, allow, funcspan_override
320
+
321
+
322
+ def record_network_request(
323
+ trace_id: str,
324
+ url: str,
325
+ method: str,
326
+ status_code: int,
327
+ success: bool,
328
+ error: str | None = None,
329
+ timestamp_start: int | None = None,
330
+ timestamp_end: int | None = None,
331
+ request_data: bytes = b"",
332
+ response_data: bytes = b"",
333
+ request_headers: bytes = b"",
334
+ response_headers: bytes = b"",
335
+ ) -> None:
336
+ """
337
+ Fire off a GraphQL NetworkRequest mutation via C extension (fast path)
338
+ or NetworkRequestTransmitter (fallback).
339
+ Handles tripartite trace-ID splitting and default timestamps.
340
+ NEW: Supports request_data and response_data capture.
341
+ """
342
+ if is_network_recording_suppressed():
343
+ return
344
+
345
+ session_id, page_visit_id, request_id = None, None, None
346
+ parts = trace_id.split("/")
347
+ if parts:
348
+ session_id = parts[0]
349
+ if len(parts) > 1:
350
+ page_visit_id = parts[1]
351
+ if len(parts) > 2:
352
+ request_id = parts[2]
353
+
354
+ now_ms = lambda: int(time.time() * 1_000) # noqa: E731
355
+ ts0 = timestamp_start or now_ms()
356
+ ts1 = timestamp_end or now_ms()
357
+
358
+ # Use C fast path if available (positional args for maximum speed)
359
+ if _FAST_NET_AVAILABLE and _sffastnet and _FAST_NET_INITIALIZED:
360
+ # Pass bytes directly for zero-copy access
361
+ # Order: request_id, page_visit_id, recording_session_id, service_uuid,
362
+ # timestamp_start, timestamp_end, response_code, success,
363
+ # error, url, method, request_data(bytes), response_data(bytes),
364
+ # request_headers(bytes), response_headers(bytes)
365
+ _sffastnet.network_request(
366
+ request_id or "",
367
+ page_visit_id or "",
368
+ session_id or "",
369
+ app_config._service_uuid or "",
370
+ ts0,
371
+ ts1,
372
+ status_code,
373
+ success,
374
+ None if success else ((error or "")[:255]),
375
+ url or "",
376
+ method.upper(),
377
+ request_data if isinstance(request_data, bytes) else b"",
378
+ response_data if isinstance(response_data, bytes) else b"",
379
+ request_headers if isinstance(request_headers, bytes) else b"",
380
+ response_headers if isinstance(response_headers, bytes) else b"",
381
+ )
382
+ else:
383
+ # Fallback to Python implementation (without request/response data for now)
384
+ NetworkRequestTransmitter().do_send(
385
+ (
386
+ request_id,
387
+ page_visit_id,
388
+ session_id,
389
+ None, # service_uuid (set by transmitter middleware)
390
+ ts0,
391
+ ts1,
392
+ status_code,
393
+ success,
394
+ None if success else (error or "")[:255],
395
+ url,
396
+ method.upper(),
397
+ )
398
+ )
@@ -0,0 +1,17 @@
1
+ import os
2
+
3
+ from ..thread_local import get_context, set_context
4
+
5
+ _original_fork = os.fork
6
+
7
+
8
+ def patched_fork():
9
+ current_context = get_context()
10
+ pid = _original_fork()
11
+ if pid == 0: # Child process
12
+ set_context(current_context)
13
+ return pid
14
+
15
+
16
+ def patch_os():
17
+ os.fork = patched_fork
@@ -0,0 +1,218 @@
1
+ import contextvars
2
+ import multiprocessing
3
+ import os
4
+ import threading
5
+ from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
6
+
7
+ from .. import app_config
8
+ from ..thread_local import (
9
+ get_context,
10
+ set_context,
11
+ is_ld_preload_active,
12
+ outbound_header_base_ctx,
13
+ _get_shared_outbound_header_base,
14
+ )
15
+ from ..env_vars import SF_DEBUG
16
+
17
+ _original_thread_init = threading.Thread.__init__
18
+ _original_process_init = multiprocessing.Process.__init__
19
+ _original_executor_submit = ThreadPoolExecutor.submit
20
+ _original_process_submit = ProcessPoolExecutor.submit
21
+
22
+ # Cache LD_PRELOAD status at module level (checked once, not per-thread)
23
+ _LD_PRELOAD_MODE = is_ld_preload_active()
24
+
25
+ # Cache SF_DEBUG flag at module load to avoid repeated checks (set during initialization)
26
+ _SF_DEBUG_ENABLED = False
27
+
28
+ # PERFORMANCE: Allow disabling thread patching entirely for benchmarks
29
+ # Set SF_DISABLE_THREAD_PATCHING=1 to skip all thread wrapping overhead
30
+ _THREAD_PATCHING_DISABLED = os.getenv("SF_DISABLE_THREAD_PATCHING") == "1"
31
+
32
+
33
+ def patched_thread_init(self, *args, **kwargs):
34
+ # PERFORMANCE: Skip context propagation when LD_PRELOAD is active
35
+ # LD_PRELOAD mode only needs outbound headers (handled by ContextVar in ThreadPoolExecutor.submit)
36
+ # Background library threads (urllib3, httpcore, etc.) don't need full context
37
+
38
+ # ULTRA-FAST PATH: Check all bypass conditions in one compound expression
39
+ # Eliminates wrapper overhead for:
40
+ # 1. SF_DISABLE_THREAD_PATCHING=1 (benchmark mode)
41
+ # 2. LD_PRELOAD mode (context handled by ContextVar)
42
+ # 3. Daemon threads (connection pools, background workers)
43
+ if _THREAD_PATCHING_DISABLED or _LD_PRELOAD_MODE or kwargs.get("daemon", False):
44
+ # Direct call without any wrapper overhead
45
+ return _original_thread_init(self, *args, **kwargs)
46
+
47
+ # Slow path: Full context propagation for Python-only mode
48
+ current_context = get_context()
49
+
50
+ original_target = kwargs.get("target")
51
+ if original_target:
52
+
53
+ def wrapped_target(*targs, **tkwargs):
54
+ set_context(current_context)
55
+ original_target(*targs, **tkwargs)
56
+
57
+ kwargs["target"] = wrapped_target
58
+ elif args and callable(args[0]):
59
+ original_target = args[0]
60
+
61
+ def wrapped_target(*targs, **tkwargs):
62
+ set_context(current_context)
63
+ original_target(*targs, **tkwargs)
64
+
65
+ args = (wrapped_target,) + args[1:]
66
+
67
+ _original_thread_init(self, *args, **kwargs)
68
+
69
+
70
+ def patched_process_init(self, *args, **kwargs):
71
+ """
72
+ Patch multiprocessing.Process.__init__() to serialize and pass context data to child processes.
73
+
74
+ Similar to Thread patching, but we serialize the outbound header base dict
75
+ since ContextVars cannot cross process boundaries (separate memory space).
76
+
77
+ Performance:
78
+ - Serialize overhead: ~100-500ns per process creation
79
+ - IPC overhead: ~10-100μs (inter-process communication)
80
+ - ContextVar reads in child: ~10-20ns each (NO LOCK)
81
+ """
82
+ # Get current outbound header base (try ContextVar first, then shared registry)
83
+ base_dict = outbound_header_base_ctx.get()
84
+ if not base_dict:
85
+ base_dict = _get_shared_outbound_header_base()
86
+
87
+ if _SF_DEBUG_ENABLED:
88
+ print(f"[Process.__init__] 📦 Serializing context for child process: {base_dict}", log=False)
89
+
90
+ original_target = kwargs.get("target")
91
+ if original_target:
92
+ def wrapped_target(*targs, **tkwargs):
93
+ # Restore outbound header base in child process's ContextVar
94
+ if base_dict:
95
+ try:
96
+ from sf_veritas.thread_local import outbound_header_base_ctx as child_ctx
97
+ child_ctx.set(base_dict)
98
+ if SF_DEBUG:
99
+ print(f"[Process child] ✅ Restored context in child process: {base_dict}", log=False)
100
+ except Exception as e:
101
+ if SF_DEBUG:
102
+ print(f"[Process child] ⚠️ Failed to restore context: {e}", log=False)
103
+
104
+ # Run original target
105
+ original_target(*targs, **tkwargs)
106
+
107
+ kwargs["target"] = wrapped_target
108
+ elif args and callable(args[0]):
109
+ original_target = args[0]
110
+
111
+ def wrapped_target(*targs, **tkwargs):
112
+ # Restore outbound header base in child process's ContextVar
113
+ if base_dict:
114
+ try:
115
+ from sf_veritas.thread_local import outbound_header_base_ctx as child_ctx
116
+ child_ctx.set(base_dict)
117
+ if SF_DEBUG:
118
+ print(f"[Process child] ✅ Restored context in child process: {base_dict}", log=False)
119
+ except Exception as e:
120
+ if SF_DEBUG:
121
+ print(f"[Process child] ⚠️ Failed to restore context: {e}", log=False)
122
+
123
+ # Run original target
124
+ original_target(*targs, **tkwargs)
125
+
126
+ args = (wrapped_target,) + args[1:]
127
+
128
+ _original_process_init(self, *args, **kwargs)
129
+
130
+
131
+ def patched_executor_submit(self, fn, /, *args, **kwargs):
132
+ """
133
+ Patch ThreadPoolExecutor.submit() to copy ContextVars to worker threads.
134
+
135
+ This ensures outbound_header_base_ctx propagates to worker threads,
136
+ eliminating lock contention on shared registry (~10ns vs 1-6ms!).
137
+
138
+ Performance:
139
+ - Before: ContextVar is None on worker thread → falls back to shared registry (LOCK) → 1-6ms
140
+ - After: ContextVar copied to worker thread → instant access (~10ns) → NO LOCK
141
+ """
142
+ # PERFORMANCE: In LD_PRELOAD mode, we still need ContextVar propagation
143
+ # but it's ultra-fast (~500ns) compared to get_context() (~264μs)
144
+
145
+ # Copy current context (includes all ContextVars)
146
+ ctx = contextvars.copy_context()
147
+
148
+ # DEBUG: Uncomment for troubleshooting (adds ~1-100μs per submit!)
149
+ # if SF_DEBUG:
150
+ # from .. import app_config
151
+ # from ..thread_local import outbound_header_base_ctx
152
+ # if app_config._interceptors_initialized:
153
+ # base_in_ctx = outbound_header_base_ctx.get(None)
154
+ # print(f"[ThreadPoolExecutor.submit] 📋 Copying context to worker thread (ctx has {len(ctx)} vars, outbound_header_base={base_in_ctx})", log=False)
155
+
156
+ # Wrap function to run in copied context (minimal overhead ~500ns)
157
+ def wrapped_fn():
158
+ return ctx.run(fn, *args, **kwargs)
159
+
160
+ # Submit wrapped function to executor
161
+ return _original_executor_submit(self, wrapped_fn)
162
+
163
+
164
+ def patched_process_submit(self, fn, /, *args, **kwargs):
165
+ """
166
+ Patch ProcessPoolExecutor.submit() to serialize and pass context data to child processes.
167
+
168
+ NOTE: ContextVars cannot be copied directly to processes (separate memory space).
169
+ Instead, we serialize the outbound header base dict and restore it in the child.
170
+
171
+ Performance:
172
+ - Serialize + IPC overhead: ~10-100μs per submit (one-time cost)
173
+ - ContextVar reads in child: ~10-20ns each (NO LOCK)
174
+ - Still better than lock contention on every outbound call!
175
+
176
+ Limitations:
177
+ - Only works if child process has sf_veritas imported and initialized
178
+ - Adds ~10-100μs overhead per submit (vs ~500ns for threads)
179
+ """
180
+ # Get current outbound header base (try ContextVar first, then shared registry)
181
+ base_dict = outbound_header_base_ctx.get()
182
+ if not base_dict:
183
+ base_dict = _get_shared_outbound_header_base()
184
+
185
+ if _SF_DEBUG_ENABLED:
186
+ print(f"[ProcessPoolExecutor.submit] 📦 Serializing context for child process: {base_dict}", log=False)
187
+
188
+ # Wrap function to restore context in child process
189
+ def wrapped_fn():
190
+ # Restore outbound header base in child process's ContextVar
191
+ if base_dict:
192
+ try:
193
+ from sf_veritas.thread_local import outbound_header_base_ctx as child_ctx
194
+ child_ctx.set(base_dict)
195
+ if SF_DEBUG:
196
+ print(f"[ProcessPoolExecutor child] ✅ Restored context in child process: {base_dict}", log=False)
197
+ except Exception as e:
198
+ if SF_DEBUG:
199
+ print(f"[ProcessPoolExecutor child] ⚠️ Failed to restore context: {e}", log=False)
200
+
201
+ # Run original function
202
+ return fn(*args, **kwargs)
203
+
204
+ # Submit wrapped function to executor
205
+ return _original_process_submit(self, wrapped_fn)
206
+
207
+
208
+ def patch_threading():
209
+ global _SF_DEBUG_ENABLED
210
+ _SF_DEBUG_ENABLED = SF_DEBUG and app_config._interceptors_initialized
211
+
212
+ threading.Thread.__init__ = patched_thread_init
213
+ multiprocessing.Process.__init__ = patched_process_init
214
+ ThreadPoolExecutor.submit = patched_executor_submit
215
+ ProcessPoolExecutor.submit = patched_process_submit
216
+
217
+ if _SF_DEBUG_ENABLED:
218
+ print("[patch_threading] ✅ Patched Thread.__init__, Process.__init__, ThreadPoolExecutor.submit, and ProcessPoolExecutor.submit for ContextVar propagation", log=False)
@@ -0,0 +1,54 @@
1
+ from typing import List, Optional
2
+
3
+ from .aiohttp import patch_aiohttp
4
+ from .async_websocket_consumer import patch_async_consumer_call
5
+ from .blacksheep import patch_blacksheep
6
+ from .bottle import patch_bottle
7
+ from .cherrypy import patch_cherrypy
8
+ from .django import find_and_modify_output_wrapper, patch_django_middleware
9
+ from .eve import patch_eve
10
+ from .falcon import patch_falcon
11
+ from .fastapi import patch_fastapi
12
+ from .flask import patch_flask
13
+ from .klein import patch_klein
14
+ from .litestar import patch_litestar
15
+ from .pyramid import patch_pyramid
16
+ from .quart import patch_quart
17
+
18
+ # Robyn is NOT SUPPORTED - see ROBYN_LIMITATION.md
19
+ # from .robyn import patch_robyn
20
+ # Sanic is NOT SUPPORTED - see SANIC_LIMITATION.md
21
+ # from .sanic import patch_sanic
22
+ from .starlette import patch_starlette
23
+ from .strawberry import patch_strawberry
24
+ from .tornado import patch_tornado
25
+
26
+
27
+ def patch_web_frameworks(routes_to_skip: Optional[List[str]] = None):
28
+ routes_to_skip = routes_to_skip or []
29
+
30
+ patch_strawberry()
31
+ patch_async_consumer_call()
32
+ find_and_modify_output_wrapper()
33
+ patch_django_middleware(routes_to_skip)
34
+ patch_fastapi(routes_to_skip)
35
+ patch_flask(routes_to_skip)
36
+ patch_falcon(routes_to_skip)
37
+ patch_bottle(routes_to_skip)
38
+ patch_quart(routes_to_skip)
39
+ patch_tornado(routes_to_skip)
40
+ patch_aiohttp(routes_to_skip)
41
+ patch_blacksheep(routes_to_skip)
42
+ patch_cherrypy(routes_to_skip)
43
+ patch_pyramid(routes_to_skip)
44
+ patch_litestar(routes_to_skip)
45
+ patch_klein(routes_to_skip)
46
+ patch_eve(routes_to_skip)
47
+ # Sanic is NOT SUPPORTED - see SANIC_LIMITATION.md
48
+ # patch_sanic(routes_to_skip)
49
+ patch_starlette(routes_to_skip)
50
+ # Robyn is NOT SUPPORTED - see ROBYN_LIMITATION.md
51
+ # patch_robyn(routes_to_skip)
52
+
53
+
54
+ __all__ = ["patch_web_frameworks"]