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.
- sf_veritas/__init__.py +20 -0
- sf_veritas/_sffastlog.c +889 -0
- sf_veritas/_sffastlog.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnet.c +924 -0
- sf_veritas/_sffastnet.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnetworkrequest.c +730 -0
- sf_veritas/_sffastnetworkrequest.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan.c +2155 -0
- sf_veritas/_sffuncspan.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan_config.c +617 -0
- sf_veritas/_sffuncspan_config.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfheadercheck.c +341 -0
- sf_veritas/_sfheadercheck.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfnetworkhop.c +1451 -0
- sf_veritas/_sfnetworkhop.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfservice.c +1175 -0
- sf_veritas/_sfservice.cpython-314-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,857 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OTEL-STYLE PURE ASYNC PATTERN:
|
|
3
|
+
• Call C extension directly AFTER response sent
|
|
4
|
+
• C queues to lock-free ring buffer and returns in ~1µs
|
|
5
|
+
• ASGI event loop returns instantly (doesn't wait)
|
|
6
|
+
• C background thread does ALL work with GIL released
|
|
7
|
+
• This should MATCH OTEL performance (identical pattern)
|
|
8
|
+
|
|
9
|
+
KEY INSIGHT: No Python threads! C extension handles everything.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import gc
|
|
13
|
+
import inspect
|
|
14
|
+
from collections import defaultdict
|
|
15
|
+
from threading import Lock
|
|
16
|
+
from typing import List, Optional
|
|
17
|
+
|
|
18
|
+
from ... import app_config
|
|
19
|
+
from ...constants import (
|
|
20
|
+
FUNCSPAN_OVERRIDE_HEADER_BYTES,
|
|
21
|
+
SAILFISH_TRACING_HEADER,
|
|
22
|
+
SAILFISH_TRACING_HEADER_BYTES,
|
|
23
|
+
)
|
|
24
|
+
from ...custom_excepthook import custom_excepthook
|
|
25
|
+
from ...env_vars import (
|
|
26
|
+
SF_DEBUG,
|
|
27
|
+
SF_NETWORKHOP_CAPTURE_ENABLED,
|
|
28
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_BODY,
|
|
29
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS,
|
|
30
|
+
SF_NETWORKHOP_CAPTURE_RESPONSE_BODY,
|
|
31
|
+
SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS,
|
|
32
|
+
SF_NETWORKHOP_REQUEST_LIMIT_MB,
|
|
33
|
+
SF_NETWORKHOP_RESPONSE_LIMIT_MB,
|
|
34
|
+
)
|
|
35
|
+
from ...fast_network_hop import fast_send_network_hop_fast, register_endpoint
|
|
36
|
+
from ...thread_local import (
|
|
37
|
+
clear_c_tls_parent_trace_id,
|
|
38
|
+
clear_current_request_path,
|
|
39
|
+
clear_outbound_header_base,
|
|
40
|
+
clear_trace_id,
|
|
41
|
+
generate_new_trace_id,
|
|
42
|
+
get_or_set_sf_trace_id,
|
|
43
|
+
get_sf_trace_id,
|
|
44
|
+
set_current_request_path,
|
|
45
|
+
set_funcspan_override,
|
|
46
|
+
set_outbound_header_base,
|
|
47
|
+
)
|
|
48
|
+
from .cors_utils import inject_sailfish_headers, should_inject_headers
|
|
49
|
+
from .utils import _is_user_code, _unwrap_user_func, should_skip_route
|
|
50
|
+
|
|
51
|
+
_SKIP_TRACING_ATTR = "_sf_skip_tracing"
|
|
52
|
+
|
|
53
|
+
# Size limits in bytes
|
|
54
|
+
_REQUEST_LIMIT_BYTES = SF_NETWORKHOP_REQUEST_LIMIT_MB * 1024 * 1024
|
|
55
|
+
_RESPONSE_LIMIT_BYTES = SF_NETWORKHOP_RESPONSE_LIMIT_MB * 1024 * 1024
|
|
56
|
+
|
|
57
|
+
# OTEL-STYLE: No thread pool! Pure async, call C extension directly
|
|
58
|
+
# C extension has its own background worker thread
|
|
59
|
+
|
|
60
|
+
# Pre-registered endpoint IDs (maps endpoint function id -> endpoint_id from C extension)
|
|
61
|
+
_ENDPOINT_REGISTRY: dict[int, int] = {}
|
|
62
|
+
|
|
63
|
+
# Track which Sanic app instances have been registered (to support multiple apps)
|
|
64
|
+
_REGISTERED_APPS: set[int] = set()
|
|
65
|
+
|
|
66
|
+
# Request data storage (keyed by request id)
|
|
67
|
+
_request_data_lock = Lock()
|
|
68
|
+
_request_data = defaultdict(dict)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _should_trace_endpoint(endpoint_fn) -> bool:
|
|
72
|
+
"""Check if endpoint should be traced."""
|
|
73
|
+
if getattr(endpoint_fn, _SKIP_TRACING_ATTR, False):
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
code = getattr(endpoint_fn, "__code__", None)
|
|
77
|
+
if not code:
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
filename = code.co_filename
|
|
81
|
+
if not _is_user_code(filename):
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
# Skip Strawberry GraphQL handlers
|
|
85
|
+
if getattr(endpoint_fn, "__module__", "").startswith("strawberry"):
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _pre_register_endpoints(app, routes_to_skip: Optional[List[str]] = None):
|
|
92
|
+
"""Pre-register all endpoints at startup."""
|
|
93
|
+
routes_to_skip = routes_to_skip or []
|
|
94
|
+
count = 0
|
|
95
|
+
skipped = 0
|
|
96
|
+
|
|
97
|
+
app_id = id(app)
|
|
98
|
+
|
|
99
|
+
# Check if this app has already been registered
|
|
100
|
+
if app_id in _REGISTERED_APPS:
|
|
101
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
102
|
+
print(
|
|
103
|
+
f"[[_pre_register_endpoints]] App {app_id} already registered, skipping",
|
|
104
|
+
log=False,
|
|
105
|
+
)
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
109
|
+
print(
|
|
110
|
+
f"[[_pre_register_endpoints]] Starting registration for app {app_id}, app has {len(app.router.routes)} routes",
|
|
111
|
+
log=False,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Iterate through all routes in the Sanic router
|
|
115
|
+
for route in app.router.routes:
|
|
116
|
+
if not hasattr(route, "handler"):
|
|
117
|
+
skipped += 1
|
|
118
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
119
|
+
print(
|
|
120
|
+
f"[[_pre_register_endpoints]] Skipping route (no handler): {route}",
|
|
121
|
+
log=False,
|
|
122
|
+
)
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
original_endpoint = route.handler
|
|
126
|
+
endpoint_fn_id = id(original_endpoint)
|
|
127
|
+
|
|
128
|
+
# Check if this specific endpoint function is already registered
|
|
129
|
+
if endpoint_fn_id in _ENDPOINT_REGISTRY:
|
|
130
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
131
|
+
print(
|
|
132
|
+
f"[[_pre_register_endpoints]] Endpoint function {original_endpoint.__name__ if hasattr(original_endpoint, '__name__') else original_endpoint} already registered (id={_ENDPOINT_REGISTRY[endpoint_fn_id]}), skipping",
|
|
133
|
+
log=False,
|
|
134
|
+
)
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
# Check for @skip_network_tracing on the wrapped function BEFORE unwrapping
|
|
138
|
+
if getattr(original_endpoint, _SKIP_TRACING_ATTR, False):
|
|
139
|
+
skipped += 1
|
|
140
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
141
|
+
print(
|
|
142
|
+
f"[[_pre_register_endpoints]] Skipping endpoint (marked with @skip_network_tracing): {original_endpoint.__name__ if hasattr(original_endpoint, '__name__') else original_endpoint}",
|
|
143
|
+
log=False,
|
|
144
|
+
)
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
unwrapped = _unwrap_user_func(original_endpoint)
|
|
148
|
+
|
|
149
|
+
if not _should_trace_endpoint(unwrapped):
|
|
150
|
+
skipped += 1
|
|
151
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
152
|
+
print(
|
|
153
|
+
f"[[_pre_register_endpoints]] Skipping endpoint (not user code): {unwrapped.__name__ if hasattr(unwrapped, '__name__') else unwrapped}",
|
|
154
|
+
log=False,
|
|
155
|
+
)
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
code = unwrapped.__code__
|
|
159
|
+
line_no_str = str(code.co_firstlineno)
|
|
160
|
+
name = unwrapped.__name__
|
|
161
|
+
filename = code.co_filename
|
|
162
|
+
|
|
163
|
+
# Extract route pattern from Sanic route (e.g., "/log/<n>")
|
|
164
|
+
route_pattern = getattr(route, "path", None)
|
|
165
|
+
|
|
166
|
+
# Check if route should be skipped based on wildcard patterns
|
|
167
|
+
if should_skip_route(route_pattern, routes_to_skip):
|
|
168
|
+
skipped += 1
|
|
169
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
170
|
+
print(
|
|
171
|
+
f"[[_pre_register_endpoints]] Skipping endpoint (route matches skip pattern): {route_pattern}",
|
|
172
|
+
log=False,
|
|
173
|
+
)
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
endpoint_id = register_endpoint(
|
|
177
|
+
line=line_no_str,
|
|
178
|
+
column="0",
|
|
179
|
+
name=name,
|
|
180
|
+
entrypoint=filename,
|
|
181
|
+
route=route_pattern,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if endpoint_id < 0:
|
|
185
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
186
|
+
print(
|
|
187
|
+
f"[[_pre_register_endpoints]] Failed to register {name} (endpoint_id={endpoint_id})",
|
|
188
|
+
log=False,
|
|
189
|
+
)
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
_ENDPOINT_REGISTRY[endpoint_fn_id] = endpoint_id
|
|
193
|
+
count += 1
|
|
194
|
+
|
|
195
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
196
|
+
print(
|
|
197
|
+
f"[[patch_sanic]] Registered: {name} @ {filename}:{line_no_str} route={route_pattern} (endpoint_fn_id={endpoint_fn_id}, endpoint_id={endpoint_id})",
|
|
198
|
+
log=False,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
202
|
+
print(
|
|
203
|
+
f"[[patch_sanic]] Total endpoints registered: {count}, skipped: {skipped}",
|
|
204
|
+
log=False,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Only mark this app as registered if we actually registered user endpoints
|
|
208
|
+
# This allows retry on startup event if routes weren't ready yet
|
|
209
|
+
if count > 0:
|
|
210
|
+
_REGISTERED_APPS.add(app_id)
|
|
211
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
212
|
+
print(f"[[patch_sanic]] App {app_id} marked as registered", log=False)
|
|
213
|
+
print(
|
|
214
|
+
f"[[patch_sanic]] OTEL-STYLE: Pure async, direct C call (no Python threads)",
|
|
215
|
+
log=False,
|
|
216
|
+
)
|
|
217
|
+
print(
|
|
218
|
+
f"[[patch_sanic]] Request headers capture: {'ENABLED' if SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS else 'DISABLED'}",
|
|
219
|
+
log=False,
|
|
220
|
+
)
|
|
221
|
+
print(
|
|
222
|
+
f"[[patch_sanic]] Request body capture: {'ENABLED' if SF_NETWORKHOP_CAPTURE_REQUEST_BODY else 'DISABLED'}",
|
|
223
|
+
log=False,
|
|
224
|
+
)
|
|
225
|
+
print(
|
|
226
|
+
f"[[patch_sanic]] Response headers capture: {'ENABLED' if SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS else 'DISABLED'}",
|
|
227
|
+
log=False,
|
|
228
|
+
)
|
|
229
|
+
print(
|
|
230
|
+
f"[[patch_sanic]] Response body capture: {'ENABLED' if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY else 'DISABLED'}",
|
|
231
|
+
log=False,
|
|
232
|
+
)
|
|
233
|
+
print(
|
|
234
|
+
f"[[patch_sanic]] C extension queues to background worker with GIL released",
|
|
235
|
+
log=False,
|
|
236
|
+
)
|
|
237
|
+
else:
|
|
238
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
239
|
+
print(
|
|
240
|
+
f"[[patch_sanic]] No user endpoints registered yet for app {app_id}, will retry on startup",
|
|
241
|
+
log=False,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def patch_sanic(routes_to_skip: Optional[List[str]] = None):
|
|
246
|
+
"""
|
|
247
|
+
OTEL-STYLE PURE ASYNC:
|
|
248
|
+
• Direct C call IMMEDIATELY after response completes
|
|
249
|
+
• Sanic returns instantly (C queues and returns in ~1µs)
|
|
250
|
+
• C background thread does all network work with GIL released
|
|
251
|
+
• This is TRUE zero overhead!
|
|
252
|
+
"""
|
|
253
|
+
routes_to_skip = routes_to_skip or []
|
|
254
|
+
|
|
255
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
256
|
+
print("[[patch_sanic]] patching sanic...", log=False)
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
from sanic import Sanic
|
|
260
|
+
except ImportError:
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
264
|
+
print("[[patch_sanic]] Patching Sanic.__init__", log=False)
|
|
265
|
+
|
|
266
|
+
orig_init = Sanic.__init__
|
|
267
|
+
|
|
268
|
+
def patched_init(self, *args, **kwargs):
|
|
269
|
+
"""
|
|
270
|
+
After the original Sanic app is created we attach:
|
|
271
|
+
• request middleware – capture header propagation + request headers/body
|
|
272
|
+
• response middleware – emit NetworkHop with response headers/body
|
|
273
|
+
• universal exception handler – capture all exceptions
|
|
274
|
+
"""
|
|
275
|
+
# Let Sanic build the app normally
|
|
276
|
+
orig_init(self, *args, **kwargs)
|
|
277
|
+
|
|
278
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
279
|
+
print(
|
|
280
|
+
f"[[patch_sanic]] patched_init called - about to register middleware",
|
|
281
|
+
log=False,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# 1. Request middleware: header propagation + request capture
|
|
285
|
+
async def _capture_request(request):
|
|
286
|
+
# Set current request path for route-based suppression (SF_DISABLE_INBOUND_NETWORK_TRACING_ON_ROUTES)
|
|
287
|
+
set_current_request_path(request.path)
|
|
288
|
+
|
|
289
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
290
|
+
print(
|
|
291
|
+
f"[[Sanic]] _capture_request called for {request.method} {request.path}",
|
|
292
|
+
log=False,
|
|
293
|
+
)
|
|
294
|
+
try:
|
|
295
|
+
# PERFORMANCE: Single-pass bytes-level header scan (no dict allocation until needed)
|
|
296
|
+
# Scan headers once, only decode what we need, use latin-1 (fast 1:1 byte map)
|
|
297
|
+
incoming_trace_raw = None # bytes
|
|
298
|
+
funcspan_raw = None # bytes
|
|
299
|
+
req_headers = None # dict[str,str] only if capture enabled
|
|
300
|
+
|
|
301
|
+
capture_req_headers = (
|
|
302
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS # local cache
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Sanic headers are already decoded strings, so we work with them directly
|
|
306
|
+
# Convert to lowercase for comparison
|
|
307
|
+
if capture_req_headers:
|
|
308
|
+
req_headers = dict(request.headers)
|
|
309
|
+
# Extract special headers
|
|
310
|
+
for k, v in request.headers.items():
|
|
311
|
+
k_lower = k.lower()
|
|
312
|
+
if k_lower == SAILFISH_TRACING_HEADER.lower():
|
|
313
|
+
incoming_trace_raw = v
|
|
314
|
+
elif k_lower == "x-sf3-functionspancaptureoverride":
|
|
315
|
+
funcspan_raw = v
|
|
316
|
+
else:
|
|
317
|
+
# Just extract special headers
|
|
318
|
+
for k, v in request.headers.items():
|
|
319
|
+
k_lower = k.lower()
|
|
320
|
+
if k_lower == SAILFISH_TRACING_HEADER.lower():
|
|
321
|
+
incoming_trace_raw = v
|
|
322
|
+
elif k_lower == "x-sf3-functionspancaptureoverride":
|
|
323
|
+
funcspan_raw = v
|
|
324
|
+
|
|
325
|
+
# CRITICAL: Seed/ensure trace_id immediately (BEFORE any outbound work)
|
|
326
|
+
if incoming_trace_raw:
|
|
327
|
+
# Incoming X-Sf3-Rid header provided - use it
|
|
328
|
+
get_or_set_sf_trace_id(
|
|
329
|
+
incoming_trace_raw, is_associated_with_inbound_request=True
|
|
330
|
+
)
|
|
331
|
+
else:
|
|
332
|
+
# No incoming X-Sf3-Rid header - generate fresh trace_id for this request
|
|
333
|
+
generate_new_trace_id()
|
|
334
|
+
|
|
335
|
+
# Optional funcspan override
|
|
336
|
+
funcspan_override_header = funcspan_raw
|
|
337
|
+
if funcspan_override_header:
|
|
338
|
+
try:
|
|
339
|
+
set_funcspan_override(funcspan_override_header)
|
|
340
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
341
|
+
print(
|
|
342
|
+
f"[[Sanic]] Set function span override from header: {funcspan_override_header}",
|
|
343
|
+
log=False,
|
|
344
|
+
)
|
|
345
|
+
except Exception as e:
|
|
346
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
347
|
+
print(
|
|
348
|
+
f"[[Sanic]] Failed to set function span override: {e}",
|
|
349
|
+
log=False,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Initialize outbound base without list/allocs from split()
|
|
353
|
+
try:
|
|
354
|
+
trace_id = get_sf_trace_id()
|
|
355
|
+
if trace_id:
|
|
356
|
+
s = str(trace_id)
|
|
357
|
+
i = s.find("/") # session
|
|
358
|
+
j = s.find("/", i + 1) if i != -1 else -1 # page
|
|
359
|
+
if j != -1:
|
|
360
|
+
base_trace = s[:j] # "session/page"
|
|
361
|
+
set_outbound_header_base(
|
|
362
|
+
base_trace=base_trace,
|
|
363
|
+
parent_trace_id=s, # "session/page/uuid"
|
|
364
|
+
funcspan=funcspan_override_header,
|
|
365
|
+
)
|
|
366
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
367
|
+
print(
|
|
368
|
+
f"[[Sanic]] Initialized outbound header base (base={base_trace[:16]}...)",
|
|
369
|
+
log=False,
|
|
370
|
+
)
|
|
371
|
+
except Exception as e:
|
|
372
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
373
|
+
print(
|
|
374
|
+
f"[[Sanic]] Failed to initialize outbound header base: {e}",
|
|
375
|
+
log=False,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# OPTIMIZATION: Skip ALL capture infrastructure if not capturing network hops
|
|
379
|
+
# We still needed to set up trace_id and outbound header base above (for outbound call tracing)
|
|
380
|
+
# but we can skip all request/response capture overhead
|
|
381
|
+
if not SF_NETWORKHOP_CAPTURE_ENABLED:
|
|
382
|
+
return
|
|
383
|
+
|
|
384
|
+
# Use request id as key for storing data
|
|
385
|
+
req_id = id(request)
|
|
386
|
+
|
|
387
|
+
# Store captured request headers (already captured in single-pass scan above if enabled)
|
|
388
|
+
if req_headers:
|
|
389
|
+
with _request_data_lock:
|
|
390
|
+
_request_data[req_id]["headers"] = req_headers
|
|
391
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
392
|
+
print(
|
|
393
|
+
f"[[Sanic]] Captured request headers: {len(req_headers)} headers",
|
|
394
|
+
log=False,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# Capture request body if enabled
|
|
398
|
+
if SF_NETWORKHOP_CAPTURE_REQUEST_BODY:
|
|
399
|
+
try:
|
|
400
|
+
# Sanic request.body is available as bytes
|
|
401
|
+
body = request.body
|
|
402
|
+
if body:
|
|
403
|
+
req_body = body[:_REQUEST_LIMIT_BYTES]
|
|
404
|
+
with _request_data_lock:
|
|
405
|
+
_request_data[req_id]["body"] = req_body
|
|
406
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
407
|
+
print(
|
|
408
|
+
f"[[Sanic]] Request body capture: {len(req_body)} bytes (method={request.method})",
|
|
409
|
+
log=False,
|
|
410
|
+
)
|
|
411
|
+
except Exception as e:
|
|
412
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
413
|
+
print(
|
|
414
|
+
f"[[Sanic]] Failed to capture request body: {e}",
|
|
415
|
+
log=False,
|
|
416
|
+
)
|
|
417
|
+
except Exception as e:
|
|
418
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
419
|
+
print(f"[[Sanic]] Request middleware error: {e}", log=False)
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
self.register_middleware(_capture_request, attach_to="request")
|
|
423
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
424
|
+
print(
|
|
425
|
+
f"[[patch_sanic]] Successfully registered request middleware",
|
|
426
|
+
log=False,
|
|
427
|
+
)
|
|
428
|
+
except TypeError: # Sanic<22 compatibility
|
|
429
|
+
self.register_middleware(_capture_request, "request")
|
|
430
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
431
|
+
print(
|
|
432
|
+
f"[[patch_sanic]] Successfully registered request middleware (legacy)",
|
|
433
|
+
log=False,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# 2. Response middleware: OTEL-STYLE NetworkHop emission with response capture
|
|
437
|
+
async def _emit_hop(request, response):
|
|
438
|
+
"""
|
|
439
|
+
OTEL-STYLE: Emit network hop in response middleware.
|
|
440
|
+
In Sanic, response middleware runs after handler but before send.
|
|
441
|
+
"""
|
|
442
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
443
|
+
print(
|
|
444
|
+
f"[[Sanic]] _emit_hop called for {request.method} {request.path}",
|
|
445
|
+
log=False,
|
|
446
|
+
)
|
|
447
|
+
req_id = id(request)
|
|
448
|
+
try:
|
|
449
|
+
handler = getattr(request, "route", None)
|
|
450
|
+
if not handler:
|
|
451
|
+
return
|
|
452
|
+
fn = getattr(handler, "handler", None)
|
|
453
|
+
if not fn:
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
# Use the pre-registered endpoint ID
|
|
457
|
+
endpoint_id = _ENDPOINT_REGISTRY.get(id(fn))
|
|
458
|
+
if endpoint_id is None or endpoint_id < 0:
|
|
459
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
460
|
+
print(
|
|
461
|
+
f"[[Sanic]] Skipping NetworkHop (endpoint not registered or endpoint_id={endpoint_id})",
|
|
462
|
+
log=False,
|
|
463
|
+
)
|
|
464
|
+
return
|
|
465
|
+
|
|
466
|
+
if endpoint_id is not None and endpoint_id >= 0:
|
|
467
|
+
try:
|
|
468
|
+
# OPTIMIZATION: Use get_sf_trace_id() directly instead of get_or_set_sf_trace_id()
|
|
469
|
+
# Trace ID is GUARANTEED to be set at request start
|
|
470
|
+
# This saves ~11-12μs by avoiding tuple unpacking and conditional logic
|
|
471
|
+
session_id = get_sf_trace_id()
|
|
472
|
+
|
|
473
|
+
# Get captured request data
|
|
474
|
+
req_headers = None
|
|
475
|
+
req_body = None
|
|
476
|
+
with _request_data_lock:
|
|
477
|
+
req_data = _request_data.get(req_id, {})
|
|
478
|
+
req_headers = req_data.get("headers")
|
|
479
|
+
req_body = req_data.get("body")
|
|
480
|
+
|
|
481
|
+
# Capture response headers if enabled
|
|
482
|
+
resp_headers = None
|
|
483
|
+
if SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS and response:
|
|
484
|
+
try:
|
|
485
|
+
# Sanic response.headers is a dict-like object
|
|
486
|
+
resp_headers = dict(response.headers)
|
|
487
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
488
|
+
print(
|
|
489
|
+
f"[[Sanic]] Captured response headers: {len(resp_headers)} headers",
|
|
490
|
+
log=False,
|
|
491
|
+
)
|
|
492
|
+
except Exception as e:
|
|
493
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
494
|
+
print(
|
|
495
|
+
f"[[Sanic]] Failed to capture response headers: {e}",
|
|
496
|
+
log=False,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
# Capture response body if enabled
|
|
500
|
+
resp_body = None
|
|
501
|
+
if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY and response:
|
|
502
|
+
try:
|
|
503
|
+
# Sanic response.body is bytes
|
|
504
|
+
if hasattr(response, "body") and response.body:
|
|
505
|
+
if isinstance(response.body, bytes):
|
|
506
|
+
resp_body = response.body[
|
|
507
|
+
:_RESPONSE_LIMIT_BYTES
|
|
508
|
+
]
|
|
509
|
+
if (
|
|
510
|
+
SF_DEBUG
|
|
511
|
+
and app_config._interceptors_initialized
|
|
512
|
+
):
|
|
513
|
+
print(
|
|
514
|
+
f"[[Sanic]] Captured response body: {len(resp_body)} bytes",
|
|
515
|
+
log=False,
|
|
516
|
+
)
|
|
517
|
+
elif isinstance(response.body, str):
|
|
518
|
+
resp_body = response.body.encode("utf-8")[
|
|
519
|
+
:_RESPONSE_LIMIT_BYTES
|
|
520
|
+
]
|
|
521
|
+
if (
|
|
522
|
+
SF_DEBUG
|
|
523
|
+
and app_config._interceptors_initialized
|
|
524
|
+
):
|
|
525
|
+
print(
|
|
526
|
+
f"[[Sanic]] Captured response body (str): {len(resp_body)} bytes",
|
|
527
|
+
log=False,
|
|
528
|
+
)
|
|
529
|
+
except Exception as e:
|
|
530
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
531
|
+
print(
|
|
532
|
+
f"[[Sanic]] Failed to capture response body: {e}",
|
|
533
|
+
log=False,
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
537
|
+
print(
|
|
538
|
+
f"[[Sanic]] About to emit network hop: endpoint_id={endpoint_id}, "
|
|
539
|
+
f"req_headers={'present' if req_headers else 'None'}, "
|
|
540
|
+
f"req_body={len(req_body) if req_body else 0} bytes, "
|
|
541
|
+
f"resp_headers={'present' if resp_headers else 'None'}, "
|
|
542
|
+
f"resp_body={len(resp_body) if resp_body else 0} bytes",
|
|
543
|
+
log=False,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
# Direct C call - queues to background worker, returns instantly
|
|
547
|
+
# Extract route and query params from request
|
|
548
|
+
raw_path = request.path
|
|
549
|
+
raw_query = (
|
|
550
|
+
request.query_string.encode("utf-8")
|
|
551
|
+
if request.query_string
|
|
552
|
+
else b""
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
fast_send_network_hop_fast(
|
|
556
|
+
session_id=session_id,
|
|
557
|
+
endpoint_id=endpoint_id,
|
|
558
|
+
raw_path=raw_path,
|
|
559
|
+
raw_query_string=raw_query,
|
|
560
|
+
request_headers=req_headers,
|
|
561
|
+
request_body=req_body,
|
|
562
|
+
response_headers=resp_headers,
|
|
563
|
+
response_body=resp_body,
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
567
|
+
print(
|
|
568
|
+
f"[[Sanic]] Emitted network hop: endpoint_id={endpoint_id} "
|
|
569
|
+
f"session={session_id}",
|
|
570
|
+
log=False,
|
|
571
|
+
)
|
|
572
|
+
except Exception as e: # noqa: BLE001 S110
|
|
573
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
574
|
+
print(
|
|
575
|
+
f"[[Sanic]] Failed to emit network hop: {e}", log=False
|
|
576
|
+
)
|
|
577
|
+
finally:
|
|
578
|
+
# Clean up request data to prevent memory leak
|
|
579
|
+
with _request_data_lock:
|
|
580
|
+
_request_data.pop(req_id, None)
|
|
581
|
+
|
|
582
|
+
# CRITICAL: Clear C TLS to prevent stale data in thread pools
|
|
583
|
+
clear_c_tls_parent_trace_id()
|
|
584
|
+
|
|
585
|
+
# CRITICAL: Clear outbound header base to prevent stale cached headers
|
|
586
|
+
# ContextVar does NOT automatically clean up in thread pools - must clear explicitly
|
|
587
|
+
clear_outbound_header_base()
|
|
588
|
+
|
|
589
|
+
# CRITICAL: Clear trace_id to ensure fresh generation for next request
|
|
590
|
+
# Without this, get_or_set_sf_trace_id() reuses trace_id from previous request
|
|
591
|
+
# causing X-Sf4-Prid to stay constant when no incoming X-Sf3-Rid header
|
|
592
|
+
clear_trace_id()
|
|
593
|
+
|
|
594
|
+
# CRITICAL: Clear current request path to prevent stale data in thread pools
|
|
595
|
+
clear_current_request_path()
|
|
596
|
+
except Exception as e:
|
|
597
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
598
|
+
print(f"[[Sanic]] Response middleware error: {e}", log=False)
|
|
599
|
+
# Clean up on error too
|
|
600
|
+
with _request_data_lock:
|
|
601
|
+
_request_data.pop(req_id, None)
|
|
602
|
+
|
|
603
|
+
# CRITICAL: Clear C TLS to prevent stale data in thread pools
|
|
604
|
+
clear_c_tls_parent_trace_id()
|
|
605
|
+
|
|
606
|
+
# CRITICAL: Clear outbound header base to prevent stale cached headers
|
|
607
|
+
clear_outbound_header_base()
|
|
608
|
+
|
|
609
|
+
# CRITICAL: Clear trace_id to ensure fresh generation for next request
|
|
610
|
+
clear_trace_id()
|
|
611
|
+
|
|
612
|
+
# CRITICAL: Clear current request path to prevent stale data in thread pools
|
|
613
|
+
clear_current_request_path()
|
|
614
|
+
|
|
615
|
+
try:
|
|
616
|
+
self.register_middleware(_emit_hop, attach_to="response")
|
|
617
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
618
|
+
print(
|
|
619
|
+
f"[[patch_sanic]] Successfully registered response middleware",
|
|
620
|
+
log=False,
|
|
621
|
+
)
|
|
622
|
+
except TypeError:
|
|
623
|
+
self.register_middleware(_emit_hop, "response")
|
|
624
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
625
|
+
print(
|
|
626
|
+
f"[[patch_sanic]] Successfully registered response middleware (legacy)",
|
|
627
|
+
log=False,
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
# 3. Universal exception handler
|
|
631
|
+
async def _capture_exception(request, exception):
|
|
632
|
+
"""
|
|
633
|
+
Called for any exception – user errors, abort/HTTPException,
|
|
634
|
+
or Sanic-specific errors. Forward to custom_excepthook
|
|
635
|
+
and then fall back to Sanic's default error handler.
|
|
636
|
+
"""
|
|
637
|
+
try:
|
|
638
|
+
custom_excepthook(type(exception), exception, exception.__traceback__)
|
|
639
|
+
except Exception:
|
|
640
|
+
pass # Don't let exception handler crash
|
|
641
|
+
|
|
642
|
+
# Delegate to default handler to keep standard 4xx/5xx payload
|
|
643
|
+
try:
|
|
644
|
+
response = request.app.error_handler.default(request, exception)
|
|
645
|
+
if inspect.isawaitable(response):
|
|
646
|
+
response = await response
|
|
647
|
+
return response
|
|
648
|
+
except Exception:
|
|
649
|
+
# If default handler fails, just re-raise
|
|
650
|
+
raise exception
|
|
651
|
+
|
|
652
|
+
# Register for base Exception class to catch everything
|
|
653
|
+
try:
|
|
654
|
+
self.error_handler.add(Exception, _capture_exception)
|
|
655
|
+
except Exception as e:
|
|
656
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
657
|
+
print(f"[[Sanic]] Failed to register exception handler: {e}", log=False)
|
|
658
|
+
|
|
659
|
+
# Try to register endpoints immediately if routes are already defined
|
|
660
|
+
# This handles apps where routes are defined before __init__ completes
|
|
661
|
+
if (
|
|
662
|
+
hasattr(self, "router")
|
|
663
|
+
and hasattr(self.router, "routes")
|
|
664
|
+
and self.router.routes
|
|
665
|
+
):
|
|
666
|
+
try:
|
|
667
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
668
|
+
print(
|
|
669
|
+
f"[[patch_sanic]] Routes already defined ({len(self.router.routes)} routes), registering immediately",
|
|
670
|
+
log=False,
|
|
671
|
+
)
|
|
672
|
+
_pre_register_endpoints(self, routes_to_skip)
|
|
673
|
+
except Exception as e:
|
|
674
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
675
|
+
print(
|
|
676
|
+
f"[[patch_sanic]] Immediate registration failed: {e}",
|
|
677
|
+
log=False,
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
# Register on after_server_start event as a fallback (for routes added after __init__)
|
|
681
|
+
@self.listener("after_server_start")
|
|
682
|
+
async def _sf_startup(app, loop):
|
|
683
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
684
|
+
print(
|
|
685
|
+
"[[patch_sanic]] After server start event fired, registering endpoints",
|
|
686
|
+
log=False,
|
|
687
|
+
)
|
|
688
|
+
# _pre_register_endpoints now checks if this app was already registered
|
|
689
|
+
_pre_register_endpoints(self, routes_to_skip)
|
|
690
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
691
|
+
print(
|
|
692
|
+
"[[patch_sanic]] ZERO-OVERHEAD pattern activated (truly async, no blocking)",
|
|
693
|
+
log=False,
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
697
|
+
print(
|
|
698
|
+
"[[patch_sanic]] OTEL-style middlewares + exception handler installed",
|
|
699
|
+
log=False,
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
Sanic.__init__ = patched_init
|
|
703
|
+
|
|
704
|
+
# Also patch any existing Sanic instances that were created before patching
|
|
705
|
+
# This handles the case where app = Sanic() happens before setup_interceptors()
|
|
706
|
+
for obj in gc.get_objects():
|
|
707
|
+
try:
|
|
708
|
+
# Wrap in try-except to safely handle lazy objects (e.g., Django settings)
|
|
709
|
+
# that trigger initialization on attribute access
|
|
710
|
+
if isinstance(obj, Sanic):
|
|
711
|
+
# Check if this app already has our middleware
|
|
712
|
+
# In Sanic, middlewares are stored in a list
|
|
713
|
+
has_our_middleware = False
|
|
714
|
+
if hasattr(obj, "middlewares"):
|
|
715
|
+
# Check if any middleware matches our function names
|
|
716
|
+
for mw in obj.middlewares:
|
|
717
|
+
if hasattr(mw, "__name__") and mw.__name__ in (
|
|
718
|
+
"_capture_request",
|
|
719
|
+
"_emit_hop",
|
|
720
|
+
):
|
|
721
|
+
has_our_middleware = True
|
|
722
|
+
break
|
|
723
|
+
|
|
724
|
+
if not has_our_middleware:
|
|
725
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
726
|
+
print(
|
|
727
|
+
f"[[patch_sanic]] Retroactively patching existing Sanic app",
|
|
728
|
+
log=False,
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
# We can't easily retroactively add middleware to an already-created Sanic app
|
|
732
|
+
# Just try to register endpoints if they exist
|
|
733
|
+
if (
|
|
734
|
+
hasattr(obj, "router")
|
|
735
|
+
and hasattr(obj.router, "routes")
|
|
736
|
+
and obj.router.routes
|
|
737
|
+
):
|
|
738
|
+
try:
|
|
739
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
740
|
+
print(
|
|
741
|
+
f"[[patch_sanic]] Retroactive immediate registration ({len(obj.router.routes)} routes)",
|
|
742
|
+
log=False,
|
|
743
|
+
)
|
|
744
|
+
_pre_register_endpoints(obj, routes_to_skip)
|
|
745
|
+
except Exception as e:
|
|
746
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
747
|
+
print(
|
|
748
|
+
f"[[patch_sanic]] Retroactive immediate registration failed: {e}",
|
|
749
|
+
log=False,
|
|
750
|
+
)
|
|
751
|
+
except Exception:
|
|
752
|
+
# Silently skip objects that fail isinstance checks (e.g., Django lazy settings)
|
|
753
|
+
pass
|
|
754
|
+
|
|
755
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
756
|
+
print("[[patch_sanic]] OTEL-style patch applied", log=False)
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def patch_sanic_cors():
|
|
760
|
+
"""
|
|
761
|
+
Patch Sanic CORS middleware to automatically inject Sailfish headers.
|
|
762
|
+
|
|
763
|
+
SAFE: Only modifies CORS if CORSMiddleware is used by the application.
|
|
764
|
+
This works for both sanic-cors and sanic-ext CORS implementations.
|
|
765
|
+
"""
|
|
766
|
+
# Try to patch sanic-ext CORS first
|
|
767
|
+
try:
|
|
768
|
+
from sanic_ext.extensions.http.cors import cors
|
|
769
|
+
|
|
770
|
+
# Check if already patched
|
|
771
|
+
if hasattr(cors, "_sf_cors_patched"):
|
|
772
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
773
|
+
print(
|
|
774
|
+
"[[patch_sanic_cors]] sanic-ext already patched, skipping",
|
|
775
|
+
log=False,
|
|
776
|
+
)
|
|
777
|
+
return
|
|
778
|
+
|
|
779
|
+
# Patch the cors configuration function
|
|
780
|
+
original_cors_init = cors.__init__ if hasattr(cors, "__init__") else None
|
|
781
|
+
|
|
782
|
+
if original_cors_init:
|
|
783
|
+
|
|
784
|
+
def patched_cors_init(self, *args, allow_headers=None, **kwargs):
|
|
785
|
+
# Intercept allow_headers parameter
|
|
786
|
+
if should_inject_headers(allow_headers):
|
|
787
|
+
allow_headers = inject_sailfish_headers(allow_headers)
|
|
788
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
789
|
+
print(
|
|
790
|
+
"[[patch_sanic_cors]] Injected Sailfish headers into sanic-ext CORS",
|
|
791
|
+
log=False,
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
# Call original init with potentially modified headers
|
|
795
|
+
original_cors_init(self, *args, allow_headers=allow_headers, **kwargs)
|
|
796
|
+
|
|
797
|
+
cors.__init__ = patched_cors_init
|
|
798
|
+
cors._sf_cors_patched = True
|
|
799
|
+
|
|
800
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
801
|
+
print(
|
|
802
|
+
"[[patch_sanic_cors]] Successfully patched sanic-ext CORS",
|
|
803
|
+
log=False,
|
|
804
|
+
)
|
|
805
|
+
except ImportError:
|
|
806
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
807
|
+
print(
|
|
808
|
+
"[[patch_sanic_cors]] sanic-ext not found, trying sanic-cors",
|
|
809
|
+
log=False,
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
# Try to patch sanic-cors
|
|
813
|
+
try:
|
|
814
|
+
from sanic_cors import CORS
|
|
815
|
+
|
|
816
|
+
# Check if already patched
|
|
817
|
+
if hasattr(CORS, "_sf_cors_patched"):
|
|
818
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
819
|
+
print(
|
|
820
|
+
"[[patch_sanic_cors]] sanic-cors already patched, skipping",
|
|
821
|
+
log=False,
|
|
822
|
+
)
|
|
823
|
+
return
|
|
824
|
+
|
|
825
|
+
original_cors_init = CORS.__init__
|
|
826
|
+
|
|
827
|
+
def patched_cors_init(self, app=None, *args, allow_headers=None, **kwargs):
|
|
828
|
+
# Intercept allow_headers parameter
|
|
829
|
+
if should_inject_headers(allow_headers):
|
|
830
|
+
allow_headers = inject_sailfish_headers(allow_headers)
|
|
831
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
832
|
+
print(
|
|
833
|
+
"[[patch_sanic_cors]] Injected Sailfish headers into sanic-cors",
|
|
834
|
+
log=False,
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
# Call original init with potentially modified headers
|
|
838
|
+
original_cors_init(self, app, *args, allow_headers=allow_headers, **kwargs)
|
|
839
|
+
|
|
840
|
+
CORS.__init__ = patched_cors_init
|
|
841
|
+
CORS._sf_cors_patched = True
|
|
842
|
+
|
|
843
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
844
|
+
print(
|
|
845
|
+
"[[patch_sanic_cors]] Successfully patched sanic-cors",
|
|
846
|
+
log=False,
|
|
847
|
+
)
|
|
848
|
+
except ImportError:
|
|
849
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
850
|
+
print(
|
|
851
|
+
"[[patch_sanic_cors]] sanic-cors not found, skipping CORS patching",
|
|
852
|
+
log=False,
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
# Call CORS patching
|
|
857
|
+
patch_sanic_cors()
|