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,520 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Adds:
|
|
3
|
+
• before_request hook → ContextVar propagation (unchanged).
|
|
4
|
+
• global add_url_rule / Blueprint.add_url_rule patch →
|
|
5
|
+
wraps every endpoint in a hop-emitting closure.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from functools import wraps
|
|
9
|
+
from types import MethodType
|
|
10
|
+
from typing import Callable, List, Optional, Set, Tuple
|
|
11
|
+
|
|
12
|
+
from ... import _sffuncspan_config, app_config
|
|
13
|
+
from ...constants import (
|
|
14
|
+
FUNCSPAN_OVERRIDE_HEADER_BYTES,
|
|
15
|
+
SAILFISH_TRACING_HEADER,
|
|
16
|
+
SAILFISH_TRACING_HEADER_BYTES,
|
|
17
|
+
)
|
|
18
|
+
from ...custom_excepthook import custom_excepthook
|
|
19
|
+
from ...env_vars import (
|
|
20
|
+
SF_DEBUG,
|
|
21
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_BODY,
|
|
22
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS,
|
|
23
|
+
SF_NETWORKHOP_CAPTURE_RESPONSE_BODY,
|
|
24
|
+
SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS,
|
|
25
|
+
SF_NETWORKHOP_REQUEST_LIMIT_MB,
|
|
26
|
+
SF_NETWORKHOP_RESPONSE_LIMIT_MB,
|
|
27
|
+
)
|
|
28
|
+
from ...fast_network_hop import fast_send_network_hop_fast, register_endpoint
|
|
29
|
+
from ...thread_local import (
|
|
30
|
+
clear_c_tls_parent_trace_id,
|
|
31
|
+
clear_current_request_path,
|
|
32
|
+
clear_outbound_header_base,
|
|
33
|
+
clear_trace_id,
|
|
34
|
+
generate_new_trace_id,
|
|
35
|
+
get_or_set_sf_trace_id,
|
|
36
|
+
get_sf_trace_id,
|
|
37
|
+
set_current_request_path,
|
|
38
|
+
set_funcspan_override,
|
|
39
|
+
set_outbound_header_base,
|
|
40
|
+
)
|
|
41
|
+
from .cors_utils import inject_sailfish_headers, should_inject_headers
|
|
42
|
+
from .utils import should_skip_route
|
|
43
|
+
|
|
44
|
+
# Size limits in bytes
|
|
45
|
+
_REQUEST_LIMIT_BYTES = SF_NETWORKHOP_REQUEST_LIMIT_MB * 1024 * 1024
|
|
46
|
+
_RESPONSE_LIMIT_BYTES = SF_NETWORKHOP_RESPONSE_LIMIT_MB * 1024 * 1024
|
|
47
|
+
|
|
48
|
+
# Pre-registered endpoint IDs (filename:line -> endpoint_id)
|
|
49
|
+
_ENDPOINT_REGISTRY: dict[tuple, int] = {}
|
|
50
|
+
|
|
51
|
+
# Module-level variable for routes to skip (set by patch_flask)
|
|
52
|
+
_ROUTES_TO_SKIP = []
|
|
53
|
+
|
|
54
|
+
# ────────────────────────────────────────────────────────────────
|
|
55
|
+
# shared helpers
|
|
56
|
+
# ────────────────────────────────────────────────────────────────
|
|
57
|
+
from .utils import _is_user_code, _unwrap_user_func # cached helpers
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _make_hop_wrapper(
|
|
61
|
+
fn: Callable,
|
|
62
|
+
hop_key: Tuple[str, int],
|
|
63
|
+
fn_name: str,
|
|
64
|
+
filename: str,
|
|
65
|
+
route: str = None,
|
|
66
|
+
):
|
|
67
|
+
"""
|
|
68
|
+
OTEL-STYLE: Store endpoint metadata in flask.g during request.
|
|
69
|
+
Emission happens in @after_request hook for zero-overhead.
|
|
70
|
+
Pre-register endpoint on first wrap.
|
|
71
|
+
"""
|
|
72
|
+
from flask import g
|
|
73
|
+
|
|
74
|
+
# Check if route should be skipped
|
|
75
|
+
if should_skip_route(route, _ROUTES_TO_SKIP):
|
|
76
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
77
|
+
print(
|
|
78
|
+
f"[[Flask]] Skipping endpoint (route matches skip pattern): {route}",
|
|
79
|
+
log=False,
|
|
80
|
+
)
|
|
81
|
+
# Return original function unwrapped - no telemetry
|
|
82
|
+
return fn
|
|
83
|
+
|
|
84
|
+
# Pre-register endpoint if not already registered
|
|
85
|
+
endpoint_id = _ENDPOINT_REGISTRY.get(hop_key)
|
|
86
|
+
if endpoint_id is None:
|
|
87
|
+
endpoint_id = register_endpoint(
|
|
88
|
+
line=str(hop_key[1]),
|
|
89
|
+
column="0",
|
|
90
|
+
name=fn_name,
|
|
91
|
+
entrypoint=filename,
|
|
92
|
+
route=route,
|
|
93
|
+
)
|
|
94
|
+
if endpoint_id >= 0:
|
|
95
|
+
_ENDPOINT_REGISTRY[hop_key] = endpoint_id
|
|
96
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
97
|
+
print(
|
|
98
|
+
f"[[Flask]] Registered endpoint: {fn_name} @ {filename}:{hop_key[1]} (id={endpoint_id})",
|
|
99
|
+
log=False,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
@wraps(fn)
|
|
103
|
+
def _wrapped(*args, **kwargs): # noqa: ANN001
|
|
104
|
+
# OTEL-STYLE: Store endpoint_id for after_request to emit
|
|
105
|
+
if not hasattr(g, "_sf_endpoint_id"):
|
|
106
|
+
g._sf_endpoint_id = endpoint_id
|
|
107
|
+
|
|
108
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
109
|
+
print(
|
|
110
|
+
f"[[Flask]] Captured endpoint: {fn_name} ({filename}:{hop_key[1]}) endpoint_id={endpoint_id}",
|
|
111
|
+
log=False,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return fn(*args, **kwargs)
|
|
115
|
+
|
|
116
|
+
return _wrapped
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _wrap_if_user_view(endpoint_fn: Callable, route: str = None):
|
|
120
|
+
"""
|
|
121
|
+
Decide whether to wrap `endpoint_fn`. Returns the (possibly wrapped)
|
|
122
|
+
callable. Suppress wrapping for library code or Strawberry handlers.
|
|
123
|
+
"""
|
|
124
|
+
real_fn = _unwrap_user_func(endpoint_fn)
|
|
125
|
+
|
|
126
|
+
# Skip Strawberry GraphQL views – Strawberry extension owns them
|
|
127
|
+
if real_fn.__module__.startswith("strawberry"):
|
|
128
|
+
return endpoint_fn
|
|
129
|
+
|
|
130
|
+
code = getattr(real_fn, "__code__", None)
|
|
131
|
+
if not code or not _is_user_code(code.co_filename):
|
|
132
|
+
return endpoint_fn
|
|
133
|
+
|
|
134
|
+
hop_key = (code.co_filename, code.co_firstlineno)
|
|
135
|
+
return _make_hop_wrapper(
|
|
136
|
+
endpoint_fn, hop_key, real_fn.__name__, code.co_filename, route=route
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ────────────────────────────────────────────────────────────────
|
|
141
|
+
# Request hooks: before (header capture) + after (OTEL-style emission)
|
|
142
|
+
# ────────────────────────────────────────────────────────────────
|
|
143
|
+
def _install_request_hooks(app):
|
|
144
|
+
from flask import g, request
|
|
145
|
+
|
|
146
|
+
@app.before_request
|
|
147
|
+
def _extract_sf_trace():
|
|
148
|
+
# Set current request path for route-based suppression (SF_DISABLE_INBOUND_NETWORK_TRACING_ON_ROUTES)
|
|
149
|
+
set_current_request_path(request.path)
|
|
150
|
+
|
|
151
|
+
# 1. CRITICAL: Seed/ensure trace_id immediately (BEFORE any outbound work)
|
|
152
|
+
hdr = request.headers.get(SAILFISH_TRACING_HEADER)
|
|
153
|
+
if hdr:
|
|
154
|
+
get_or_set_sf_trace_id(hdr, is_associated_with_inbound_request=True)
|
|
155
|
+
else:
|
|
156
|
+
# No incoming X-Sf3-Rid header - generate fresh trace_id for this request
|
|
157
|
+
generate_new_trace_id()
|
|
158
|
+
|
|
159
|
+
# Optional funcspan override (highest priority!)
|
|
160
|
+
funcspan_override_header = request.headers.get(
|
|
161
|
+
"X-Sf3-FunctionSpanCaptureOverride"
|
|
162
|
+
)
|
|
163
|
+
if funcspan_override_header:
|
|
164
|
+
try:
|
|
165
|
+
set_funcspan_override(funcspan_override_header)
|
|
166
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
167
|
+
print(
|
|
168
|
+
f"[[Flask.before_request]] Set function span override from header: {funcspan_override_header}",
|
|
169
|
+
log=False,
|
|
170
|
+
)
|
|
171
|
+
except Exception as e:
|
|
172
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
173
|
+
print(
|
|
174
|
+
f"[[Flask.before_request]] Failed to set function span override: {e}",
|
|
175
|
+
log=False,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Initialize outbound base without list/allocs from split()
|
|
179
|
+
try:
|
|
180
|
+
trace_id = get_sf_trace_id()
|
|
181
|
+
if trace_id:
|
|
182
|
+
s = str(trace_id)
|
|
183
|
+
i = s.find("/") # session
|
|
184
|
+
j = s.find("/", i + 1) if i != -1 else -1 # page
|
|
185
|
+
if j != -1:
|
|
186
|
+
base_trace = s[:j] # "session/page"
|
|
187
|
+
set_outbound_header_base(
|
|
188
|
+
base_trace=base_trace,
|
|
189
|
+
parent_trace_id=s, # "session/page/uuid"
|
|
190
|
+
funcspan=funcspan_override_header,
|
|
191
|
+
)
|
|
192
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
193
|
+
print(
|
|
194
|
+
f"[[Flask.before_request]] Initialized outbound header base (base={base_trace[:16]}...)",
|
|
195
|
+
log=False,
|
|
196
|
+
)
|
|
197
|
+
except Exception as e:
|
|
198
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
199
|
+
print(
|
|
200
|
+
f"[[Flask.before_request]] Failed to initialize outbound header base: {e}",
|
|
201
|
+
log=False,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
@app.after_request
|
|
205
|
+
def _emit_network_hop(response):
|
|
206
|
+
"""
|
|
207
|
+
OTEL-STYLE: Emit network hop AFTER response is built.
|
|
208
|
+
This ensures telemetry doesn't impact response time to client.
|
|
209
|
+
Captures request/response headers and bodies if enabled.
|
|
210
|
+
"""
|
|
211
|
+
endpoint_id = getattr(g, "_sf_endpoint_id", None)
|
|
212
|
+
if endpoint_id is not None and endpoint_id >= 0:
|
|
213
|
+
try:
|
|
214
|
+
_, session_id = get_or_set_sf_trace_id()
|
|
215
|
+
|
|
216
|
+
# Capture request headers if enabled
|
|
217
|
+
req_headers = None
|
|
218
|
+
if SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS:
|
|
219
|
+
try:
|
|
220
|
+
req_headers = dict(request.headers)
|
|
221
|
+
except Exception:
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
# Capture request body if enabled
|
|
225
|
+
req_body = None
|
|
226
|
+
if SF_NETWORKHOP_CAPTURE_REQUEST_BODY:
|
|
227
|
+
try:
|
|
228
|
+
# Flask caches request.data, so this is safe
|
|
229
|
+
body = request.get_data()
|
|
230
|
+
if body and len(body) > 0:
|
|
231
|
+
req_body = body[:_REQUEST_LIMIT_BYTES]
|
|
232
|
+
except Exception:
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
# Capture response headers if enabled
|
|
236
|
+
resp_headers = None
|
|
237
|
+
if SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS:
|
|
238
|
+
try:
|
|
239
|
+
resp_headers = dict(response.headers)
|
|
240
|
+
except Exception:
|
|
241
|
+
pass
|
|
242
|
+
|
|
243
|
+
# Capture response body if enabled
|
|
244
|
+
resp_body = None
|
|
245
|
+
if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY:
|
|
246
|
+
try:
|
|
247
|
+
if hasattr(response, "get_data"):
|
|
248
|
+
body = response.get_data()
|
|
249
|
+
if body and len(body) > 0:
|
|
250
|
+
resp_body = body[:_RESPONSE_LIMIT_BYTES]
|
|
251
|
+
except Exception:
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
# Extract raw path and query string for C to parse
|
|
255
|
+
raw_path = request.path # e.g., "/log"
|
|
256
|
+
raw_query = request.query_string # e.g., b"foo=5"
|
|
257
|
+
|
|
258
|
+
# Direct C call - queues to background worker, returns instantly
|
|
259
|
+
# C will parse route and query_params from raw data
|
|
260
|
+
fast_send_network_hop_fast(
|
|
261
|
+
session_id=session_id,
|
|
262
|
+
endpoint_id=endpoint_id,
|
|
263
|
+
raw_path=raw_path,
|
|
264
|
+
raw_query_string=raw_query,
|
|
265
|
+
request_headers=req_headers,
|
|
266
|
+
request_body=req_body,
|
|
267
|
+
response_headers=resp_headers,
|
|
268
|
+
response_body=resp_body,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
272
|
+
print(
|
|
273
|
+
f"[[Flask]] Emitted network hop: endpoint_id={endpoint_id} "
|
|
274
|
+
f"session={session_id}",
|
|
275
|
+
log=False,
|
|
276
|
+
)
|
|
277
|
+
except Exception as e: # noqa: BLE001 S110
|
|
278
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
279
|
+
print(f"[[Flask]] Failed to emit network hop: {e}", log=False)
|
|
280
|
+
|
|
281
|
+
# CRITICAL: Clear C TLS to prevent stale data in thread pools
|
|
282
|
+
clear_c_tls_parent_trace_id()
|
|
283
|
+
|
|
284
|
+
# CRITICAL: Clear outbound header base to prevent stale cached headers
|
|
285
|
+
# ContextVar does NOT automatically clean up in thread pools - must clear explicitly
|
|
286
|
+
clear_outbound_header_base()
|
|
287
|
+
|
|
288
|
+
# CRITICAL: Clear trace_id to ensure fresh generation for next request
|
|
289
|
+
# Without this, get_or_set_sf_trace_id() reuses trace_id from previous request
|
|
290
|
+
# causing X-Sf4-Prid to stay constant when no incoming X-Sf3-Rid header
|
|
291
|
+
clear_trace_id()
|
|
292
|
+
|
|
293
|
+
# CRITICAL: Clear current request path to prevent stale data in thread pools
|
|
294
|
+
clear_current_request_path()
|
|
295
|
+
|
|
296
|
+
# Clear function span override for this request (thread-local cleanup)
|
|
297
|
+
try:
|
|
298
|
+
_sffuncspan_config.clear_thread_override()
|
|
299
|
+
except Exception:
|
|
300
|
+
pass
|
|
301
|
+
|
|
302
|
+
return response
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ────────────────────────────────────────────────────────────────
|
|
306
|
+
# Monkey-patch Flask & Blueprint
|
|
307
|
+
# ────────────────────────────────────────────────────────────────
|
|
308
|
+
try:
|
|
309
|
+
import flask
|
|
310
|
+
from flask import Blueprint
|
|
311
|
+
|
|
312
|
+
def _patch_add_url_rule(cls):
|
|
313
|
+
"""
|
|
314
|
+
Patch *cls*.add_url_rule (cls is Flask or Blueprint) so the final
|
|
315
|
+
stored view function is wrapped after Flask registers it. Works for:
|
|
316
|
+
• view_func positional
|
|
317
|
+
• endpoint string lookup
|
|
318
|
+
• CBV's as_view()
|
|
319
|
+
"""
|
|
320
|
+
original_add = cls.add_url_rule
|
|
321
|
+
|
|
322
|
+
def patched_add(
|
|
323
|
+
self, rule, endpoint=None, view_func=None, **options
|
|
324
|
+
): # noqa: ANN001
|
|
325
|
+
# Resolve endpoint name
|
|
326
|
+
ep_name = endpoint or (view_func and view_func.__name__)
|
|
327
|
+
|
|
328
|
+
# Check if endpoint already registered
|
|
329
|
+
already_registered = ep_name and ep_name in self.view_functions
|
|
330
|
+
|
|
331
|
+
# If already registered, use existing function to avoid Flask's
|
|
332
|
+
# "overwriting endpoint" assertion when same function is used for multiple routes
|
|
333
|
+
if already_registered:
|
|
334
|
+
view_func = self.view_functions[ep_name]
|
|
335
|
+
|
|
336
|
+
# 1. Let Flask register the route
|
|
337
|
+
original_add(self, rule, endpoint=endpoint, view_func=view_func, **options)
|
|
338
|
+
|
|
339
|
+
# 2. Wrap only if this is a new endpoint registration
|
|
340
|
+
if already_registered:
|
|
341
|
+
return # Already wrapped during first registration
|
|
342
|
+
|
|
343
|
+
# This is a new endpoint, wrap it
|
|
344
|
+
if not ep_name:
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
target = self.view_functions.get(ep_name)
|
|
348
|
+
if not callable(target):
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
# 3. Wrap if user code (pass route pattern)
|
|
352
|
+
wrapped = _wrap_if_user_view(target, route=rule)
|
|
353
|
+
self.view_functions[ep_name] = wrapped
|
|
354
|
+
|
|
355
|
+
cls.add_url_rule = patched_add
|
|
356
|
+
|
|
357
|
+
def patch_flask(routes_to_skip: Optional[List[str]] = None):
|
|
358
|
+
"""
|
|
359
|
+
OTEL-STYLE PURE ASYNC:
|
|
360
|
+
• Installs before_request header propagation
|
|
361
|
+
• Installs after_request for OTEL-style network hop emission
|
|
362
|
+
• Wraps every endpoint to capture metadata (not emit)
|
|
363
|
+
• Patches exception handlers for custom_excepthook
|
|
364
|
+
"""
|
|
365
|
+
global _ROUTES_TO_SKIP
|
|
366
|
+
_ROUTES_TO_SKIP = routes_to_skip or []
|
|
367
|
+
|
|
368
|
+
if getattr(flask.Flask, "__sf_tracing_patched__", False):
|
|
369
|
+
return # idempotent
|
|
370
|
+
|
|
371
|
+
# --- 1. Patch Flask.__init__ to add request hooks -----------
|
|
372
|
+
original_flask_init = flask.Flask.__init__
|
|
373
|
+
|
|
374
|
+
def patched_init(self, *args, **kwargs):
|
|
375
|
+
original_flask_init(self, *args, **kwargs)
|
|
376
|
+
_install_request_hooks(self)
|
|
377
|
+
|
|
378
|
+
flask.Flask.__init__ = patched_init
|
|
379
|
+
|
|
380
|
+
# --- 2. Patch add_url_rule for both Flask and Blueprint -----------------
|
|
381
|
+
_patch_add_url_rule(flask.Flask)
|
|
382
|
+
_patch_add_url_rule(Blueprint)
|
|
383
|
+
|
|
384
|
+
# --- 3. Patch exception handlers once on the class ----------------------
|
|
385
|
+
_mw_path = "sf_veritas_exception_patch_applied"
|
|
386
|
+
if not getattr(flask.Flask, _mw_path, False):
|
|
387
|
+
orig_handle_exc = flask.Flask.handle_exception
|
|
388
|
+
orig_handle_user_exc = flask.Flask.handle_user_exception
|
|
389
|
+
|
|
390
|
+
def _patched_handle_exception(self, e):
|
|
391
|
+
custom_excepthook(type(e), e, e.__traceback__)
|
|
392
|
+
return orig_handle_exc(self, e)
|
|
393
|
+
|
|
394
|
+
def _patched_handle_user_exception(self, e):
|
|
395
|
+
custom_excepthook(type(e), e, e.__traceback__)
|
|
396
|
+
return orig_handle_user_exc(self, e)
|
|
397
|
+
|
|
398
|
+
flask.Flask.handle_exception = _patched_handle_exception # 500 errors
|
|
399
|
+
flask.Flask.handle_user_exception = (
|
|
400
|
+
_patched_handle_user_exception # HTTPExc.
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
setattr(flask.Flask, _mw_path, True)
|
|
404
|
+
|
|
405
|
+
flask.Flask.__sf_tracing_patched__ = True
|
|
406
|
+
|
|
407
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
408
|
+
print(
|
|
409
|
+
"[[patch_flask]] tracing hooks + exception capture installed", log=False
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
def patch_flask_cors():
|
|
413
|
+
"""
|
|
414
|
+
Patch flask-cors to automatically inject Sailfish headers into CORS allow_headers.
|
|
415
|
+
|
|
416
|
+
Patches:
|
|
417
|
+
1. CORS class __init__ method to intercept allow_headers parameter
|
|
418
|
+
2. CORS class init_app method for factory pattern support
|
|
419
|
+
3. cross_origin decorator for route-specific CORS
|
|
420
|
+
|
|
421
|
+
SAFE: Only modifies CORS if flask-cors is installed and used.
|
|
422
|
+
Reference: https://corydolphin.com/flask-cors/extension/
|
|
423
|
+
"""
|
|
424
|
+
# Import guard: only patch if flask-cors is installed
|
|
425
|
+
try:
|
|
426
|
+
from flask_cors import CORS, cross_origin
|
|
427
|
+
except ImportError:
|
|
428
|
+
# flask-cors not installed, skip patching
|
|
429
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
430
|
+
print(
|
|
431
|
+
"[[patch_flask_cors]] flask-cors not installed, skipping", log=False
|
|
432
|
+
)
|
|
433
|
+
return
|
|
434
|
+
|
|
435
|
+
# Check if already patched to avoid double-patching
|
|
436
|
+
if hasattr(CORS, "_sf_cors_patched"):
|
|
437
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
438
|
+
print("[[patch_flask_cors]] Already patched, skipping", log=False)
|
|
439
|
+
return
|
|
440
|
+
|
|
441
|
+
# Save original methods
|
|
442
|
+
original_cors_init = CORS.__init__
|
|
443
|
+
original_init_app = getattr(CORS, 'init_app', None)
|
|
444
|
+
|
|
445
|
+
# Patch CORS.__init__ (handles: CORS(app, allow_headers=["foo"]))
|
|
446
|
+
def patched_cors_init(self, app=None, **kwargs):
|
|
447
|
+
# Intercept and inject Sailfish headers into allow_headers
|
|
448
|
+
if "allow_headers" in kwargs:
|
|
449
|
+
original_headers = kwargs["allow_headers"]
|
|
450
|
+
if should_inject_headers(original_headers):
|
|
451
|
+
injected_headers = inject_sailfish_headers(original_headers)
|
|
452
|
+
kwargs["allow_headers"] = injected_headers
|
|
453
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
454
|
+
print(
|
|
455
|
+
f"[[patch_flask_cors]] Injected Sailfish headers into CORS.__init__: {injected_headers}",
|
|
456
|
+
log=False,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
# Call original init
|
|
460
|
+
original_cors_init(self, app, **kwargs)
|
|
461
|
+
|
|
462
|
+
# Patch CORS.init_app (handles factory pattern: cors = CORS(); cors.init_app(app, allow_headers=["foo"]))
|
|
463
|
+
def patched_init_app(self, app, **kwargs):
|
|
464
|
+
# Intercept and inject Sailfish headers into allow_headers
|
|
465
|
+
if "allow_headers" in kwargs:
|
|
466
|
+
original_headers = kwargs["allow_headers"]
|
|
467
|
+
if should_inject_headers(original_headers):
|
|
468
|
+
injected_headers = inject_sailfish_headers(original_headers)
|
|
469
|
+
kwargs["allow_headers"] = injected_headers
|
|
470
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
471
|
+
print(
|
|
472
|
+
f"[[patch_flask_cors]] Injected Sailfish headers into CORS.init_app: {injected_headers}",
|
|
473
|
+
log=False,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
# Call original init_app
|
|
477
|
+
if original_init_app:
|
|
478
|
+
original_init_app(self, app, **kwargs)
|
|
479
|
+
|
|
480
|
+
# Apply patches to CORS class
|
|
481
|
+
CORS.__init__ = patched_cors_init
|
|
482
|
+
if original_init_app:
|
|
483
|
+
CORS.init_app = patched_init_app
|
|
484
|
+
CORS._sf_cors_patched = True
|
|
485
|
+
|
|
486
|
+
# Patch cross_origin decorator (handles: @cross_origin(allow_headers=["foo"]))
|
|
487
|
+
original_cross_origin = cross_origin
|
|
488
|
+
|
|
489
|
+
def patched_cross_origin(*args, **kwargs):
|
|
490
|
+
# Intercept and inject Sailfish headers into allow_headers
|
|
491
|
+
if "allow_headers" in kwargs:
|
|
492
|
+
original_headers = kwargs["allow_headers"]
|
|
493
|
+
if should_inject_headers(original_headers):
|
|
494
|
+
injected_headers = inject_sailfish_headers(original_headers)
|
|
495
|
+
kwargs["allow_headers"] = injected_headers
|
|
496
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
497
|
+
print(
|
|
498
|
+
f"[[patch_flask_cors]] Injected Sailfish headers into @cross_origin: {injected_headers}",
|
|
499
|
+
log=False,
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
# Call original decorator
|
|
503
|
+
return original_cross_origin(*args, **kwargs)
|
|
504
|
+
|
|
505
|
+
# Replace the cross_origin function in the flask_cors module
|
|
506
|
+
import flask_cors
|
|
507
|
+
|
|
508
|
+
flask_cors.cross_origin = patched_cross_origin
|
|
509
|
+
|
|
510
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
511
|
+
print("[[patch_flask_cors]] Successfully patched flask-cors (CORS, init_app, cross_origin)", log=False)
|
|
512
|
+
|
|
513
|
+
# Apply CORS patching when module is loaded
|
|
514
|
+
patch_flask_cors()
|
|
515
|
+
|
|
516
|
+
except ImportError: # Flask not installed
|
|
517
|
+
|
|
518
|
+
def patch_flask(routes_to_skip: Optional[List[str]] = None): # noqa: D401
|
|
519
|
+
"""No-op when Flask is absent."""
|
|
520
|
+
return
|