sf-veritas 0.10.3__cp311-cp311-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-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnet.c +924 -0
- sf_veritas/_sffastnet.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnetworkrequest.c +730 -0
- sf_veritas/_sffastnetworkrequest.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan.c +2155 -0
- sf_veritas/_sffuncspan.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan_config.c +617 -0
- sf_veritas/_sffuncspan_config.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfheadercheck.c +341 -0
- sf_veritas/_sfheadercheck.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfnetworkhop.c +1451 -0
- sf_veritas/_sfnetworkhop.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfservice.c +1175 -0
- sf_veritas/_sfservice.cpython-311-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,428 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import sys
|
|
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_outbound_header_base,
|
|
28
|
+
clear_trace_id,
|
|
29
|
+
generate_new_trace_id,
|
|
30
|
+
get_or_set_sf_trace_id,
|
|
31
|
+
get_sf_trace_id,
|
|
32
|
+
set_current_request_path,
|
|
33
|
+
set_funcspan_override,
|
|
34
|
+
set_outbound_header_base,
|
|
35
|
+
)
|
|
36
|
+
from .cors_utils import inject_sailfish_headers, should_inject_headers
|
|
37
|
+
from .utils import _is_user_code, should_skip_route # cached helpers
|
|
38
|
+
|
|
39
|
+
# Size limits in bytes
|
|
40
|
+
_REQUEST_LIMIT_BYTES = SF_NETWORKHOP_REQUEST_LIMIT_MB * 1024 * 1024
|
|
41
|
+
_RESPONSE_LIMIT_BYTES = SF_NETWORKHOP_RESPONSE_LIMIT_MB * 1024 * 1024
|
|
42
|
+
|
|
43
|
+
# Pre-registered endpoint IDs
|
|
44
|
+
_ENDPOINT_REGISTRY: dict[tuple, int] = {}
|
|
45
|
+
|
|
46
|
+
# Routes to skip (set by patch_pyramid)
|
|
47
|
+
_ROUTES_TO_SKIP = []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ------------------------------------------------------------------ #
|
|
51
|
+
# 1.2 Tween factory: header + one-shot profile tracer + exceptions #
|
|
52
|
+
# ------------------------------------------------------------------ #
|
|
53
|
+
def _sf_tracing_tween_factory(handler, registry):
|
|
54
|
+
"""
|
|
55
|
+
OTEL-STYLE Pyramid tween that:
|
|
56
|
+
• Reads SAILFISH_TRACING_HEADER header → ContextVar.
|
|
57
|
+
• Captures request headers and body when enabled.
|
|
58
|
+
• Sets a one-shot profiler to capture endpoint metadata and pre-register endpoint.
|
|
59
|
+
• Captures response headers and body when enabled.
|
|
60
|
+
• Emits NetworkHop AFTER handler completes (OTEL-style zero-overhead).
|
|
61
|
+
• Funnels *all* exceptions (including HTTPException) through
|
|
62
|
+
`custom_excepthook` before letting Pyramid continue normal handling.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def _tween(request):
|
|
66
|
+
# Set current request path for route-based suppression (SF_DISABLE_INBOUND_NETWORK_TRACING_ON_ROUTES)
|
|
67
|
+
set_current_request_path(request.path)
|
|
68
|
+
|
|
69
|
+
# ── 1) Propagate incoming trace header ──────────────────────────
|
|
70
|
+
# PERFORMANCE: Single-pass bytes-level header scan (matching FastAPI pattern)
|
|
71
|
+
hdr = request.headers.get(SAILFISH_TRACING_HEADER)
|
|
72
|
+
funcspan_override_header = request.headers.get(
|
|
73
|
+
"X-Sf3-FunctionSpanCaptureOverride"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# CRITICAL: Seed/ensure trace_id immediately (BEFORE any outbound work)
|
|
77
|
+
if hdr:
|
|
78
|
+
get_or_set_sf_trace_id(hdr, is_associated_with_inbound_request=True)
|
|
79
|
+
else:
|
|
80
|
+
# No incoming X-Sf3-Rid header - generate fresh trace_id for this request
|
|
81
|
+
generate_new_trace_id()
|
|
82
|
+
|
|
83
|
+
# Check for function span capture override header (highest priority!)
|
|
84
|
+
if funcspan_override_header:
|
|
85
|
+
try:
|
|
86
|
+
set_funcspan_override(funcspan_override_header)
|
|
87
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
88
|
+
print(
|
|
89
|
+
f"[[Pyramid.tween]] Set function span override from header: {funcspan_override_header}",
|
|
90
|
+
log=False,
|
|
91
|
+
)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
94
|
+
print(
|
|
95
|
+
f"[[Pyramid.tween]] Failed to set function span override: {e}",
|
|
96
|
+
log=False,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Initialize outbound base without list/allocs from split()
|
|
100
|
+
try:
|
|
101
|
+
trace_id = get_sf_trace_id()
|
|
102
|
+
if trace_id:
|
|
103
|
+
s = str(trace_id)
|
|
104
|
+
i = s.find("/") # session
|
|
105
|
+
j = s.find("/", i + 1) if i != -1 else -1 # page
|
|
106
|
+
if j != -1:
|
|
107
|
+
base_trace = s[:j] # "session/page"
|
|
108
|
+
set_outbound_header_base(
|
|
109
|
+
base_trace=base_trace,
|
|
110
|
+
parent_trace_id=s, # "session/page/uuid"
|
|
111
|
+
funcspan=funcspan_override_header,
|
|
112
|
+
)
|
|
113
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
114
|
+
print(
|
|
115
|
+
f"[[Pyramid.tween]] Initialized outbound header base (base={base_trace[:16]}...)",
|
|
116
|
+
log=False,
|
|
117
|
+
)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
120
|
+
print(
|
|
121
|
+
f"[[Pyramid.tween]] Failed to initialize outbound header base: {e}",
|
|
122
|
+
log=False,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# ── 2) Capture request headers if enabled ────────────────────────
|
|
126
|
+
req_headers = None
|
|
127
|
+
if SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS:
|
|
128
|
+
try:
|
|
129
|
+
req_headers = dict(request.headers)
|
|
130
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
131
|
+
print(
|
|
132
|
+
f"[[Pyramid]] Captured request headers: {len(req_headers)} headers",
|
|
133
|
+
log=False,
|
|
134
|
+
)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
137
|
+
print(
|
|
138
|
+
f"[[Pyramid]] Failed to capture request headers: {e}", log=False
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# ── 3) Capture request body if enabled ────────────────────────────
|
|
142
|
+
req_body = None
|
|
143
|
+
if SF_NETWORKHOP_CAPTURE_REQUEST_BODY:
|
|
144
|
+
try:
|
|
145
|
+
# Pyramid: request.body gives bytes
|
|
146
|
+
body = request.body
|
|
147
|
+
if body:
|
|
148
|
+
req_body = body[:_REQUEST_LIMIT_BYTES]
|
|
149
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
150
|
+
print(
|
|
151
|
+
f"[[Pyramid]] Request body capture: {len(req_body)} bytes",
|
|
152
|
+
log=False,
|
|
153
|
+
)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
156
|
+
print(f"[[Pyramid]] Failed to capture request body: {e}", log=False)
|
|
157
|
+
|
|
158
|
+
# ── 4) OTEL-STYLE: One-shot tracer to capture endpoint metadata and pre-register ──
|
|
159
|
+
endpoint_id = None
|
|
160
|
+
|
|
161
|
+
def tracer(frame, event, _arg):
|
|
162
|
+
nonlocal endpoint_id
|
|
163
|
+
if event != "call": # only Python calls
|
|
164
|
+
return tracer
|
|
165
|
+
fn_path = frame.f_code.co_filename
|
|
166
|
+
if _is_user_code(fn_path):
|
|
167
|
+
# Skip Strawberry GraphQL handlers
|
|
168
|
+
try:
|
|
169
|
+
fn_module = frame.f_globals.get("__name__", "")
|
|
170
|
+
if fn_module.startswith("strawberry"):
|
|
171
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
172
|
+
print(
|
|
173
|
+
f"[[Pyramid]] Skipping endpoint (Strawberry GraphQL handler): {fn_module}",
|
|
174
|
+
log=False,
|
|
175
|
+
)
|
|
176
|
+
sys.setprofile(None)
|
|
177
|
+
return None
|
|
178
|
+
except Exception:
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
hop_key = (fn_path, frame.f_lineno)
|
|
182
|
+
|
|
183
|
+
# Get route pattern if available
|
|
184
|
+
route_pattern = request.path
|
|
185
|
+
|
|
186
|
+
# Check if route should be skipped
|
|
187
|
+
if route_pattern and should_skip_route(route_pattern, _ROUTES_TO_SKIP):
|
|
188
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
189
|
+
print(
|
|
190
|
+
f"[[Pyramid]] Skipping endpoint (route matches skip pattern): {route_pattern}",
|
|
191
|
+
log=False,
|
|
192
|
+
)
|
|
193
|
+
sys.setprofile(None)
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
# Pre-register endpoint if not already registered
|
|
197
|
+
endpoint_id = _ENDPOINT_REGISTRY.get(hop_key)
|
|
198
|
+
if endpoint_id is None:
|
|
199
|
+
endpoint_id = register_endpoint(
|
|
200
|
+
line=str(frame.f_lineno),
|
|
201
|
+
column="0",
|
|
202
|
+
name=frame.f_code.co_name,
|
|
203
|
+
entrypoint=fn_path,
|
|
204
|
+
route=route_pattern,
|
|
205
|
+
)
|
|
206
|
+
if endpoint_id >= 0:
|
|
207
|
+
_ENDPOINT_REGISTRY[hop_key] = endpoint_id
|
|
208
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
209
|
+
print(
|
|
210
|
+
f"[[Pyramid]] Registered endpoint: {frame.f_code.co_name} @ "
|
|
211
|
+
f"{fn_path}:{frame.f_lineno} (id={endpoint_id})",
|
|
212
|
+
log=False,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
216
|
+
print(
|
|
217
|
+
f"[[Pyramid]] Captured endpoint: {frame.f_code.co_name} "
|
|
218
|
+
f"({fn_path}:{frame.f_lineno}) endpoint_id={endpoint_id}",
|
|
219
|
+
log=False,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
sys.setprofile(None) # disable after first capture
|
|
223
|
+
return None
|
|
224
|
+
return tracer
|
|
225
|
+
|
|
226
|
+
sys.setprofile(tracer)
|
|
227
|
+
|
|
228
|
+
# ── 5) Call downstream handler & capture **all** exceptions ─────
|
|
229
|
+
try:
|
|
230
|
+
response = handler(request)
|
|
231
|
+
except Exception as exc: # HTTPException included
|
|
232
|
+
custom_excepthook(type(exc), exc, exc.__traceback__)
|
|
233
|
+
raise # re-raise for Pyramid
|
|
234
|
+
finally:
|
|
235
|
+
sys.setprofile(None) # safety-net cleanup
|
|
236
|
+
|
|
237
|
+
# CRITICAL: Clear C TLS to prevent stale data in thread pools
|
|
238
|
+
clear_c_tls_parent_trace_id()
|
|
239
|
+
|
|
240
|
+
# CRITICAL: Clear outbound header base to prevent stale cached headers
|
|
241
|
+
# ContextVar does NOT automatically clean up in thread pools - must clear explicitly
|
|
242
|
+
clear_outbound_header_base()
|
|
243
|
+
|
|
244
|
+
# CRITICAL: Clear trace_id to ensure fresh generation for next request
|
|
245
|
+
# Without this, get_or_set_sf_trace_id() reuses trace_id from previous request
|
|
246
|
+
# causing X-Sf4-Prid to stay constant when no incoming X-Sf3-Rid header
|
|
247
|
+
clear_trace_id()
|
|
248
|
+
|
|
249
|
+
# CRITICAL: Clear current request path to prevent stale data in thread pools
|
|
250
|
+
clear_current_request_path()
|
|
251
|
+
|
|
252
|
+
# ── 6) Capture response headers if enabled ────────────────────────
|
|
253
|
+
resp_headers = None
|
|
254
|
+
if SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS:
|
|
255
|
+
try:
|
|
256
|
+
resp_headers = dict(response.headers)
|
|
257
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
258
|
+
print(
|
|
259
|
+
f"[[Pyramid]] Captured response headers: {len(resp_headers)} headers",
|
|
260
|
+
log=False,
|
|
261
|
+
)
|
|
262
|
+
except Exception as e:
|
|
263
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
264
|
+
print(
|
|
265
|
+
f"[[Pyramid]] Failed to capture response headers: {e}",
|
|
266
|
+
log=False,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# ── 7) Capture response body if enabled ────────────────────────────
|
|
270
|
+
resp_body = None
|
|
271
|
+
if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY:
|
|
272
|
+
try:
|
|
273
|
+
# Pyramid: response.body gives bytes (or use app_iter for streaming)
|
|
274
|
+
if hasattr(response, "body"):
|
|
275
|
+
body = response.body
|
|
276
|
+
if body:
|
|
277
|
+
resp_body = body[:_RESPONSE_LIMIT_BYTES]
|
|
278
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
279
|
+
print(
|
|
280
|
+
f"[[Pyramid]] Captured response body: {len(resp_body)} bytes",
|
|
281
|
+
log=False,
|
|
282
|
+
)
|
|
283
|
+
except Exception as e:
|
|
284
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
285
|
+
print(
|
|
286
|
+
f"[[Pyramid]] Failed to capture response body: {e}", log=False
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# ── 8) OTEL-STYLE: Emit network hop AFTER handler completes ─────
|
|
290
|
+
if endpoint_id is not None and endpoint_id >= 0:
|
|
291
|
+
try:
|
|
292
|
+
_, session_id = get_or_set_sf_trace_id()
|
|
293
|
+
|
|
294
|
+
# Extract raw path and query string for C to parse
|
|
295
|
+
raw_path = request.path # e.g., "/log"
|
|
296
|
+
raw_query = (
|
|
297
|
+
request.query_string.encode("utf-8")
|
|
298
|
+
if request.query_string
|
|
299
|
+
else b""
|
|
300
|
+
) # e.g., b"foo=5"
|
|
301
|
+
|
|
302
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
303
|
+
print(
|
|
304
|
+
f"[[Pyramid]] About to emit network hop: endpoint_id={endpoint_id}, "
|
|
305
|
+
f"req_headers={'present' if req_headers else 'None'}, "
|
|
306
|
+
f"req_body={len(req_body) if req_body else 0} bytes, "
|
|
307
|
+
f"resp_headers={'present' if resp_headers else 'None'}, "
|
|
308
|
+
f"resp_body={len(resp_body) if resp_body else 0} bytes",
|
|
309
|
+
log=False,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Direct C call - queues to background worker, returns instantly
|
|
313
|
+
# C will parse route and query_params from raw data
|
|
314
|
+
fast_send_network_hop_fast(
|
|
315
|
+
session_id=session_id,
|
|
316
|
+
endpoint_id=endpoint_id,
|
|
317
|
+
raw_path=raw_path,
|
|
318
|
+
raw_query_string=raw_query,
|
|
319
|
+
request_headers=req_headers,
|
|
320
|
+
request_body=req_body,
|
|
321
|
+
response_headers=resp_headers,
|
|
322
|
+
response_body=resp_body,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
326
|
+
print(
|
|
327
|
+
f"[[Pyramid]] Emitted network hop: endpoint_id={endpoint_id} "
|
|
328
|
+
f"session={session_id}",
|
|
329
|
+
log=False,
|
|
330
|
+
)
|
|
331
|
+
except Exception as e: # noqa: BLE001 S110
|
|
332
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
333
|
+
print(f"[[Pyramid]] Failed to emit network hop: {e}", log=False)
|
|
334
|
+
|
|
335
|
+
return response
|
|
336
|
+
|
|
337
|
+
return _tween
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# ------------------------------------------------------------------ #
|
|
341
|
+
# 1.3 Monkey-patch Configurator to auto-add our tween #
|
|
342
|
+
# ------------------------------------------------------------------ #
|
|
343
|
+
def patch_pyramid(routes_to_skip: Optional[List[str]] = None):
|
|
344
|
+
"""
|
|
345
|
+
Ensure every Pyramid Configurator implicitly registers our tween
|
|
346
|
+
at the INVOCATION stage (just above MAIN).
|
|
347
|
+
"""
|
|
348
|
+
global _ROUTES_TO_SKIP
|
|
349
|
+
_ROUTES_TO_SKIP = routes_to_skip or []
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
import pyramid.config
|
|
353
|
+
import pyramid.tweens
|
|
354
|
+
except ImportError:
|
|
355
|
+
return # Pyramid not installed
|
|
356
|
+
|
|
357
|
+
original_init = pyramid.config.Configurator.__init__
|
|
358
|
+
|
|
359
|
+
def patched_init(self, *args, **kwargs):
|
|
360
|
+
original_init(self, *args, **kwargs)
|
|
361
|
+
# Use a dotted name—implicit ordering places it just above MAIN
|
|
362
|
+
dotted = f"{_sf_tracing_tween_factory.__module__}._sf_tracing_tween_factory"
|
|
363
|
+
# 'over=pyramid.tweens.MAIN' ensures our tween runs *before* the main handler
|
|
364
|
+
self.add_tween(dotted, over=pyramid.tweens.MAIN)
|
|
365
|
+
|
|
366
|
+
pyramid.config.Configurator.__init__ = patched_init
|
|
367
|
+
|
|
368
|
+
# ---- CORS patching --------------------------------------------------------
|
|
369
|
+
patch_pyramid_cors()
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def patch_pyramid_cors():
|
|
373
|
+
"""
|
|
374
|
+
Patch Pyramid's Response to automatically inject Sailfish headers into CORS.
|
|
375
|
+
|
|
376
|
+
SAFE: Only modifies Access-Control-Allow-Headers if the application sets it.
|
|
377
|
+
Pyramid doesn't have a standard CORS library built-in, but users often use
|
|
378
|
+
pyramid-cors or set headers manually. We patch Response.headerlist to intercept.
|
|
379
|
+
"""
|
|
380
|
+
try:
|
|
381
|
+
from pyramid.response import Response
|
|
382
|
+
from webob.headers import ResponseHeaders
|
|
383
|
+
except ImportError:
|
|
384
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
385
|
+
print(
|
|
386
|
+
"[[patch_pyramid_cors]] Pyramid Response not available, skipping",
|
|
387
|
+
log=False,
|
|
388
|
+
)
|
|
389
|
+
return
|
|
390
|
+
|
|
391
|
+
# Check if already patched
|
|
392
|
+
if hasattr(ResponseHeaders, "_sf_cors_patched"):
|
|
393
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
394
|
+
print("[[patch_pyramid_cors]] Already patched, skipping", log=False)
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
# Patch ResponseHeaders.__setitem__ to intercept header setting
|
|
398
|
+
# Pyramid uses WebOb's ResponseHeaders for response.headers
|
|
399
|
+
original_setitem = ResponseHeaders.__setitem__
|
|
400
|
+
|
|
401
|
+
def patched_setitem(self, name, value):
|
|
402
|
+
# Intercept Access-Control-Allow-Headers header
|
|
403
|
+
if name.lower() == "access-control-allow-headers":
|
|
404
|
+
if should_inject_headers(value):
|
|
405
|
+
injected = inject_sailfish_headers(value)
|
|
406
|
+
# CRITICAL: Convert list back to comma-separated string for WSGI
|
|
407
|
+
# WSGI requires header values to be strings, not lists
|
|
408
|
+
if isinstance(injected, list):
|
|
409
|
+
value = ", ".join(injected)
|
|
410
|
+
else:
|
|
411
|
+
value = injected
|
|
412
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
413
|
+
print(
|
|
414
|
+
f"[[patch_pyramid_cors]] Injected Sailfish headers into Access-Control-Allow-Headers: {value}",
|
|
415
|
+
log=False,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# Call original __setitem__
|
|
419
|
+
return original_setitem(self, name, value)
|
|
420
|
+
|
|
421
|
+
ResponseHeaders.__setitem__ = patched_setitem
|
|
422
|
+
ResponseHeaders._sf_cors_patched = True
|
|
423
|
+
|
|
424
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
425
|
+
print(
|
|
426
|
+
"[[patch_pyramid_cors]] Successfully patched Pyramid ResponseHeaders.__setitem__",
|
|
427
|
+
log=False,
|
|
428
|
+
)
|