sf-veritas 0.10.3__cp310-cp310-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.
- sf_veritas/__init__.py +20 -0
- sf_veritas/_sffastlog.c +889 -0
- sf_veritas/_sffastlog.cpython-310-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnet.c +924 -0
- sf_veritas/_sffastnet.cpython-310-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnetworkrequest.c +730 -0
- sf_veritas/_sffastnetworkrequest.cpython-310-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan.c +2155 -0
- sf_veritas/_sffuncspan.cpython-310-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan_config.c +617 -0
- sf_veritas/_sffuncspan_config.cpython-310-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfheadercheck.c +341 -0
- sf_veritas/_sfheadercheck.cpython-310-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfnetworkhop.c +1451 -0
- sf_veritas/_sfnetworkhop.cpython-310-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfservice.c +1175 -0
- sf_veritas/_sfservice.cpython-310-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfteepreload.c +5167 -0
- sf_veritas/app_config.py +49 -0
- sf_veritas/cli.py +336 -0
- sf_veritas/constants.py +10 -0
- sf_veritas/custom_excepthook.py +304 -0
- sf_veritas/custom_log_handler.py +129 -0
- sf_veritas/custom_output_wrapper.py +144 -0
- sf_veritas/custom_print.py +146 -0
- sf_veritas/django_app.py +5 -0
- sf_veritas/env_vars.py +186 -0
- sf_veritas/exception_handling_middleware.py +18 -0
- sf_veritas/exception_metaclass.py +69 -0
- sf_veritas/fast_frame_info.py +116 -0
- sf_veritas/fast_network_hop.py +293 -0
- sf_veritas/frame_tools.py +112 -0
- sf_veritas/funcspan_config_loader.py +556 -0
- sf_veritas/function_span_profiler.py +1174 -0
- sf_veritas/import_hook.py +62 -0
- sf_veritas/infra_details/__init__.py +3 -0
- sf_veritas/infra_details/get_infra_details.py +24 -0
- sf_veritas/infra_details/kubernetes/__init__.py +3 -0
- sf_veritas/infra_details/kubernetes/get_cluster_name.py +147 -0
- sf_veritas/infra_details/kubernetes/get_details.py +7 -0
- sf_veritas/infra_details/running_on/__init__.py +17 -0
- sf_veritas/infra_details/running_on/kubernetes.py +11 -0
- sf_veritas/interceptors.py +497 -0
- sf_veritas/libsfnettee.so +0 -0
- sf_veritas/local_env_detect.py +118 -0
- sf_veritas/package_metadata.py +6 -0
- sf_veritas/patches/__init__.py +0 -0
- sf_veritas/patches/concurrent_futures.py +19 -0
- sf_veritas/patches/constants.py +1 -0
- sf_veritas/patches/exceptions.py +82 -0
- sf_veritas/patches/multiprocessing.py +32 -0
- sf_veritas/patches/network_libraries/__init__.py +76 -0
- sf_veritas/patches/network_libraries/aiohttp.py +281 -0
- sf_veritas/patches/network_libraries/curl_cffi.py +363 -0
- sf_veritas/patches/network_libraries/http_client.py +419 -0
- sf_veritas/patches/network_libraries/httpcore.py +515 -0
- sf_veritas/patches/network_libraries/httplib2.py +204 -0
- sf_veritas/patches/network_libraries/httpx.py +515 -0
- sf_veritas/patches/network_libraries/niquests.py +211 -0
- sf_veritas/patches/network_libraries/pycurl.py +385 -0
- sf_veritas/patches/network_libraries/requests.py +633 -0
- sf_veritas/patches/network_libraries/tornado.py +341 -0
- sf_veritas/patches/network_libraries/treq.py +270 -0
- sf_veritas/patches/network_libraries/urllib_request.py +468 -0
- sf_veritas/patches/network_libraries/utils.py +398 -0
- sf_veritas/patches/os.py +17 -0
- sf_veritas/patches/threading.py +218 -0
- sf_veritas/patches/web_frameworks/__init__.py +54 -0
- sf_veritas/patches/web_frameworks/aiohttp.py +793 -0
- sf_veritas/patches/web_frameworks/async_websocket_consumer.py +317 -0
- sf_veritas/patches/web_frameworks/blacksheep.py +527 -0
- sf_veritas/patches/web_frameworks/bottle.py +502 -0
- sf_veritas/patches/web_frameworks/cherrypy.py +678 -0
- sf_veritas/patches/web_frameworks/cors_utils.py +122 -0
- sf_veritas/patches/web_frameworks/django.py +944 -0
- sf_veritas/patches/web_frameworks/eve.py +395 -0
- sf_veritas/patches/web_frameworks/falcon.py +926 -0
- sf_veritas/patches/web_frameworks/fastapi.py +724 -0
- sf_veritas/patches/web_frameworks/flask.py +520 -0
- sf_veritas/patches/web_frameworks/klein.py +501 -0
- sf_veritas/patches/web_frameworks/litestar.py +551 -0
- sf_veritas/patches/web_frameworks/pyramid.py +428 -0
- sf_veritas/patches/web_frameworks/quart.py +824 -0
- sf_veritas/patches/web_frameworks/robyn.py +697 -0
- sf_veritas/patches/web_frameworks/sanic.py +857 -0
- sf_veritas/patches/web_frameworks/starlette.py +723 -0
- sf_veritas/patches/web_frameworks/strawberry.py +813 -0
- sf_veritas/patches/web_frameworks/tornado.py +481 -0
- sf_veritas/patches/web_frameworks/utils.py +91 -0
- sf_veritas/print_override.py +13 -0
- sf_veritas/regular_data_transmitter.py +409 -0
- sf_veritas/request_interceptor.py +401 -0
- sf_veritas/request_utils.py +550 -0
- sf_veritas/server_status.py +1 -0
- sf_veritas/shutdown_flag.py +11 -0
- sf_veritas/subprocess_startup.py +3 -0
- sf_veritas/test_cli.py +145 -0
- sf_veritas/thread_local.py +970 -0
- sf_veritas/timeutil.py +114 -0
- sf_veritas/transmit_exception_to_sailfish.py +28 -0
- sf_veritas/transmitter.py +132 -0
- sf_veritas/types.py +47 -0
- sf_veritas/unified_interceptor.py +1580 -0
- sf_veritas/utils.py +39 -0
- sf_veritas-0.10.3.dist-info/METADATA +97 -0
- sf_veritas-0.10.3.dist-info/RECORD +132 -0
- sf_veritas-0.10.3.dist-info/WHEEL +5 -0
- sf_veritas-0.10.3.dist-info/entry_points.txt +2 -0
- sf_veritas-0.10.3.dist-info/top_level.txt +1 -0
- sf_veritas.libs/libbrotlicommon-6ce2a53c.so.1.0.6 +0 -0
- sf_veritas.libs/libbrotlidec-811d1be3.so.1.0.6 +0 -0
- sf_veritas.libs/libcom_err-730ca923.so.2.1 +0 -0
- sf_veritas.libs/libcrypt-52aca757.so.1.1.0 +0 -0
- sf_veritas.libs/libcrypto-bdaed0ea.so.1.1.1k +0 -0
- sf_veritas.libs/libcurl-eaa3cf66.so.4.5.0 +0 -0
- sf_veritas.libs/libgssapi_krb5-323bbd21.so.2.2 +0 -0
- sf_veritas.libs/libidn2-2f4a5893.so.0.3.6 +0 -0
- sf_veritas.libs/libk5crypto-9a74ff38.so.3.1 +0 -0
- sf_veritas.libs/libkeyutils-2777d33d.so.1.6 +0 -0
- sf_veritas.libs/libkrb5-a55300e8.so.3.3 +0 -0
- sf_veritas.libs/libkrb5support-e6594cfc.so.0.1 +0 -0
- sf_veritas.libs/liblber-2-d20824ef.4.so.2.10.9 +0 -0
- sf_veritas.libs/libldap-2-cea2a960.4.so.2.10.9 +0 -0
- sf_veritas.libs/libnghttp2-39367a22.so.14.17.0 +0 -0
- sf_veritas.libs/libpcre2-8-516f4c9d.so.0.7.1 +0 -0
- sf_veritas.libs/libpsl-99becdd3.so.5.3.1 +0 -0
- sf_veritas.libs/libsasl2-7de4d792.so.3.0.0 +0 -0
- sf_veritas.libs/libselinux-d0805dcb.so.1 +0 -0
- sf_veritas.libs/libssh-c11d285b.so.4.8.7 +0 -0
- sf_veritas.libs/libssl-60250281.so.1.1.1k +0 -0
- sf_veritas.libs/libunistring-05abdd40.so.2.1.0 +0 -0
- 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
|
+
)
|
sf_veritas/patches/os.py
ADDED
|
@@ -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"]
|