sf-veritas 0.10.3__cp313-cp313-manylinux_2_28_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sf-veritas might be problematic. Click here for more details.
- sf_veritas/__init__.py +20 -0
- sf_veritas/_sffastlog.c +889 -0
- sf_veritas/_sffastlog.cpython-313-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnet.c +924 -0
- sf_veritas/_sffastnet.cpython-313-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnetworkrequest.c +730 -0
- sf_veritas/_sffastnetworkrequest.cpython-313-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan.c +2155 -0
- sf_veritas/_sffuncspan.cpython-313-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan_config.c +617 -0
- sf_veritas/_sffuncspan_config.cpython-313-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfheadercheck.c +341 -0
- sf_veritas/_sfheadercheck.cpython-313-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfnetworkhop.c +1451 -0
- sf_veritas/_sfnetworkhop.cpython-313-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfservice.c +1175 -0
- sf_veritas/_sfservice.cpython-313-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,481 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import site
|
|
3
|
+
import sysconfig
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from typing import Any, Callable, List, Optional, Set, Tuple
|
|
6
|
+
|
|
7
|
+
from ... import app_config
|
|
8
|
+
from ...constants import (
|
|
9
|
+
FUNCSPAN_OVERRIDE_HEADER_BYTES,
|
|
10
|
+
SAILFISH_TRACING_HEADER,
|
|
11
|
+
SAILFISH_TRACING_HEADER_BYTES,
|
|
12
|
+
)
|
|
13
|
+
from ...custom_excepthook import custom_excepthook
|
|
14
|
+
from ...env_vars import (
|
|
15
|
+
SF_DEBUG,
|
|
16
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_BODY,
|
|
17
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS,
|
|
18
|
+
SF_NETWORKHOP_CAPTURE_RESPONSE_BODY,
|
|
19
|
+
SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS,
|
|
20
|
+
SF_NETWORKHOP_REQUEST_LIMIT_MB,
|
|
21
|
+
SF_NETWORKHOP_RESPONSE_LIMIT_MB,
|
|
22
|
+
)
|
|
23
|
+
from ...fast_network_hop import fast_send_network_hop_fast, register_endpoint
|
|
24
|
+
from ...thread_local import (
|
|
25
|
+
clear_c_tls_parent_trace_id,
|
|
26
|
+
clear_current_request_path,
|
|
27
|
+
clear_funcspan_override,
|
|
28
|
+
clear_outbound_header_base,
|
|
29
|
+
clear_trace_id,
|
|
30
|
+
generate_new_trace_id,
|
|
31
|
+
get_or_set_sf_trace_id,
|
|
32
|
+
get_sf_trace_id,
|
|
33
|
+
set_current_request_path,
|
|
34
|
+
set_funcspan_override,
|
|
35
|
+
set_outbound_header_base,
|
|
36
|
+
)
|
|
37
|
+
from .cors_utils import inject_sailfish_headers, should_inject_headers
|
|
38
|
+
from .utils import _is_user_code, _unwrap_user_func, should_skip_route
|
|
39
|
+
|
|
40
|
+
# Size limits in bytes
|
|
41
|
+
_REQUEST_LIMIT_BYTES = SF_NETWORKHOP_REQUEST_LIMIT_MB * 1024 * 1024
|
|
42
|
+
_RESPONSE_LIMIT_BYTES = SF_NETWORKHOP_RESPONSE_LIMIT_MB * 1024 * 1024
|
|
43
|
+
|
|
44
|
+
# Pre-registered endpoint IDs
|
|
45
|
+
_ENDPOINT_REGISTRY: dict[tuple, int] = {}
|
|
46
|
+
|
|
47
|
+
# Module-level variable for routes to skip (set by patch_tornado)
|
|
48
|
+
_ROUTES_TO_SKIP = []
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def patch_tornado(routes_to_skip: Optional[List[str]] = None):
|
|
52
|
+
"""
|
|
53
|
+
Monkey-patch tornado.web.RequestHandler so that every request:
|
|
54
|
+
|
|
55
|
+
1. Propagates SAILFISH_TRACING_HEADER into the ContextVar.
|
|
56
|
+
2. Emits ONE NetworkHop when user-land verb handler starts.
|
|
57
|
+
3. Funnels *all* exceptions—including tornado.web.HTTPError—through
|
|
58
|
+
custom_excepthook before Tornado's own error machinery runs.
|
|
59
|
+
|
|
60
|
+
Safe no-op if Tornado isn't installed.
|
|
61
|
+
"""
|
|
62
|
+
global _ROUTES_TO_SKIP
|
|
63
|
+
_ROUTES_TO_SKIP = routes_to_skip or []
|
|
64
|
+
try:
|
|
65
|
+
import tornado.web
|
|
66
|
+
except ImportError: # Tornado not installed
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
# --------------------------------------------------------------- #
|
|
70
|
+
# a) Header capture + endpoint metadata (prepare)
|
|
71
|
+
# --------------------------------------------------------------- #
|
|
72
|
+
original_prepare = tornado.web.RequestHandler.prepare
|
|
73
|
+
|
|
74
|
+
def patched_prepare(self, *args, **kwargs):
|
|
75
|
+
# Set current request path for route-based suppression (SF_DISABLE_INBOUND_NETWORK_TRACING_ON_ROUTES)
|
|
76
|
+
set_current_request_path(self.request.path)
|
|
77
|
+
|
|
78
|
+
# -- 1) PERFORMANCE: Single-pass bytes-level header scan (similar to FastAPI optimization)
|
|
79
|
+
# Tornado stores headers as HTTPHeaders object, iterate once to extract needed headers
|
|
80
|
+
incoming_trace_header = None
|
|
81
|
+
funcspan_override_header = None
|
|
82
|
+
|
|
83
|
+
# Scan headers once
|
|
84
|
+
for name, value in self.request.headers.get_all():
|
|
85
|
+
name_lower = name.lower()
|
|
86
|
+
if name_lower == SAILFISH_TRACING_HEADER.lower():
|
|
87
|
+
incoming_trace_header = value
|
|
88
|
+
elif name_lower == "x-sf3-functionspancaptureoverride":
|
|
89
|
+
funcspan_override_header = value
|
|
90
|
+
|
|
91
|
+
# -- 2) CRITICAL: Seed/ensure trace_id immediately (BEFORE any outbound work)
|
|
92
|
+
if incoming_trace_header:
|
|
93
|
+
# Incoming X-Sf3-Rid header provided - use it
|
|
94
|
+
get_or_set_sf_trace_id(
|
|
95
|
+
incoming_trace_header, is_associated_with_inbound_request=True
|
|
96
|
+
)
|
|
97
|
+
else:
|
|
98
|
+
# No incoming X-Sf3-Rid header - generate fresh trace_id for this request
|
|
99
|
+
generate_new_trace_id()
|
|
100
|
+
|
|
101
|
+
# -- 3) Set function span override if provided
|
|
102
|
+
if funcspan_override_header:
|
|
103
|
+
try:
|
|
104
|
+
set_funcspan_override(funcspan_override_header)
|
|
105
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
106
|
+
print(
|
|
107
|
+
f"[[Tornado.prepare]] Set function span override from header: {funcspan_override_header}",
|
|
108
|
+
log=False,
|
|
109
|
+
)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
112
|
+
print(
|
|
113
|
+
f"[[Tornado.prepare]] Failed to set function span override: {e}",
|
|
114
|
+
log=False,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# -- 4) Initialize outbound base without list/allocs from split()
|
|
118
|
+
try:
|
|
119
|
+
trace_id = get_sf_trace_id()
|
|
120
|
+
if trace_id:
|
|
121
|
+
s = str(trace_id)
|
|
122
|
+
i = s.find("/") # session
|
|
123
|
+
j = s.find("/", i + 1) if i != -1 else -1 # page
|
|
124
|
+
if j != -1:
|
|
125
|
+
base_trace = s[:j] # "session/page"
|
|
126
|
+
set_outbound_header_base(
|
|
127
|
+
base_trace=base_trace,
|
|
128
|
+
parent_trace_id=s, # "session/page/uuid"
|
|
129
|
+
funcspan=funcspan_override_header,
|
|
130
|
+
)
|
|
131
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
132
|
+
print(
|
|
133
|
+
f"[[Tornado.prepare]] Initialized outbound header base (base={base_trace[:16]}...)",
|
|
134
|
+
log=False,
|
|
135
|
+
)
|
|
136
|
+
except Exception as e:
|
|
137
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
138
|
+
print(
|
|
139
|
+
f"[[Tornado.prepare]] Failed to initialize outbound header base: {e}",
|
|
140
|
+
log=False,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# -- 5) Capture request headers if enabled
|
|
144
|
+
if SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS:
|
|
145
|
+
try:
|
|
146
|
+
req_headers = dict(self.request.headers)
|
|
147
|
+
self._sf_request_headers = req_headers
|
|
148
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
149
|
+
print(
|
|
150
|
+
f"[[Tornado]] Captured request headers: {len(req_headers)} headers",
|
|
151
|
+
log=False,
|
|
152
|
+
)
|
|
153
|
+
except Exception as e:
|
|
154
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
155
|
+
print(
|
|
156
|
+
f"[[Tornado]] Failed to capture request headers: {e}", log=False
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# -- 6) Capture request body if enabled
|
|
160
|
+
if SF_NETWORKHOP_CAPTURE_REQUEST_BODY:
|
|
161
|
+
try:
|
|
162
|
+
body = self.request.body
|
|
163
|
+
if body:
|
|
164
|
+
req_body = body[:_REQUEST_LIMIT_BYTES]
|
|
165
|
+
self._sf_request_body = req_body
|
|
166
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
167
|
+
print(
|
|
168
|
+
f"[[Tornado]] Request body capture: {len(req_body)} bytes",
|
|
169
|
+
log=False,
|
|
170
|
+
)
|
|
171
|
+
except Exception as e:
|
|
172
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
173
|
+
print(f"[[Tornado]] Failed to capture request body: {e}", log=False)
|
|
174
|
+
|
|
175
|
+
# -- 7) OTEL-STYLE: Pre-register endpoint and store endpoint_id
|
|
176
|
+
method_name = self.request.method.lower()
|
|
177
|
+
handler_fn = getattr(self, method_name, None)
|
|
178
|
+
|
|
179
|
+
if callable(handler_fn):
|
|
180
|
+
module = getattr(handler_fn, "__module__", "")
|
|
181
|
+
if not module.startswith("strawberry"):
|
|
182
|
+
real_fn = _unwrap_user_func(handler_fn)
|
|
183
|
+
code_obj = getattr(real_fn, "__code__", None)
|
|
184
|
+
if code_obj and _is_user_code(code_obj.co_filename):
|
|
185
|
+
hop_key = (code_obj.co_filename, code_obj.co_firstlineno)
|
|
186
|
+
|
|
187
|
+
# Extract route pattern from the handler's route_spec
|
|
188
|
+
route_pattern = None
|
|
189
|
+
try:
|
|
190
|
+
# Tornado stores the route pattern in the request's path_kwargs
|
|
191
|
+
# We can get the pattern from the application's handlers
|
|
192
|
+
if hasattr(self, "application") and hasattr(
|
|
193
|
+
self.application, "handlers"
|
|
194
|
+
):
|
|
195
|
+
# Find the matching route spec
|
|
196
|
+
for host_pattern, handlers in self.application.handlers:
|
|
197
|
+
for spec in handlers:
|
|
198
|
+
# spec is a URLSpec with regex, handler_class, kwargs, name
|
|
199
|
+
if spec.handler_class == type(self):
|
|
200
|
+
route_pattern = spec.regex.pattern
|
|
201
|
+
break
|
|
202
|
+
if route_pattern:
|
|
203
|
+
break
|
|
204
|
+
except Exception:
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
# Check if route should be skipped
|
|
208
|
+
if should_skip_route(
|
|
209
|
+
route_pattern or self.request.path, _ROUTES_TO_SKIP
|
|
210
|
+
):
|
|
211
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
212
|
+
print(
|
|
213
|
+
f"[[Tornado]] Skipping endpoint (route matches skip pattern): {route_pattern or self.request.path}",
|
|
214
|
+
log=False,
|
|
215
|
+
)
|
|
216
|
+
return original_prepare(self, *args, **kwargs)
|
|
217
|
+
|
|
218
|
+
# Pre-register endpoint if not already registered
|
|
219
|
+
endpoint_id = _ENDPOINT_REGISTRY.get(hop_key)
|
|
220
|
+
if endpoint_id is None:
|
|
221
|
+
endpoint_id = register_endpoint(
|
|
222
|
+
line=str(code_obj.co_firstlineno),
|
|
223
|
+
column="0",
|
|
224
|
+
name=real_fn.__name__,
|
|
225
|
+
entrypoint=code_obj.co_filename,
|
|
226
|
+
route=route_pattern,
|
|
227
|
+
)
|
|
228
|
+
if endpoint_id >= 0:
|
|
229
|
+
_ENDPOINT_REGISTRY[hop_key] = endpoint_id
|
|
230
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
231
|
+
print(
|
|
232
|
+
f"[[Tornado]] Registered endpoint: {real_fn.__name__} @ "
|
|
233
|
+
f"{code_obj.co_filename}:{code_obj.co_firstlineno} (id={endpoint_id})",
|
|
234
|
+
log=False,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Store endpoint_id for on_finish() to emit
|
|
238
|
+
self._sf_endpoint_id = endpoint_id
|
|
239
|
+
|
|
240
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
241
|
+
print(
|
|
242
|
+
f"[[Tornado]] Captured endpoint: {real_fn.__name__} "
|
|
243
|
+
f"({code_obj.co_filename}:{code_obj.co_firstlineno}) endpoint_id={endpoint_id}",
|
|
244
|
+
log=False,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return original_prepare(self, *args, **kwargs)
|
|
248
|
+
|
|
249
|
+
tornado.web.RequestHandler.prepare = patched_prepare
|
|
250
|
+
|
|
251
|
+
# --------------------------------------------------------------- #
|
|
252
|
+
# b) Exception capture – patch _execute and write_error
|
|
253
|
+
# --------------------------------------------------------------- #
|
|
254
|
+
original_execute = tornado.web.RequestHandler._execute
|
|
255
|
+
original_write_error = tornado.web.RequestHandler.write_error
|
|
256
|
+
|
|
257
|
+
async def patched_execute(self, *args, **kwargs):
|
|
258
|
+
try:
|
|
259
|
+
return await original_execute(self, *args, **kwargs)
|
|
260
|
+
except Exception as exc: # HTTPError included
|
|
261
|
+
custom_excepthook(type(exc), exc, exc.__traceback__)
|
|
262
|
+
raise # let Tornado handle 500/4xx
|
|
263
|
+
|
|
264
|
+
def patched_write_error(self, status_code, **kwargs):
|
|
265
|
+
"""
|
|
266
|
+
Tornado calls write_error for HTTPError and uncaught exceptions.
|
|
267
|
+
Capture the exception (when provided) before rendering.
|
|
268
|
+
"""
|
|
269
|
+
exc_info = kwargs.get("exc_info")
|
|
270
|
+
if exc_info and isinstance(exc_info, tuple) and exc_info[1]:
|
|
271
|
+
exc_type, exc_val, exc_tb = exc_info
|
|
272
|
+
custom_excepthook(exc_type, exc_val, exc_tb)
|
|
273
|
+
# Fallback: still call original renderer
|
|
274
|
+
return original_write_error(self, status_code, **kwargs)
|
|
275
|
+
|
|
276
|
+
tornado.web.RequestHandler._execute = patched_execute
|
|
277
|
+
tornado.web.RequestHandler.write_error = patched_write_error
|
|
278
|
+
|
|
279
|
+
# --------------------------------------------------------------- #
|
|
280
|
+
# c) CORS patching – inject Sailfish headers
|
|
281
|
+
# --------------------------------------------------------------- #
|
|
282
|
+
patch_tornado_cors()
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def patch_tornado_cors():
|
|
286
|
+
"""
|
|
287
|
+
Patch Tornado's RequestHandler to automatically inject Sailfish headers into CORS.
|
|
288
|
+
|
|
289
|
+
SAFE: Only modifies Access-Control-Allow-Headers if the handler sets it.
|
|
290
|
+
Tornado doesn't have a standard CORS library, so we patch the common patterns:
|
|
291
|
+
1. set_default_headers() - called for every request
|
|
292
|
+
2. options() - called for preflight requests
|
|
293
|
+
"""
|
|
294
|
+
try:
|
|
295
|
+
import tornado.web
|
|
296
|
+
except ImportError:
|
|
297
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
298
|
+
print("[[patch_tornado_cors]] Tornado not available, skipping", log=False)
|
|
299
|
+
return
|
|
300
|
+
|
|
301
|
+
# Check if already patched
|
|
302
|
+
if hasattr(tornado.web.RequestHandler, "_sf_cors_patched"):
|
|
303
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
304
|
+
print("[[patch_tornado_cors]] Already patched, skipping", log=False)
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
# Patch set_default_headers to intercept and modify Access-Control-Allow-Headers
|
|
308
|
+
original_set_header = tornado.web.RequestHandler.set_header
|
|
309
|
+
|
|
310
|
+
def patched_set_header(self, name, value):
|
|
311
|
+
# Intercept Access-Control-Allow-Headers header
|
|
312
|
+
if name.lower() == "access-control-allow-headers":
|
|
313
|
+
if should_inject_headers(value):
|
|
314
|
+
injected = inject_sailfish_headers(value)
|
|
315
|
+
# Convert list back to comma-separated string for Tornado
|
|
316
|
+
# (inject_sailfish_headers returns a list, but Tornado expects a string)
|
|
317
|
+
if isinstance(injected, list):
|
|
318
|
+
value = ", ".join(injected)
|
|
319
|
+
else:
|
|
320
|
+
value = injected
|
|
321
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
322
|
+
print(
|
|
323
|
+
f"[[patch_tornado_cors]] Injected Sailfish headers: {value}",
|
|
324
|
+
log=False,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# Call original set_header
|
|
328
|
+
return original_set_header(self, name, value)
|
|
329
|
+
|
|
330
|
+
tornado.web.RequestHandler.set_header = patched_set_header
|
|
331
|
+
tornado.web.RequestHandler._sf_cors_patched = True
|
|
332
|
+
|
|
333
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
334
|
+
print(
|
|
335
|
+
"[[patch_tornado_cors]] Successfully patched Tornado RequestHandler.set_header",
|
|
336
|
+
log=False,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# Hook into on_finish for OTEL-style post-response emission
|
|
341
|
+
try:
|
|
342
|
+
import tornado.web
|
|
343
|
+
|
|
344
|
+
original_on_finish = tornado.web.RequestHandler.on_finish
|
|
345
|
+
|
|
346
|
+
def patched_on_finish(self):
|
|
347
|
+
"""
|
|
348
|
+
OTEL-STYLE: Emit network hop AFTER response is sent.
|
|
349
|
+
Tornado calls on_finish() after the response is fully sent to client.
|
|
350
|
+
"""
|
|
351
|
+
# Emit network hop if we captured endpoint_id
|
|
352
|
+
endpoint_id = getattr(self, "_sf_endpoint_id", None)
|
|
353
|
+
if endpoint_id is not None and endpoint_id >= 0:
|
|
354
|
+
try:
|
|
355
|
+
_, session_id = get_or_set_sf_trace_id()
|
|
356
|
+
|
|
357
|
+
# Get captured request data
|
|
358
|
+
req_headers = getattr(self, "_sf_request_headers", None)
|
|
359
|
+
req_body = getattr(self, "_sf_request_body", None)
|
|
360
|
+
|
|
361
|
+
# Capture response headers if enabled
|
|
362
|
+
resp_headers = None
|
|
363
|
+
if SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS:
|
|
364
|
+
try:
|
|
365
|
+
resp_headers = dict(self._headers)
|
|
366
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
367
|
+
print(
|
|
368
|
+
f"[[Tornado]] Captured response headers: {len(resp_headers)} headers",
|
|
369
|
+
log=False,
|
|
370
|
+
)
|
|
371
|
+
except Exception as e:
|
|
372
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
373
|
+
print(
|
|
374
|
+
f"[[Tornado]] Failed to capture response headers: {e}",
|
|
375
|
+
log=False,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# Capture response body if enabled
|
|
379
|
+
resp_body = None
|
|
380
|
+
if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY:
|
|
381
|
+
try:
|
|
382
|
+
# Tornado stores written chunks in self._write_buffer
|
|
383
|
+
if hasattr(self, "_write_buffer") and self._write_buffer:
|
|
384
|
+
body_parts = [
|
|
385
|
+
chunk
|
|
386
|
+
for chunk in self._write_buffer
|
|
387
|
+
if isinstance(chunk, bytes)
|
|
388
|
+
]
|
|
389
|
+
if body_parts:
|
|
390
|
+
full_body = b"".join(body_parts)
|
|
391
|
+
resp_body = full_body[:_RESPONSE_LIMIT_BYTES]
|
|
392
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
393
|
+
print(
|
|
394
|
+
f"[[Tornado]] Captured response body: {len(resp_body)} bytes",
|
|
395
|
+
log=False,
|
|
396
|
+
)
|
|
397
|
+
except Exception as e:
|
|
398
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
399
|
+
print(
|
|
400
|
+
f"[[Tornado]] Failed to capture response body: {e}",
|
|
401
|
+
log=False,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# Extract raw path and query string for C to parse
|
|
405
|
+
raw_path = self.request.path # e.g., "/log"
|
|
406
|
+
# Tornado's request.query is the query string without '?'
|
|
407
|
+
raw_query = (
|
|
408
|
+
self.request.query.encode("utf-8") if self.request.query else b""
|
|
409
|
+
) # e.g., b"foo=5"
|
|
410
|
+
|
|
411
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
412
|
+
print(
|
|
413
|
+
f"[[Tornado]] About to emit network hop: endpoint_id={endpoint_id}, "
|
|
414
|
+
f"req_headers={'present' if req_headers else 'None'}, "
|
|
415
|
+
f"req_body={len(req_body) if req_body else 0} bytes, "
|
|
416
|
+
f"resp_headers={'present' if resp_headers else 'None'}, "
|
|
417
|
+
f"resp_body={len(resp_body) if resp_body else 0} bytes",
|
|
418
|
+
log=False,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
# Direct C call - queues to background worker, returns instantly
|
|
422
|
+
# C will parse route and query_params from raw data
|
|
423
|
+
fast_send_network_hop_fast(
|
|
424
|
+
session_id=session_id,
|
|
425
|
+
endpoint_id=endpoint_id,
|
|
426
|
+
raw_path=raw_path,
|
|
427
|
+
raw_query_string=raw_query,
|
|
428
|
+
request_headers=req_headers,
|
|
429
|
+
request_body=req_body,
|
|
430
|
+
response_headers=resp_headers,
|
|
431
|
+
response_body=resp_body,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
435
|
+
print(
|
|
436
|
+
f"[[Tornado]] Emitted network hop: endpoint_id={endpoint_id} "
|
|
437
|
+
f"session={session_id}",
|
|
438
|
+
log=False,
|
|
439
|
+
)
|
|
440
|
+
except Exception as e: # noqa: BLE001 S110
|
|
441
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
442
|
+
print(f"[[Tornado]] Failed to emit network hop: {e}", log=False)
|
|
443
|
+
|
|
444
|
+
# Clear function span override for this request (ContextVar cleanup)
|
|
445
|
+
try:
|
|
446
|
+
clear_funcspan_override()
|
|
447
|
+
except Exception:
|
|
448
|
+
pass
|
|
449
|
+
|
|
450
|
+
# CRITICAL: Clear C TLS to prevent stale data in thread pools
|
|
451
|
+
try:
|
|
452
|
+
clear_c_tls_parent_trace_id()
|
|
453
|
+
except Exception:
|
|
454
|
+
pass
|
|
455
|
+
|
|
456
|
+
# CRITICAL: Clear outbound header base to prevent stale cached headers
|
|
457
|
+
# ContextVar does NOT automatically clean up in thread pools - must clear explicitly
|
|
458
|
+
try:
|
|
459
|
+
clear_outbound_header_base()
|
|
460
|
+
except Exception:
|
|
461
|
+
pass
|
|
462
|
+
|
|
463
|
+
# CRITICAL: Clear trace_id to ensure fresh generation for next request
|
|
464
|
+
# Without this, get_or_set_sf_trace_id() reuses trace_id from previous request
|
|
465
|
+
# causing X-Sf4-Prid to stay constant when no incoming X-Sf3-Rid header
|
|
466
|
+
try:
|
|
467
|
+
clear_trace_id()
|
|
468
|
+
except Exception:
|
|
469
|
+
pass
|
|
470
|
+
|
|
471
|
+
# CRITICAL: Clear current request path to prevent stale data in thread pools
|
|
472
|
+
try:
|
|
473
|
+
clear_current_request_path()
|
|
474
|
+
except Exception:
|
|
475
|
+
pass
|
|
476
|
+
|
|
477
|
+
return original_on_finish(self)
|
|
478
|
+
|
|
479
|
+
tornado.web.RequestHandler.on_finish = patched_on_finish
|
|
480
|
+
except ImportError:
|
|
481
|
+
pass
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import fnmatch
|
|
2
|
+
import inspect
|
|
3
|
+
import sysconfig
|
|
4
|
+
from typing import Any, Callable, List, Optional, Set
|
|
5
|
+
|
|
6
|
+
_stdlib = sysconfig.get_paths()["stdlib"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_ATTR_CANDIDATES = (
|
|
10
|
+
"resolver",
|
|
11
|
+
"func",
|
|
12
|
+
"python_func",
|
|
13
|
+
"_resolver",
|
|
14
|
+
"wrapped_func",
|
|
15
|
+
"__func",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _is_user_code(path: Optional[str] = None) -> bool:
|
|
20
|
+
return (
|
|
21
|
+
bool(path)
|
|
22
|
+
and not path.startswith(_stdlib)
|
|
23
|
+
and "site-packages" not in path
|
|
24
|
+
and "dist-packages" not in path
|
|
25
|
+
and not path.startswith("<")
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _unwrap_user_func(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
30
|
+
"""Unwrap decorators & closures until we find your user function."""
|
|
31
|
+
seen: Set[int] = set()
|
|
32
|
+
queue = [fn]
|
|
33
|
+
while queue:
|
|
34
|
+
current = queue.pop()
|
|
35
|
+
if id(current) in seen:
|
|
36
|
+
continue
|
|
37
|
+
seen.add(id(current))
|
|
38
|
+
|
|
39
|
+
if inspect.isfunction(current) and _is_user_code(current.__code__.co_filename):
|
|
40
|
+
return current
|
|
41
|
+
|
|
42
|
+
inner = getattr(current, "__wrapped__", None)
|
|
43
|
+
if inner:
|
|
44
|
+
queue.append(inner)
|
|
45
|
+
|
|
46
|
+
for attr in _ATTR_CANDIDATES:
|
|
47
|
+
attr_val = getattr(current, attr, None)
|
|
48
|
+
if inspect.isfunction(attr_val):
|
|
49
|
+
queue.append(attr_val)
|
|
50
|
+
|
|
51
|
+
for cell in getattr(current, "__closure__", []) or []:
|
|
52
|
+
cc = cell.cell_contents
|
|
53
|
+
if inspect.isfunction(cc):
|
|
54
|
+
queue.append(cc)
|
|
55
|
+
|
|
56
|
+
return fn # fallback
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def should_skip_route(route_pattern: str, routes_to_skip: List[str]) -> bool:
|
|
60
|
+
"""
|
|
61
|
+
Check if route should be skipped based on wildcard patterns.
|
|
62
|
+
|
|
63
|
+
Supports Unix shell-style wildcards:
|
|
64
|
+
- Exact match: "/healthz" matches "/healthz"
|
|
65
|
+
- Wildcard *: "/he*" matches "/health", "/healthz", "/healthz/foo"
|
|
66
|
+
- Wildcard ?: "/health?" matches "/healthz" but not "/health"
|
|
67
|
+
- Character sets: "/health[z12]" matches "/healthz", "/health1", "/health2"
|
|
68
|
+
|
|
69
|
+
Examples:
|
|
70
|
+
- "/he*" → matches "/health", "/healthz", "/healthz/foo"
|
|
71
|
+
- "/metrics*" → matches "/metrics", "/metrics/detailed"
|
|
72
|
+
- "/api/internal/*" → matches "/api/internal/status", "/api/internal/debug"
|
|
73
|
+
- "*/admin" → matches "/foo/admin", "/bar/admin"
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
route_pattern: Route pattern to check (e.g., "/healthz", "/log/{n}")
|
|
77
|
+
routes_to_skip: List of patterns to skip (can contain wildcards)
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
True if route should be skipped, False otherwise
|
|
81
|
+
"""
|
|
82
|
+
if not routes_to_skip or not route_pattern:
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
for skip_pattern in routes_to_skip:
|
|
86
|
+
# Use fnmatch for Unix shell-style wildcards
|
|
87
|
+
# This supports * (matches anything) and ? (matches single char)
|
|
88
|
+
if fnmatch.fnmatch(route_pattern, skip_pattern):
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
return False
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import builtins
|
|
2
|
+
|
|
3
|
+
from .custom_print import SF_DEBUG, custom_print
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def override_print():
|
|
7
|
+
if hasattr(builtins, "_original_print"):
|
|
8
|
+
return
|
|
9
|
+
# Save the original print function
|
|
10
|
+
builtins._original_print = builtins.print
|
|
11
|
+
|
|
12
|
+
# Override the built-in print function
|
|
13
|
+
builtins.print = custom_print
|