sf-veritas 0.11.10__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.
- sf_veritas/__init__.py +46 -0
- sf_veritas/_auto_preload.py +73 -0
- sf_veritas/_sfconfig.c +162 -0
- sf_veritas/_sfconfig.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfcrashhandler.c +267 -0
- sf_veritas/_sfcrashhandler.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastlog.c +953 -0
- sf_veritas/_sffastlog.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnet.c +994 -0
- sf_veritas/_sffastnet.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnetworkrequest.c +727 -0
- sf_veritas/_sffastnetworkrequest.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan.c +2791 -0
- sf_veritas/_sffuncspan.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan_config.c +730 -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 +1454 -0
- sf_veritas/_sfnetworkhop.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfservice.c +1223 -0
- sf_veritas/_sfservice.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfteepreload.c +6227 -0
- sf_veritas/app_config.py +57 -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 +146 -0
- sf_veritas/custom_output_wrapper.py +153 -0
- sf_veritas/custom_print.py +153 -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 +693 -0
- sf_veritas/function_span_profiler.py +1313 -0
- sf_veritas/get_preload_path.py +34 -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 +543 -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/_patch_tracker.py +74 -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 +99 -0
- sf_veritas/patches/network_libraries/aiohttp.py +294 -0
- sf_veritas/patches/network_libraries/curl_cffi.py +363 -0
- sf_veritas/patches/network_libraries/http_client.py +670 -0
- sf_veritas/patches/network_libraries/httpcore.py +580 -0
- sf_veritas/patches/network_libraries/httplib2.py +315 -0
- sf_veritas/patches/network_libraries/httpx.py +557 -0
- sf_veritas/patches/network_libraries/niquests.py +218 -0
- sf_veritas/patches/network_libraries/pycurl.py +399 -0
- sf_veritas/patches/network_libraries/requests.py +595 -0
- sf_veritas/patches/network_libraries/ssl_socket.py +822 -0
- sf_veritas/patches/network_libraries/tornado.py +360 -0
- sf_veritas/patches/network_libraries/treq.py +270 -0
- sf_veritas/patches/network_libraries/urllib_request.py +483 -0
- sf_veritas/patches/network_libraries/utils.py +598 -0
- sf_veritas/patches/os.py +17 -0
- sf_veritas/patches/threading.py +231 -0
- sf_veritas/patches/web_frameworks/__init__.py +54 -0
- sf_veritas/patches/web_frameworks/aiohttp.py +798 -0
- sf_veritas/patches/web_frameworks/async_websocket_consumer.py +337 -0
- sf_veritas/patches/web_frameworks/blacksheep.py +532 -0
- sf_veritas/patches/web_frameworks/bottle.py +513 -0
- sf_veritas/patches/web_frameworks/cherrypy.py +683 -0
- sf_veritas/patches/web_frameworks/cors_utils.py +122 -0
- sf_veritas/patches/web_frameworks/django.py +963 -0
- sf_veritas/patches/web_frameworks/eve.py +401 -0
- sf_veritas/patches/web_frameworks/falcon.py +931 -0
- sf_veritas/patches/web_frameworks/fastapi.py +738 -0
- sf_veritas/patches/web_frameworks/flask.py +526 -0
- sf_veritas/patches/web_frameworks/klein.py +501 -0
- sf_veritas/patches/web_frameworks/litestar.py +616 -0
- sf_veritas/patches/web_frameworks/pyramid.py +440 -0
- sf_veritas/patches/web_frameworks/quart.py +841 -0
- sf_veritas/patches/web_frameworks/robyn.py +708 -0
- sf_veritas/patches/web_frameworks/sanic.py +874 -0
- sf_veritas/patches/web_frameworks/starlette.py +742 -0
- sf_veritas/patches/web_frameworks/strawberry.py +1446 -0
- sf_veritas/patches/web_frameworks/tornado.py +485 -0
- sf_veritas/patches/web_frameworks/utils.py +170 -0
- sf_veritas/print_override.py +13 -0
- sf_veritas/regular_data_transmitter.py +444 -0
- sf_veritas/request_interceptor.py +401 -0
- sf_veritas/request_utils.py +550 -0
- sf_veritas/segfault_handler.py +116 -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 +1319 -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 +1678 -0
- sf_veritas/utils.py +39 -0
- sf_veritas-0.11.10.dist-info/METADATA +97 -0
- sf_veritas-0.11.10.dist-info/RECORD +141 -0
- sf_veritas-0.11.10.dist-info/WHEEL +5 -0
- sf_veritas-0.11.10.dist-info/entry_points.txt +2 -0
- sf_veritas-0.11.10.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,841 @@
|
|
|
1
|
+
"""
|
|
2
|
+
• SFTracingQuartASGIMiddleware: pulls SAILFISH_TRACING_HEADER into your ContextVar.
|
|
3
|
+
• patch_quart(): wraps Quart.__init__, installs middleware and
|
|
4
|
+
redefines .route so that each user-land view emits one NetworkHop.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import inspect
|
|
9
|
+
import os
|
|
10
|
+
import sysconfig
|
|
11
|
+
import threading
|
|
12
|
+
from functools import lru_cache, wraps
|
|
13
|
+
from typing import Any, Callable, List, Optional, Set, Tuple
|
|
14
|
+
|
|
15
|
+
from ... import _sffuncspan, app_config
|
|
16
|
+
from ...constants import (
|
|
17
|
+
FUNCSPAN_OVERRIDE_HEADER_BYTES,
|
|
18
|
+
SAILFISH_TRACING_HEADER,
|
|
19
|
+
SAILFISH_TRACING_HEADER_BYTES,
|
|
20
|
+
)
|
|
21
|
+
from ...custom_excepthook import custom_excepthook
|
|
22
|
+
from ...env_vars import (
|
|
23
|
+
SF_DEBUG,
|
|
24
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_BODY,
|
|
25
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS,
|
|
26
|
+
SF_NETWORKHOP_CAPTURE_RESPONSE_BODY,
|
|
27
|
+
SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS,
|
|
28
|
+
SF_NETWORKHOP_REQUEST_LIMIT_MB,
|
|
29
|
+
SF_NETWORKHOP_RESPONSE_LIMIT_MB,
|
|
30
|
+
)
|
|
31
|
+
from ...fast_network_hop import fast_send_network_hop_fast, register_endpoint
|
|
32
|
+
from ...thread_local import (
|
|
33
|
+
clear_c_tls_parent_trace_id,
|
|
34
|
+
clear_current_request_path,
|
|
35
|
+
clear_funcspan_override,
|
|
36
|
+
clear_outbound_header_base,
|
|
37
|
+
clear_trace_id,
|
|
38
|
+
generate_new_trace_id,
|
|
39
|
+
get_or_set_sf_trace_id,
|
|
40
|
+
get_sf_trace_id,
|
|
41
|
+
set_current_request_path,
|
|
42
|
+
set_funcspan_override,
|
|
43
|
+
set_outbound_header_base,
|
|
44
|
+
)
|
|
45
|
+
from .cors_utils import inject_sailfish_headers, should_inject_headers
|
|
46
|
+
from .utils import _unwrap_user_func # your cached helpers
|
|
47
|
+
from .utils import _is_user_code, should_skip_route, reinitialize_log_print_capture_for_worker
|
|
48
|
+
|
|
49
|
+
# Size limits in bytes
|
|
50
|
+
_REQUEST_LIMIT_BYTES = SF_NETWORKHOP_REQUEST_LIMIT_MB * 1024 * 1024
|
|
51
|
+
_RESPONSE_LIMIT_BYTES = SF_NETWORKHOP_RESPONSE_LIMIT_MB * 1024 * 1024
|
|
52
|
+
|
|
53
|
+
# Pre-registered endpoint IDs
|
|
54
|
+
_ENDPOINT_REGISTRY: dict[tuple, int] = {}
|
|
55
|
+
|
|
56
|
+
# Module-level variable for routes to skip (set by patch_quart)
|
|
57
|
+
_ROUTES_TO_SKIP = []
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
import quart
|
|
61
|
+
from quart.app import Quart
|
|
62
|
+
from quart.wrappers import Response
|
|
63
|
+
except ImportError:
|
|
64
|
+
# Quart not installed → no-op
|
|
65
|
+
def patch_quart(routes_to_skip: Optional[List[str]] = None):
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
else:
|
|
69
|
+
# ──────────────────────────────────────────────────────────
|
|
70
|
+
# OTEL-STYLE: Request hooks (before + after) - Flask-style
|
|
71
|
+
# ──────────────────────────────────────────────────────────
|
|
72
|
+
def _install_request_hooks(app):
|
|
73
|
+
"""Install Flask-style before/after request hooks for Quart."""
|
|
74
|
+
from quart import g, request
|
|
75
|
+
|
|
76
|
+
@app.before_request
|
|
77
|
+
async def _extract_sf_header():
|
|
78
|
+
"""OTEL-STYLE: Extract trace header and capture request data before handler."""
|
|
79
|
+
# Set current request path for route-based suppression (SF_DISABLE_INBOUND_NETWORK_TRACING_ON_ROUTES)
|
|
80
|
+
set_current_request_path(request.path)
|
|
81
|
+
|
|
82
|
+
rid = request.headers.get(SAILFISH_TRACING_HEADER)
|
|
83
|
+
if rid:
|
|
84
|
+
get_or_set_sf_trace_id(rid, is_associated_with_inbound_request=True)
|
|
85
|
+
else:
|
|
86
|
+
# No incoming header - generate fresh trace_id for this request
|
|
87
|
+
generate_new_trace_id()
|
|
88
|
+
|
|
89
|
+
# Check for function span capture override header (highest priority!)
|
|
90
|
+
funcspan_override_header = request.headers.get(
|
|
91
|
+
"X-Sf3-FunctionSpanCaptureOverride"
|
|
92
|
+
)
|
|
93
|
+
if funcspan_override_header:
|
|
94
|
+
try:
|
|
95
|
+
set_funcspan_override(funcspan_override_header)
|
|
96
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
97
|
+
print(
|
|
98
|
+
f"[[Quart.before_request]] Set function span override from header: {funcspan_override_header}",
|
|
99
|
+
log=False,
|
|
100
|
+
)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
103
|
+
print(
|
|
104
|
+
f"[[Quart.before_request]] Failed to set function span override: {e}",
|
|
105
|
+
log=False,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Initialize outbound base without list/allocs from split()
|
|
109
|
+
try:
|
|
110
|
+
trace_id = get_sf_trace_id()
|
|
111
|
+
if trace_id:
|
|
112
|
+
s = str(trace_id)
|
|
113
|
+
i = s.find("/") # session
|
|
114
|
+
j = s.find("/", i + 1) if i != -1 else -1 # page
|
|
115
|
+
if j != -1:
|
|
116
|
+
base_trace = s[:j] # "session/page"
|
|
117
|
+
set_outbound_header_base(
|
|
118
|
+
base_trace=base_trace,
|
|
119
|
+
parent_trace_id=s, # "session/page/uuid"
|
|
120
|
+
funcspan=funcspan_override_header,
|
|
121
|
+
)
|
|
122
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
123
|
+
print(
|
|
124
|
+
f"[[Quart.before_request]] Initialized outbound header base (base={base_trace[:16]}...)",
|
|
125
|
+
log=False,
|
|
126
|
+
)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
129
|
+
print(
|
|
130
|
+
f"[[Quart.before_request]] Failed to initialize outbound header base: {e}",
|
|
131
|
+
log=False,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Capture request headers if enabled
|
|
135
|
+
if SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS:
|
|
136
|
+
try:
|
|
137
|
+
req_headers = dict(request.headers)
|
|
138
|
+
g._sf_request_headers = req_headers
|
|
139
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
140
|
+
print(
|
|
141
|
+
f"[[Quart]] Captured request headers: {len(req_headers)} headers",
|
|
142
|
+
log=False,
|
|
143
|
+
)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
146
|
+
print(
|
|
147
|
+
f"[[Quart]] Failed to capture request headers: {e}",
|
|
148
|
+
log=False,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Capture request body if enabled
|
|
152
|
+
if SF_NETWORKHOP_CAPTURE_REQUEST_BODY:
|
|
153
|
+
try:
|
|
154
|
+
# Quart: await request.get_data() gets raw bytes
|
|
155
|
+
body = await request.get_data()
|
|
156
|
+
if body:
|
|
157
|
+
req_body = body[:_REQUEST_LIMIT_BYTES]
|
|
158
|
+
g._sf_request_body = req_body
|
|
159
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
160
|
+
print(
|
|
161
|
+
f"[[Quart]] Request body capture: {len(req_body)} bytes",
|
|
162
|
+
log=False,
|
|
163
|
+
)
|
|
164
|
+
except Exception as e:
|
|
165
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
166
|
+
print(
|
|
167
|
+
f"[[Quart]] Failed to capture request body: {e}", log=False
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
@app.after_request
|
|
171
|
+
async def _emit_network_hop(response):
|
|
172
|
+
"""
|
|
173
|
+
OTEL-STYLE: Emit network hop AFTER response is built.
|
|
174
|
+
Quart is Flask-based, so we use the same @after_request pattern.
|
|
175
|
+
"""
|
|
176
|
+
endpoint_id = getattr(g, "_sf_endpoint_id", None)
|
|
177
|
+
if endpoint_id is not None and endpoint_id >= 0:
|
|
178
|
+
try:
|
|
179
|
+
_, session_id = get_or_set_sf_trace_id()
|
|
180
|
+
|
|
181
|
+
# Get captured request data
|
|
182
|
+
req_headers = getattr(g, "_sf_request_headers", None)
|
|
183
|
+
req_body = getattr(g, "_sf_request_body", None)
|
|
184
|
+
|
|
185
|
+
# Capture response headers if enabled
|
|
186
|
+
resp_headers = None
|
|
187
|
+
if SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS:
|
|
188
|
+
try:
|
|
189
|
+
resp_headers = dict(response.headers)
|
|
190
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
191
|
+
print(
|
|
192
|
+
f"[[Quart]] Captured response headers: {len(resp_headers)} headers",
|
|
193
|
+
log=False,
|
|
194
|
+
)
|
|
195
|
+
except Exception as e:
|
|
196
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
197
|
+
print(
|
|
198
|
+
f"[[Quart]] Failed to capture response headers: {e}",
|
|
199
|
+
log=False,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Capture response body if enabled
|
|
203
|
+
resp_body = None
|
|
204
|
+
if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY:
|
|
205
|
+
try:
|
|
206
|
+
# Quart response: get_data() returns bytes (async)
|
|
207
|
+
body = await response.get_data()
|
|
208
|
+
if body:
|
|
209
|
+
resp_body = body[:_RESPONSE_LIMIT_BYTES]
|
|
210
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
211
|
+
print(
|
|
212
|
+
f"[[Quart]] Captured response body: {len(resp_body)} bytes",
|
|
213
|
+
log=False,
|
|
214
|
+
)
|
|
215
|
+
except Exception as e:
|
|
216
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
217
|
+
print(
|
|
218
|
+
f"[[Quart]] Failed to capture response body: {e}",
|
|
219
|
+
log=False,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Extract raw path and query string for C to parse
|
|
223
|
+
raw_path = request.path # e.g., "/log"
|
|
224
|
+
raw_query = request.query_string # Already bytes (e.g., b"foo=5")
|
|
225
|
+
|
|
226
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
227
|
+
print(
|
|
228
|
+
f"[[Quart]] About to emit network hop: endpoint_id={endpoint_id}, "
|
|
229
|
+
f"req_headers={'present' if req_headers else 'None'}, "
|
|
230
|
+
f"req_body={len(req_body) if req_body else 0} bytes, "
|
|
231
|
+
f"resp_headers={'present' if resp_headers else 'None'}, "
|
|
232
|
+
f"resp_body={len(resp_body) if resp_body else 0} bytes",
|
|
233
|
+
log=False,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Direct C call - queues to background worker, returns instantly
|
|
237
|
+
# C will parse route and query_params from raw data
|
|
238
|
+
fast_send_network_hop_fast(
|
|
239
|
+
session_id=session_id,
|
|
240
|
+
endpoint_id=endpoint_id,
|
|
241
|
+
raw_path=raw_path,
|
|
242
|
+
raw_query_string=raw_query,
|
|
243
|
+
request_headers=req_headers,
|
|
244
|
+
request_body=req_body,
|
|
245
|
+
response_headers=resp_headers,
|
|
246
|
+
response_body=resp_body,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
250
|
+
print(
|
|
251
|
+
f"[[Quart]] Emitted network hop: endpoint_id={endpoint_id} "
|
|
252
|
+
f"session={session_id}",
|
|
253
|
+
log=False,
|
|
254
|
+
)
|
|
255
|
+
except Exception as e:
|
|
256
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
257
|
+
print(f"[[Quart]] Failed to emit network hop: {e}", log=False)
|
|
258
|
+
|
|
259
|
+
# CRITICAL: Clear C TLS to prevent stale data in thread pools
|
|
260
|
+
clear_c_tls_parent_trace_id()
|
|
261
|
+
|
|
262
|
+
# CRITICAL: Clear outbound header base to prevent stale cached headers
|
|
263
|
+
# ContextVar does NOT automatically clean up in thread pools - must clear explicitly
|
|
264
|
+
clear_outbound_header_base()
|
|
265
|
+
|
|
266
|
+
# CRITICAL: Clear trace_id to ensure fresh generation for next request
|
|
267
|
+
# Without this, get_or_set_sf_trace_id() reuses trace_id from previous request
|
|
268
|
+
# causing X-Sf4-Prid to stay constant when no incoming X-Sf3-Rid header
|
|
269
|
+
clear_trace_id()
|
|
270
|
+
|
|
271
|
+
# CRITICAL: Clear current request path to prevent stale data in thread pools
|
|
272
|
+
clear_current_request_path()
|
|
273
|
+
|
|
274
|
+
# Clear function span override for this request (ContextVar cleanup - also syncs C thread-local)
|
|
275
|
+
try:
|
|
276
|
+
clear_funcspan_override()
|
|
277
|
+
except Exception:
|
|
278
|
+
pass
|
|
279
|
+
|
|
280
|
+
return response
|
|
281
|
+
|
|
282
|
+
# ──────────────────────────────────────────────────────────
|
|
283
|
+
# OTEL-STYLE: Per-view endpoint metadata capture
|
|
284
|
+
# ──────────────────────────────────────────────────────────
|
|
285
|
+
def _hop_wrapper(view_fn: Callable, route: str = None):
|
|
286
|
+
"""
|
|
287
|
+
OTEL-STYLE: Pre-register endpoint and store endpoint_id in quart.g.
|
|
288
|
+
Emission happens in @after_request hook with captured body/headers.
|
|
289
|
+
"""
|
|
290
|
+
from quart import g
|
|
291
|
+
|
|
292
|
+
real_fn = _unwrap_user_func(view_fn)
|
|
293
|
+
|
|
294
|
+
code = getattr(real_fn, "__code__", None)
|
|
295
|
+
if not code or not _is_user_code(code.co_filename):
|
|
296
|
+
return view_fn
|
|
297
|
+
|
|
298
|
+
# Skip Strawberry GraphQL handlers
|
|
299
|
+
if getattr(real_fn, "__module__", "").startswith("strawberry"):
|
|
300
|
+
return view_fn
|
|
301
|
+
|
|
302
|
+
# Check if route should be skipped
|
|
303
|
+
if should_skip_route(route, _ROUTES_TO_SKIP):
|
|
304
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
305
|
+
print(
|
|
306
|
+
f"[[Quart]] Skipping endpoint (route matches skip pattern): {route}",
|
|
307
|
+
log=False,
|
|
308
|
+
)
|
|
309
|
+
return view_fn # Return original function unwrapped - no telemetry
|
|
310
|
+
|
|
311
|
+
hop_key = (code.co_filename, code.co_firstlineno)
|
|
312
|
+
fn_name = real_fn.__name__
|
|
313
|
+
filename = code.co_filename
|
|
314
|
+
line_no = code.co_firstlineno
|
|
315
|
+
|
|
316
|
+
# Pre-register endpoint if user code
|
|
317
|
+
endpoint_id = _ENDPOINT_REGISTRY.get(hop_key)
|
|
318
|
+
if endpoint_id is None:
|
|
319
|
+
endpoint_id = register_endpoint(
|
|
320
|
+
line=str(line_no),
|
|
321
|
+
column="0",
|
|
322
|
+
name=fn_name,
|
|
323
|
+
entrypoint=filename,
|
|
324
|
+
route=route,
|
|
325
|
+
)
|
|
326
|
+
if endpoint_id >= 0:
|
|
327
|
+
_ENDPOINT_REGISTRY[hop_key] = endpoint_id
|
|
328
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
329
|
+
print(
|
|
330
|
+
f"[[Quart]] Registered endpoint: {fn_name} @ {filename}:{line_no} (id={endpoint_id})",
|
|
331
|
+
log=False,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
@wraps(view_fn)
|
|
335
|
+
async def _wrapped(*args, **kwargs):
|
|
336
|
+
# OTEL-STYLE: Store endpoint_id for after_request to emit
|
|
337
|
+
if not hasattr(g, "_sf_endpoint_id"):
|
|
338
|
+
g._sf_endpoint_id = endpoint_id
|
|
339
|
+
|
|
340
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
341
|
+
print(
|
|
342
|
+
f"[[Quart]] Captured endpoint: {fn_name} ({filename}:{line_no}) endpoint_id={endpoint_id}",
|
|
343
|
+
log=False,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
return await view_fn(*args, **kwargs)
|
|
347
|
+
|
|
348
|
+
return _wrapped
|
|
349
|
+
|
|
350
|
+
def _patch_add_route(cls):
|
|
351
|
+
"""
|
|
352
|
+
Patch add_url_rule on Quart so that the final stored endpoint function
|
|
353
|
+
is wrapped after Quart has done its own bookkeeping.
|
|
354
|
+
"""
|
|
355
|
+
original_add = cls.add_url_rule
|
|
356
|
+
|
|
357
|
+
def patched_add(self, rule, endpoint=None, view_func=None, **options):
|
|
358
|
+
# let Quart register the route first
|
|
359
|
+
original_add(self, rule, endpoint=endpoint, view_func=view_func, **options)
|
|
360
|
+
|
|
361
|
+
ep = endpoint or (view_func and view_func.__name__)
|
|
362
|
+
if not ep: # defensive
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
target = self.view_functions.get(ep)
|
|
366
|
+
if callable(target):
|
|
367
|
+
self.view_functions[ep] = _hop_wrapper(target, route=rule)
|
|
368
|
+
|
|
369
|
+
cls.add_url_rule = patched_add
|
|
370
|
+
|
|
371
|
+
# ──────────────────────────────────────────────────────────
|
|
372
|
+
# ASGI middleware - TRUE ZERO OVERHEAD (emits AFTER response sent)
|
|
373
|
+
# ──────────────────────────────────────────────────────────
|
|
374
|
+
class SFZeroOverheadQuartMiddleware:
|
|
375
|
+
"""
|
|
376
|
+
OTEL-STYLE ZERO-OVERHEAD network hop capture middleware.
|
|
377
|
+
|
|
378
|
+
- Propagates inbound SAILFISH_TRACING_HEADER → ContextVar
|
|
379
|
+
- Pre-registers endpoints at startup for ultra-fast emission
|
|
380
|
+
- Captures request/response headers and body when enabled
|
|
381
|
+
- Emits NetworkHop AFTER response sent (pure async, no blocking)
|
|
382
|
+
- Funnels all exceptions through custom_excepthook
|
|
383
|
+
"""
|
|
384
|
+
|
|
385
|
+
def __init__(self, app):
|
|
386
|
+
self.app = app
|
|
387
|
+
self._endpoint_cache = {} # Cache endpoint_id by function id
|
|
388
|
+
|
|
389
|
+
async def __call__(self, scope, receive, send):
|
|
390
|
+
if scope.get("type") != "http":
|
|
391
|
+
await self.app(scope, receive, send)
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
# PERFORMANCE: Single-pass bytes-level header scan (no dict allocation until needed)
|
|
395
|
+
# Scan headers once on bytes, only decode what we need, use latin-1 (fast 1:1 byte map)
|
|
396
|
+
hdr_tuples = scope.get("headers") or ()
|
|
397
|
+
incoming_trace_raw = None # bytes
|
|
398
|
+
funcspan_raw = None # bytes
|
|
399
|
+
req_headers = None # dict[str,str] only if capture enabled
|
|
400
|
+
|
|
401
|
+
capture_req_headers = SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS # local cache
|
|
402
|
+
|
|
403
|
+
if capture_req_headers:
|
|
404
|
+
# decode once using latin-1 (1:1 bytes, faster than utf-8 and never throws)
|
|
405
|
+
tmp = {}
|
|
406
|
+
for k, v in hdr_tuples:
|
|
407
|
+
kl = k.lower()
|
|
408
|
+
if kl == SAILFISH_TRACING_HEADER_BYTES:
|
|
409
|
+
incoming_trace_raw = v
|
|
410
|
+
elif kl == FUNCSPAN_OVERRIDE_HEADER_BYTES:
|
|
411
|
+
funcspan_raw = v
|
|
412
|
+
# build the dict while we're here
|
|
413
|
+
tmp[k.decode("latin-1")] = v.decode("latin-1")
|
|
414
|
+
req_headers = tmp
|
|
415
|
+
else:
|
|
416
|
+
for k, v in hdr_tuples:
|
|
417
|
+
kl = k.lower()
|
|
418
|
+
if kl == SAILFISH_TRACING_HEADER_BYTES:
|
|
419
|
+
incoming_trace_raw = v
|
|
420
|
+
elif kl == FUNCSPAN_OVERRIDE_HEADER_BYTES:
|
|
421
|
+
funcspan_raw = v
|
|
422
|
+
# no dict build
|
|
423
|
+
|
|
424
|
+
# CRITICAL: Seed/ensure trace_id immediately (BEFORE any outbound work)
|
|
425
|
+
if incoming_trace_raw:
|
|
426
|
+
# Incoming X-Sf3-Rid header provided - use it
|
|
427
|
+
incoming_trace = incoming_trace_raw.decode("latin-1")
|
|
428
|
+
get_or_set_sf_trace_id(
|
|
429
|
+
incoming_trace, is_associated_with_inbound_request=True
|
|
430
|
+
)
|
|
431
|
+
else:
|
|
432
|
+
# No incoming X-Sf3-Rid header - generate fresh trace_id for this request
|
|
433
|
+
generate_new_trace_id()
|
|
434
|
+
|
|
435
|
+
# Optional funcspan override (decode only if present)
|
|
436
|
+
funcspan_override_header = (
|
|
437
|
+
funcspan_raw.decode("latin-1") if funcspan_raw else None
|
|
438
|
+
)
|
|
439
|
+
if funcspan_override_header:
|
|
440
|
+
try:
|
|
441
|
+
set_funcspan_override(funcspan_override_header)
|
|
442
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
443
|
+
print(
|
|
444
|
+
f"[[Quart.middleware]] Set function span override from header: {funcspan_override_header}",
|
|
445
|
+
log=False,
|
|
446
|
+
)
|
|
447
|
+
except Exception as e:
|
|
448
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
449
|
+
print(
|
|
450
|
+
f"[[Quart.middleware]] Failed to set function span override: {e}",
|
|
451
|
+
log=False,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
# Initialize outbound base without list/allocs from split()
|
|
455
|
+
try:
|
|
456
|
+
trace_id = get_sf_trace_id()
|
|
457
|
+
if trace_id:
|
|
458
|
+
s = str(trace_id)
|
|
459
|
+
i = s.find("/") # session
|
|
460
|
+
j = s.find("/", i + 1) if i != -1 else -1 # page
|
|
461
|
+
if j != -1:
|
|
462
|
+
base_trace = s[:j] # "session/page"
|
|
463
|
+
set_outbound_header_base(
|
|
464
|
+
base_trace=base_trace,
|
|
465
|
+
parent_trace_id=s, # "session/page/uuid"
|
|
466
|
+
funcspan=funcspan_override_header,
|
|
467
|
+
)
|
|
468
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
469
|
+
print(
|
|
470
|
+
f"[[Quart.middleware]] Initialized outbound header base (base={base_trace[:16]}...)",
|
|
471
|
+
log=False,
|
|
472
|
+
)
|
|
473
|
+
except Exception as e:
|
|
474
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
475
|
+
print(
|
|
476
|
+
f"[[Quart.middleware]] Failed to initialize outbound header base: {e}",
|
|
477
|
+
log=False,
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
# Pre-register endpoint and get endpoint_id
|
|
481
|
+
endpoint_id = None
|
|
482
|
+
endpoint_fn = scope.get("endpoint")
|
|
483
|
+
|
|
484
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
485
|
+
print(
|
|
486
|
+
f"[[Quart]] endpoint_fn={endpoint_fn}, type={type(endpoint_fn) if endpoint_fn else None}",
|
|
487
|
+
log=False,
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
if endpoint_fn:
|
|
491
|
+
# Check cache first
|
|
492
|
+
fn_id = id(endpoint_fn)
|
|
493
|
+
if fn_id in self._endpoint_cache:
|
|
494
|
+
endpoint_id = self._endpoint_cache[fn_id]
|
|
495
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
496
|
+
print(
|
|
497
|
+
f"[[Quart]] Using cached endpoint_id={endpoint_id}",
|
|
498
|
+
log=False,
|
|
499
|
+
)
|
|
500
|
+
else:
|
|
501
|
+
# Extract metadata and register
|
|
502
|
+
user_fn = _unwrap_user_func(endpoint_fn)
|
|
503
|
+
code = getattr(user_fn, "__code__", None)
|
|
504
|
+
|
|
505
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
506
|
+
print(
|
|
507
|
+
f"[[Quart]] user_fn={user_fn.__name__ if hasattr(user_fn, '__name__') else user_fn}, code={code}, is_user_code={_is_user_code(code.co_filename) if code else False}",
|
|
508
|
+
log=False,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
# Skip Strawberry GraphQL handlers
|
|
512
|
+
if getattr(user_fn, "__module__", "").startswith("strawberry"):
|
|
513
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
514
|
+
print(
|
|
515
|
+
f"[[Quart]] Skipping Strawberry GraphQL endpoint",
|
|
516
|
+
log=False,
|
|
517
|
+
)
|
|
518
|
+
# Don't register, don't cache
|
|
519
|
+
elif code and _is_user_code(code.co_filename):
|
|
520
|
+
hop_key = (code.co_filename, code.co_firstlineno)
|
|
521
|
+
|
|
522
|
+
# Check global registry first
|
|
523
|
+
endpoint_id = _ENDPOINT_REGISTRY.get(hop_key)
|
|
524
|
+
if endpoint_id is None:
|
|
525
|
+
endpoint_id = register_endpoint(
|
|
526
|
+
line=str(code.co_firstlineno),
|
|
527
|
+
column="0",
|
|
528
|
+
name=user_fn.__name__,
|
|
529
|
+
entrypoint=code.co_filename,
|
|
530
|
+
route=None,
|
|
531
|
+
)
|
|
532
|
+
if endpoint_id >= 0:
|
|
533
|
+
_ENDPOINT_REGISTRY[hop_key] = endpoint_id
|
|
534
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
535
|
+
print(
|
|
536
|
+
f"[[Quart]] Registered endpoint: {user_fn.__name__} @ {code.co_filename}:{code.co_firstlineno} (id={endpoint_id})",
|
|
537
|
+
log=False,
|
|
538
|
+
)
|
|
539
|
+
else:
|
|
540
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
541
|
+
print(
|
|
542
|
+
f"[[Quart]] Failed to register endpoint (returned {endpoint_id})",
|
|
543
|
+
log=False,
|
|
544
|
+
)
|
|
545
|
+
else:
|
|
546
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
547
|
+
print(
|
|
548
|
+
f"[[Quart]] Using pre-registered endpoint_id={endpoint_id}",
|
|
549
|
+
log=False,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
# Cache by function id for fast lookup
|
|
553
|
+
self._endpoint_cache[fn_id] = endpoint_id
|
|
554
|
+
|
|
555
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
556
|
+
print(f"[[Quart]] Final endpoint_id={endpoint_id}", log=False)
|
|
557
|
+
|
|
558
|
+
# NOTE: req_headers already captured in single-pass scan above (if enabled)
|
|
559
|
+
|
|
560
|
+
# Capture request body if enabled
|
|
561
|
+
body_parts = []
|
|
562
|
+
body_size = 0
|
|
563
|
+
if SF_NETWORKHOP_CAPTURE_REQUEST_BODY:
|
|
564
|
+
try:
|
|
565
|
+
# Save original receive before wrapping
|
|
566
|
+
original_receive = receive
|
|
567
|
+
|
|
568
|
+
async def receive_with_body():
|
|
569
|
+
nonlocal body_size
|
|
570
|
+
message = await original_receive()
|
|
571
|
+
if message["type"] == "http.request":
|
|
572
|
+
body_part = message.get("body", b"")
|
|
573
|
+
if body_part and body_size < _REQUEST_LIMIT_BYTES:
|
|
574
|
+
remaining = _REQUEST_LIMIT_BYTES - body_size
|
|
575
|
+
body_parts.append(body_part[:remaining])
|
|
576
|
+
body_size += len(body_part)
|
|
577
|
+
return message
|
|
578
|
+
|
|
579
|
+
receive = receive_with_body
|
|
580
|
+
|
|
581
|
+
except Exception as e:
|
|
582
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
583
|
+
print(
|
|
584
|
+
f"[[Quart]] Failed to setup request body capture: {e}",
|
|
585
|
+
log=False,
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
# Capture response headers and body
|
|
589
|
+
resp_headers = None
|
|
590
|
+
resp_body_parts = []
|
|
591
|
+
resp_body_size = 0
|
|
592
|
+
|
|
593
|
+
# OTEL-STYLE: Wrap send to capture response data and emit AFTER response sent
|
|
594
|
+
async def wrapped_send(message):
|
|
595
|
+
nonlocal resp_headers, resp_body_size
|
|
596
|
+
|
|
597
|
+
# Capture response headers
|
|
598
|
+
if (
|
|
599
|
+
message["type"] == "http.response.start"
|
|
600
|
+
and SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS
|
|
601
|
+
):
|
|
602
|
+
try:
|
|
603
|
+
headers = message.get("headers", [])
|
|
604
|
+
resp_headers = {
|
|
605
|
+
name.decode("utf-8"): val.decode("utf-8")
|
|
606
|
+
for name, val in headers
|
|
607
|
+
}
|
|
608
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
609
|
+
print(
|
|
610
|
+
f"[[Quart]] Captured response headers: {len(resp_headers)} headers",
|
|
611
|
+
log=False,
|
|
612
|
+
)
|
|
613
|
+
except Exception as e:
|
|
614
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
615
|
+
print(
|
|
616
|
+
f"[[Quart]] Failed to capture response headers: {e}",
|
|
617
|
+
log=False,
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
# Capture response body
|
|
621
|
+
if (
|
|
622
|
+
message["type"] == "http.response.body"
|
|
623
|
+
and SF_NETWORKHOP_CAPTURE_RESPONSE_BODY
|
|
624
|
+
):
|
|
625
|
+
try:
|
|
626
|
+
body_part = message.get("body", b"")
|
|
627
|
+
if body_part and resp_body_size < _RESPONSE_LIMIT_BYTES:
|
|
628
|
+
remaining = _RESPONSE_LIMIT_BYTES - resp_body_size
|
|
629
|
+
resp_body_parts.append(body_part[:remaining])
|
|
630
|
+
resp_body_size += len(body_part)
|
|
631
|
+
except Exception as e:
|
|
632
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
633
|
+
print(
|
|
634
|
+
f"[[Quart]] Failed to capture response body chunk: {e}",
|
|
635
|
+
log=False,
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
await send(message)
|
|
639
|
+
|
|
640
|
+
# After final response body sent, emit network hop
|
|
641
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
642
|
+
print(
|
|
643
|
+
f"[[Quart]] Message type: {message.get('type')}, more_body: {message.get('more_body', False)}, endpoint_id: {endpoint_id}",
|
|
644
|
+
log=False,
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
if message["type"] == "http.response.body" and not message.get(
|
|
648
|
+
"more_body", False
|
|
649
|
+
):
|
|
650
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
651
|
+
print(
|
|
652
|
+
f"[[Quart]] Final response body message, checking endpoint_id={endpoint_id}",
|
|
653
|
+
log=False,
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
if endpoint_id is not None and endpoint_id >= 0:
|
|
657
|
+
try:
|
|
658
|
+
_, session_id = get_or_set_sf_trace_id()
|
|
659
|
+
|
|
660
|
+
# Finalize request body
|
|
661
|
+
final_req_body = (
|
|
662
|
+
b"".join(body_parts) if body_parts else None
|
|
663
|
+
)
|
|
664
|
+
if final_req_body and SF_DEBUG:
|
|
665
|
+
print(
|
|
666
|
+
f"[[Quart]] Request body capture: {len(final_req_body)} bytes",
|
|
667
|
+
log=False,
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
# Finalize response body
|
|
671
|
+
final_resp_body = (
|
|
672
|
+
b"".join(resp_body_parts) if resp_body_parts else None
|
|
673
|
+
)
|
|
674
|
+
if final_resp_body and SF_DEBUG:
|
|
675
|
+
print(
|
|
676
|
+
f"[[Quart]] Captured response body: {len(final_resp_body)} bytes",
|
|
677
|
+
log=False,
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
681
|
+
print(
|
|
682
|
+
f"[[Quart]] About to emit network hop: endpoint_id={endpoint_id}, "
|
|
683
|
+
f"req_headers={'present' if req_headers else 'None'}, "
|
|
684
|
+
f"req_body={len(final_req_body) if final_req_body else 0} bytes, "
|
|
685
|
+
f"resp_headers={'present' if resp_headers else 'None'}, "
|
|
686
|
+
f"resp_body={len(final_resp_body) if final_resp_body else 0} bytes",
|
|
687
|
+
log=False,
|
|
688
|
+
)
|
|
689
|
+
|
|
690
|
+
fast_send_network_hop_fast(
|
|
691
|
+
session_id=session_id,
|
|
692
|
+
endpoint_id=endpoint_id,
|
|
693
|
+
raw_path=scope.get("path"),
|
|
694
|
+
raw_query_string=scope.get("query_string", b""),
|
|
695
|
+
request_headers=req_headers,
|
|
696
|
+
request_body=final_req_body,
|
|
697
|
+
response_headers=resp_headers,
|
|
698
|
+
response_body=final_resp_body,
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
702
|
+
print(
|
|
703
|
+
f"[[Quart]] Emitted network hop: endpoint_id={endpoint_id} "
|
|
704
|
+
f"session={session_id}",
|
|
705
|
+
log=False,
|
|
706
|
+
)
|
|
707
|
+
except Exception as e: # noqa: BLE001 S110
|
|
708
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
709
|
+
print(
|
|
710
|
+
f"[[Quart]] Failed to emit network hop: {e}",
|
|
711
|
+
log=False,
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
try:
|
|
715
|
+
await self.app(scope, receive, wrapped_send)
|
|
716
|
+
except Exception as exc: # noqa: BLE001
|
|
717
|
+
custom_excepthook(type(exc), exc, exc.__traceback__)
|
|
718
|
+
raise
|
|
719
|
+
finally:
|
|
720
|
+
# CRITICAL: Clear C TLS to prevent stale data in thread pools
|
|
721
|
+
clear_c_tls_parent_trace_id()
|
|
722
|
+
|
|
723
|
+
# CRITICAL: Clear outbound header base to prevent stale cached headers
|
|
724
|
+
# ContextVar does NOT automatically clean up in thread pools - must clear explicitly
|
|
725
|
+
clear_outbound_header_base()
|
|
726
|
+
|
|
727
|
+
# CRITICAL: Clear trace_id to ensure fresh generation for next request
|
|
728
|
+
# Without this, get_or_set_sf_trace_id() reuses trace_id from previous request
|
|
729
|
+
# causing X-Sf4-Prid to stay constant when no incoming X-Sf3-Rid header
|
|
730
|
+
clear_trace_id()
|
|
731
|
+
|
|
732
|
+
# CRITICAL: Clear current request path to prevent stale data in thread pools
|
|
733
|
+
clear_current_request_path()
|
|
734
|
+
|
|
735
|
+
# Clear function span override for this request (ContextVar cleanup - also syncs C thread-local)
|
|
736
|
+
try:
|
|
737
|
+
clear_funcspan_override()
|
|
738
|
+
except Exception:
|
|
739
|
+
pass
|
|
740
|
+
|
|
741
|
+
# ──────────────────────────────────────────────────────────
|
|
742
|
+
# Main patch entry-point (Flask-style hooks for Quart)
|
|
743
|
+
# ──────────────────────────────────────────────────────────
|
|
744
|
+
def patch_quart(routes_to_skip: Optional[List[str]] = None):
|
|
745
|
+
"""
|
|
746
|
+
Quart patch using Flask-style before/after request hooks:
|
|
747
|
+
• Wraps view functions to capture endpoint_id
|
|
748
|
+
• Uses @before_request to capture request data
|
|
749
|
+
• Uses @after_request to emit AFTER response built (Flask-style)
|
|
750
|
+
• Captures request/response headers and body when enabled
|
|
751
|
+
• Direct C call with route/query params extracted from request object
|
|
752
|
+
|
|
753
|
+
Note: Quart is Flask-based and doesn't expose endpoints in ASGI scope,
|
|
754
|
+
so we use Flask-style hooks instead of ASGI middleware.
|
|
755
|
+
"""
|
|
756
|
+
global _ROUTES_TO_SKIP
|
|
757
|
+
_ROUTES_TO_SKIP = routes_to_skip or []
|
|
758
|
+
|
|
759
|
+
# Guard against double-patching
|
|
760
|
+
if getattr(Quart, "__sf_tracing_patched__", False):
|
|
761
|
+
return
|
|
762
|
+
|
|
763
|
+
original_init = Quart.__init__
|
|
764
|
+
|
|
765
|
+
def patched_init(self, *args, **kwargs):
|
|
766
|
+
original_init(self, *args, **kwargs)
|
|
767
|
+
|
|
768
|
+
# Note: Profiler is already installed by unified_interceptor.py
|
|
769
|
+
|
|
770
|
+
# Install Flask-style hooks for request/response capture
|
|
771
|
+
_install_request_hooks(self)
|
|
772
|
+
|
|
773
|
+
# Patch add_url_rule to wrap view functions
|
|
774
|
+
_patch_add_route(self.__class__)
|
|
775
|
+
|
|
776
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
777
|
+
print("[[patch_quart]] Flask-style hooks installed", log=False)
|
|
778
|
+
|
|
779
|
+
Quart.__init__ = patched_init
|
|
780
|
+
Quart.__sf_tracing_patched__ = True
|
|
781
|
+
|
|
782
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
783
|
+
print(
|
|
784
|
+
"[[patch_quart]] Flask-style patch applied (emits in @after_request)",
|
|
785
|
+
log=False,
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
def patch_quart_cors():
|
|
789
|
+
"""
|
|
790
|
+
Patch quart-cors to automatically inject Sailfish headers.
|
|
791
|
+
|
|
792
|
+
SAFE: Only modifies CORS if quart-cors is installed and used.
|
|
793
|
+
"""
|
|
794
|
+
try:
|
|
795
|
+
from quart_cors import cors
|
|
796
|
+
except ImportError:
|
|
797
|
+
# quart-cors not installed, skip patching
|
|
798
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
799
|
+
print(
|
|
800
|
+
"[[patch_quart_cors]] quart-cors not installed, skipping", log=False
|
|
801
|
+
)
|
|
802
|
+
return
|
|
803
|
+
|
|
804
|
+
# Check if already patched
|
|
805
|
+
if hasattr(cors, "_sf_cors_patched"):
|
|
806
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
807
|
+
print("[[patch_quart_cors]] Already patched, skipping", log=False)
|
|
808
|
+
return
|
|
809
|
+
|
|
810
|
+
# Patch the cors decorator/function
|
|
811
|
+
original_cors = cors
|
|
812
|
+
|
|
813
|
+
def patched_cors(app=None, **kwargs):
|
|
814
|
+
# Intercept allow_headers parameter
|
|
815
|
+
if "allow_headers" in kwargs:
|
|
816
|
+
original_headers = kwargs["allow_headers"]
|
|
817
|
+
if should_inject_headers(original_headers):
|
|
818
|
+
kwargs["allow_headers"] = inject_sailfish_headers(original_headers)
|
|
819
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
820
|
+
print(
|
|
821
|
+
"[[patch_quart_cors]] Injected Sailfish headers into quart-cors",
|
|
822
|
+
log=False,
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
# Call original cors
|
|
826
|
+
return original_cors(app, **kwargs)
|
|
827
|
+
|
|
828
|
+
# Replace the cors function in the module
|
|
829
|
+
import quart_cors as qc_module
|
|
830
|
+
|
|
831
|
+
qc_module.cors = patched_cors
|
|
832
|
+
cors._sf_cors_patched = True
|
|
833
|
+
|
|
834
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
835
|
+
print("[[patch_quart_cors]] Successfully patched quart-cors", log=False)
|
|
836
|
+
|
|
837
|
+
# Call CORS patching
|
|
838
|
+
patch_quart_cors()
|
|
839
|
+
|
|
840
|
+
# Expose the patch function
|
|
841
|
+
__all__ = ["patch_quart"]
|