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,502 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
from ... import app_config
|
|
4
|
+
from ...constants import (
|
|
5
|
+
FUNCSPAN_OVERRIDE_HEADER_BYTES,
|
|
6
|
+
SAILFISH_TRACING_HEADER,
|
|
7
|
+
SAILFISH_TRACING_HEADER_BYTES,
|
|
8
|
+
)
|
|
9
|
+
from ...custom_excepthook import custom_excepthook
|
|
10
|
+
from ...env_vars import (
|
|
11
|
+
SF_DEBUG,
|
|
12
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_BODY,
|
|
13
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS,
|
|
14
|
+
SF_NETWORKHOP_CAPTURE_RESPONSE_BODY,
|
|
15
|
+
SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS,
|
|
16
|
+
SF_NETWORKHOP_REQUEST_LIMIT_MB,
|
|
17
|
+
SF_NETWORKHOP_RESPONSE_LIMIT_MB,
|
|
18
|
+
)
|
|
19
|
+
from ...fast_network_hop import fast_send_network_hop_fast, register_endpoint
|
|
20
|
+
from ...thread_local import (
|
|
21
|
+
clear_c_tls_parent_trace_id,
|
|
22
|
+
clear_current_request_path,
|
|
23
|
+
clear_outbound_header_base,
|
|
24
|
+
clear_trace_id,
|
|
25
|
+
generate_new_trace_id,
|
|
26
|
+
get_or_set_sf_trace_id,
|
|
27
|
+
get_sf_trace_id,
|
|
28
|
+
set_current_request_path,
|
|
29
|
+
set_funcspan_override,
|
|
30
|
+
set_outbound_header_base,
|
|
31
|
+
)
|
|
32
|
+
from .cors_utils import inject_sailfish_headers, should_inject_headers
|
|
33
|
+
from .utils import _is_user_code, _unwrap_user_func, should_skip_route # cached helpers
|
|
34
|
+
|
|
35
|
+
# Size limits in bytes
|
|
36
|
+
_REQUEST_LIMIT_BYTES = SF_NETWORKHOP_REQUEST_LIMIT_MB * 1024 * 1024
|
|
37
|
+
_RESPONSE_LIMIT_BYTES = SF_NETWORKHOP_RESPONSE_LIMIT_MB * 1024 * 1024
|
|
38
|
+
|
|
39
|
+
# Pre-registered endpoint IDs
|
|
40
|
+
_ENDPOINT_REGISTRY: dict[tuple, int] = {}
|
|
41
|
+
|
|
42
|
+
# Module-level variable for routes to skip (set by patch_bottle)
|
|
43
|
+
_ROUTES_TO_SKIP = []
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ------------------------------------------------------------------------------------
|
|
47
|
+
# 1. Hop-capturing plugin ----------------------------------------------------------------
|
|
48
|
+
# ------------------------------------------------------------------------------------
|
|
49
|
+
class _SFTracingPlugin:
|
|
50
|
+
"""Bottle plugin (API v2) – wraps each route callback exactly once."""
|
|
51
|
+
|
|
52
|
+
name = "sf_network_hop"
|
|
53
|
+
api = 2
|
|
54
|
+
|
|
55
|
+
def apply(self, callback, route):
|
|
56
|
+
# 1. Resolve real user function
|
|
57
|
+
real_fn = _unwrap_user_func(callback)
|
|
58
|
+
mod = real_fn.__module__
|
|
59
|
+
code = getattr(real_fn, "__code__", None)
|
|
60
|
+
|
|
61
|
+
# 2. Skip library frames and Strawberry GraphQL handlers
|
|
62
|
+
if (
|
|
63
|
+
not code
|
|
64
|
+
or not _is_user_code(code.co_filename)
|
|
65
|
+
or mod.startswith("strawberry")
|
|
66
|
+
):
|
|
67
|
+
return callback # no wrapping
|
|
68
|
+
|
|
69
|
+
filename, line_no, fn_name = (
|
|
70
|
+
code.co_filename,
|
|
71
|
+
code.co_firstlineno,
|
|
72
|
+
real_fn.__name__,
|
|
73
|
+
)
|
|
74
|
+
hop_key = (filename, line_no)
|
|
75
|
+
|
|
76
|
+
# Get route pattern from route object
|
|
77
|
+
route_pattern = getattr(route, "rule", None) if route else None
|
|
78
|
+
|
|
79
|
+
# Check if route should be skipped
|
|
80
|
+
if should_skip_route(route_pattern, _ROUTES_TO_SKIP):
|
|
81
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
82
|
+
print(
|
|
83
|
+
f"[[Bottle]] Skipping endpoint (route matches skip pattern): {route_pattern}",
|
|
84
|
+
log=False,
|
|
85
|
+
)
|
|
86
|
+
return callback # no wrapping
|
|
87
|
+
|
|
88
|
+
# Pre-register endpoint
|
|
89
|
+
endpoint_id = _ENDPOINT_REGISTRY.get(hop_key)
|
|
90
|
+
if endpoint_id is None:
|
|
91
|
+
endpoint_id = register_endpoint(
|
|
92
|
+
line=str(line_no),
|
|
93
|
+
column="0",
|
|
94
|
+
name=fn_name,
|
|
95
|
+
entrypoint=filename,
|
|
96
|
+
route=route_pattern,
|
|
97
|
+
)
|
|
98
|
+
if endpoint_id >= 0:
|
|
99
|
+
_ENDPOINT_REGISTRY[hop_key] = endpoint_id
|
|
100
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
101
|
+
print(
|
|
102
|
+
f"[[Bottle]] Registered endpoint: {fn_name} @ {filename}:{line_no} (id={endpoint_id})",
|
|
103
|
+
log=False,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# 3. Wrapper that stores endpoint_id for after_request hook
|
|
107
|
+
from bottle import request # local to avoid hard dep
|
|
108
|
+
|
|
109
|
+
def _wrapped(*args, **kwargs): # noqa: ANN001
|
|
110
|
+
sent = request.environ.setdefault("_sf_hops_sent", set())
|
|
111
|
+
if hop_key not in sent:
|
|
112
|
+
# OTEL-STYLE: Store endpoint_id for after_request hook
|
|
113
|
+
request.environ["_sf_endpoint_id"] = endpoint_id
|
|
114
|
+
sent.add(hop_key)
|
|
115
|
+
|
|
116
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
117
|
+
print(
|
|
118
|
+
f"[[Bottle]] Captured endpoint: {fn_name} ({filename}:{line_no}) endpoint_id={endpoint_id}",
|
|
119
|
+
log=False,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return callback(*args, **kwargs)
|
|
123
|
+
|
|
124
|
+
return _wrapped
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ------------------------------------------------------------------------------------
|
|
128
|
+
# 2. Request hooks: before (header + body capture) + after (OTEL-style emission) ----
|
|
129
|
+
# ------------------------------------------------------------------------------------
|
|
130
|
+
def _install_request_hooks(app):
|
|
131
|
+
from bottle import request, response
|
|
132
|
+
|
|
133
|
+
@app.hook("before_request")
|
|
134
|
+
def _extract_sf_trace_and_capture_request():
|
|
135
|
+
# Set current request path for route-based suppression (SF_DISABLE_INBOUND_NETWORK_TRACING_ON_ROUTES)
|
|
136
|
+
set_current_request_path(request.path)
|
|
137
|
+
|
|
138
|
+
# PERFORMANCE: Single-pass bytes-level header scan (no dict allocation until needed)
|
|
139
|
+
# Scan headers once, only decode what we need, use latin-1 (fast 1:1 byte map)
|
|
140
|
+
incoming_trace_raw = None # bytes
|
|
141
|
+
funcspan_raw = None # bytes
|
|
142
|
+
req_headers = None # dict[str,str] only if capture enabled
|
|
143
|
+
|
|
144
|
+
capture_req_headers = SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS # local cache
|
|
145
|
+
|
|
146
|
+
# Convert Bottle headers to list of tuples for scanning
|
|
147
|
+
# Bottle request.headers is a WSGIHeaderDict, iterate over items
|
|
148
|
+
if capture_req_headers:
|
|
149
|
+
# decode once using latin-1 (1:1 bytes, faster than utf-8 and never throws)
|
|
150
|
+
tmp = {}
|
|
151
|
+
for k, v in request.headers.items():
|
|
152
|
+
k_bytes = k.lower().encode("latin-1")
|
|
153
|
+
v_bytes = v.encode("latin-1")
|
|
154
|
+
if k_bytes == SAILFISH_TRACING_HEADER_BYTES:
|
|
155
|
+
incoming_trace_raw = v_bytes
|
|
156
|
+
elif k_bytes == FUNCSPAN_OVERRIDE_HEADER_BYTES:
|
|
157
|
+
funcspan_raw = v_bytes
|
|
158
|
+
# build the dict while we're here
|
|
159
|
+
tmp[k] = v
|
|
160
|
+
req_headers = tmp
|
|
161
|
+
request.environ["_sf_request_headers"] = req_headers
|
|
162
|
+
else:
|
|
163
|
+
for k, v in request.headers.items():
|
|
164
|
+
k_bytes = k.lower().encode("latin-1")
|
|
165
|
+
if k_bytes == SAILFISH_TRACING_HEADER_BYTES:
|
|
166
|
+
incoming_trace_raw = v.encode("latin-1")
|
|
167
|
+
elif k_bytes == FUNCSPAN_OVERRIDE_HEADER_BYTES:
|
|
168
|
+
funcspan_raw = v.encode("latin-1")
|
|
169
|
+
# no dict build
|
|
170
|
+
request.environ["_sf_request_headers"] = None
|
|
171
|
+
|
|
172
|
+
# CRITICAL: Seed/ensure trace_id immediately (BEFORE any outbound work)
|
|
173
|
+
if incoming_trace_raw:
|
|
174
|
+
# Incoming X-Sf3-Rid header provided - use it
|
|
175
|
+
incoming_trace = incoming_trace_raw.decode("latin-1")
|
|
176
|
+
get_or_set_sf_trace_id(
|
|
177
|
+
incoming_trace, is_associated_with_inbound_request=True
|
|
178
|
+
)
|
|
179
|
+
else:
|
|
180
|
+
# No incoming X-Sf3-Rid header - generate fresh trace_id for this request
|
|
181
|
+
generate_new_trace_id()
|
|
182
|
+
|
|
183
|
+
# Optional funcspan override (decode only if present)
|
|
184
|
+
funcspan_override_header = (
|
|
185
|
+
funcspan_raw.decode("latin-1") if funcspan_raw else None
|
|
186
|
+
)
|
|
187
|
+
if funcspan_override_header:
|
|
188
|
+
try:
|
|
189
|
+
set_funcspan_override(funcspan_override_header)
|
|
190
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
191
|
+
print(
|
|
192
|
+
f"[[Bottle.before_request]] Set function span override from header: {funcspan_override_header}",
|
|
193
|
+
log=False,
|
|
194
|
+
)
|
|
195
|
+
except Exception as e:
|
|
196
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
197
|
+
print(
|
|
198
|
+
f"[[Bottle.before_request]] Failed to set function span override: {e}",
|
|
199
|
+
log=False,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Initialize outbound base without list/allocs from split()
|
|
203
|
+
try:
|
|
204
|
+
trace_id = get_sf_trace_id()
|
|
205
|
+
if trace_id:
|
|
206
|
+
s = str(trace_id)
|
|
207
|
+
i = s.find("/") # session
|
|
208
|
+
j = s.find("/", i + 1) if i != -1 else -1 # page
|
|
209
|
+
if j != -1:
|
|
210
|
+
base_trace = s[:j] # "session/page"
|
|
211
|
+
set_outbound_header_base(
|
|
212
|
+
base_trace=base_trace,
|
|
213
|
+
parent_trace_id=s, # "session/page/uuid"
|
|
214
|
+
funcspan=funcspan_override_header,
|
|
215
|
+
)
|
|
216
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
217
|
+
print(
|
|
218
|
+
f"[[Bottle.before_request]] Initialized outbound header base (base={base_trace[:16]}...)",
|
|
219
|
+
log=False,
|
|
220
|
+
)
|
|
221
|
+
except Exception as e:
|
|
222
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
223
|
+
print(
|
|
224
|
+
f"[[Bottle.before_request]] Failed to initialize outbound header base: {e}",
|
|
225
|
+
log=False,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Capture request body if enabled
|
|
229
|
+
if SF_NETWORKHOP_CAPTURE_REQUEST_BODY:
|
|
230
|
+
try:
|
|
231
|
+
# Bottle request.body is a cached property
|
|
232
|
+
body = request.body.read(_REQUEST_LIMIT_BYTES)
|
|
233
|
+
request.environ["_sf_request_body"] = body if body else None
|
|
234
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
235
|
+
print(
|
|
236
|
+
f"[[Bottle]] Request body capture: {len(body) if body else 0} bytes (method={request.method})",
|
|
237
|
+
log=False,
|
|
238
|
+
)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
241
|
+
print(f"[[Bottle]] Failed to capture request body: {e}", log=False)
|
|
242
|
+
request.environ["_sf_request_body"] = None
|
|
243
|
+
else:
|
|
244
|
+
request.environ["_sf_request_body"] = None
|
|
245
|
+
|
|
246
|
+
@app.hook("after_request")
|
|
247
|
+
def _emit_network_hop():
|
|
248
|
+
"""
|
|
249
|
+
OTEL-STYLE: Emit network hop AFTER response is built.
|
|
250
|
+
Bottle's after_request hook runs after the handler completes.
|
|
251
|
+
Captures response headers/body if enabled.
|
|
252
|
+
"""
|
|
253
|
+
try:
|
|
254
|
+
endpoint_id = request.environ.get("_sf_endpoint_id")
|
|
255
|
+
if endpoint_id is not None and endpoint_id >= 0:
|
|
256
|
+
try:
|
|
257
|
+
_, session_id = get_or_set_sf_trace_id()
|
|
258
|
+
|
|
259
|
+
# Get captured request data
|
|
260
|
+
req_headers = request.environ.get("_sf_request_headers")
|
|
261
|
+
req_body = request.environ.get("_sf_request_body")
|
|
262
|
+
|
|
263
|
+
# Capture response headers if enabled
|
|
264
|
+
resp_headers = None
|
|
265
|
+
if SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS:
|
|
266
|
+
try:
|
|
267
|
+
resp_headers = dict(response.headers)
|
|
268
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
269
|
+
print(
|
|
270
|
+
f"[[Bottle]] Captured response headers: {len(resp_headers)} headers",
|
|
271
|
+
log=False,
|
|
272
|
+
)
|
|
273
|
+
except Exception as e:
|
|
274
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
275
|
+
print(
|
|
276
|
+
f"[[Bottle]] Failed to capture response headers: {e}",
|
|
277
|
+
log=False,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Capture response body if enabled
|
|
281
|
+
resp_body = None
|
|
282
|
+
if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY:
|
|
283
|
+
try:
|
|
284
|
+
# Bottle response.body is bytes
|
|
285
|
+
if hasattr(response, "body") and response.body:
|
|
286
|
+
if isinstance(response.body, bytes):
|
|
287
|
+
resp_body = response.body[:_RESPONSE_LIMIT_BYTES]
|
|
288
|
+
if (
|
|
289
|
+
SF_DEBUG
|
|
290
|
+
and app_config._interceptors_initialized
|
|
291
|
+
):
|
|
292
|
+
print(
|
|
293
|
+
f"[[Bottle]] Captured from body (bytes): {len(resp_body)} bytes",
|
|
294
|
+
log=False,
|
|
295
|
+
)
|
|
296
|
+
elif isinstance(response.body, str):
|
|
297
|
+
resp_body = response.body.encode("utf-8")[
|
|
298
|
+
:_RESPONSE_LIMIT_BYTES
|
|
299
|
+
]
|
|
300
|
+
if (
|
|
301
|
+
SF_DEBUG
|
|
302
|
+
and app_config._interceptors_initialized
|
|
303
|
+
):
|
|
304
|
+
print(
|
|
305
|
+
f"[[Bottle]] Captured from body (str): {len(resp_body)} bytes",
|
|
306
|
+
log=False,
|
|
307
|
+
)
|
|
308
|
+
elif isinstance(response.body, list):
|
|
309
|
+
# Body is a list of bytes
|
|
310
|
+
resp_body = b"".join(response.body)[
|
|
311
|
+
:_RESPONSE_LIMIT_BYTES
|
|
312
|
+
]
|
|
313
|
+
if (
|
|
314
|
+
SF_DEBUG
|
|
315
|
+
and app_config._interceptors_initialized
|
|
316
|
+
):
|
|
317
|
+
print(
|
|
318
|
+
f"[[Bottle]] Captured from body (list): {len(resp_body)} bytes",
|
|
319
|
+
log=False,
|
|
320
|
+
)
|
|
321
|
+
except Exception as e:
|
|
322
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
323
|
+
print(
|
|
324
|
+
f"[[Bottle]] Failed to capture response body: {e}",
|
|
325
|
+
log=False,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Extract raw path and query string for C to parse
|
|
329
|
+
raw_path = request.path # e.g., "/log"
|
|
330
|
+
raw_query = (
|
|
331
|
+
request.query_string.encode("utf-8")
|
|
332
|
+
if request.query_string
|
|
333
|
+
else b""
|
|
334
|
+
) # e.g., b"foo=5"
|
|
335
|
+
|
|
336
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
337
|
+
print(
|
|
338
|
+
f"[[Bottle]] About to emit network hop: endpoint_id={endpoint_id}, "
|
|
339
|
+
f"req_headers={'present' if req_headers else 'None'}, "
|
|
340
|
+
f"req_body={len(req_body) if req_body else 0} bytes, "
|
|
341
|
+
f"resp_headers={'present' if resp_headers else 'None'}, "
|
|
342
|
+
f"resp_body={len(resp_body) if resp_body else 0} bytes",
|
|
343
|
+
log=False,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Direct C call - queues to background worker, returns instantly
|
|
347
|
+
# C will parse route and query_params from raw data
|
|
348
|
+
fast_send_network_hop_fast(
|
|
349
|
+
session_id=session_id,
|
|
350
|
+
endpoint_id=endpoint_id,
|
|
351
|
+
raw_path=raw_path,
|
|
352
|
+
raw_query_string=raw_query,
|
|
353
|
+
request_headers=req_headers,
|
|
354
|
+
request_body=req_body,
|
|
355
|
+
response_headers=resp_headers,
|
|
356
|
+
response_body=resp_body,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
360
|
+
print(
|
|
361
|
+
f"[[Bottle]] Emitted network hop: endpoint_id={endpoint_id} "
|
|
362
|
+
f"session={session_id}",
|
|
363
|
+
log=False,
|
|
364
|
+
)
|
|
365
|
+
except Exception as e: # noqa: BLE001 S110
|
|
366
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
367
|
+
print(f"[[Bottle]] Failed to emit network hop: {e}", log=False)
|
|
368
|
+
finally:
|
|
369
|
+
# CRITICAL: Clear C TLS to prevent stale data in thread pools
|
|
370
|
+
clear_c_tls_parent_trace_id()
|
|
371
|
+
|
|
372
|
+
# CRITICAL: Clear outbound header base to prevent stale cached headers
|
|
373
|
+
# ContextVar does NOT automatically clean up in thread pools - must clear explicitly
|
|
374
|
+
clear_outbound_header_base()
|
|
375
|
+
|
|
376
|
+
# CRITICAL: Clear trace_id to ensure fresh generation for next request
|
|
377
|
+
# Without this, get_or_set_sf_trace_id() reuses trace_id from previous request
|
|
378
|
+
# causing X-Sf4-Prid to stay constant when no incoming X-Sf3-Rid header
|
|
379
|
+
clear_trace_id()
|
|
380
|
+
|
|
381
|
+
# CRITICAL: Clear current request path to prevent stale data in thread pools
|
|
382
|
+
clear_current_request_path()
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
# ------------------------------------------------------------------------------------
|
|
386
|
+
# NEW: Global error-handler wrapper for Bottle
|
|
387
|
+
# ------------------------------------------------------------------------------------
|
|
388
|
+
def _install_error_handler(app):
|
|
389
|
+
"""
|
|
390
|
+
Replace ``app.default_error_handler`` so *any* exception or HTTPError
|
|
391
|
+
(including those raised via ``abort()`` or ``HTTPError(status=500)``)
|
|
392
|
+
is reported to ``custom_excepthook`` before Bottle builds the response.
|
|
393
|
+
|
|
394
|
+
Bottle always funnels errors through this function, regardless of debug
|
|
395
|
+
mode. See Bottle docs on *Error Handlers*.
|
|
396
|
+
"""
|
|
397
|
+
original_handler = app.default_error_handler
|
|
398
|
+
|
|
399
|
+
def _sf_error_handler(error):
|
|
400
|
+
# Forward full traceback (HTTPError keeps it on .__traceback__)
|
|
401
|
+
custom_excepthook(type(error), error, getattr(error, "__traceback__", None))
|
|
402
|
+
return original_handler(error)
|
|
403
|
+
|
|
404
|
+
app.default_error_handler = _sf_error_handler
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
# ------------------------------------------------------------------------------------
|
|
408
|
+
# 3. Public patch function – call this once at startup
|
|
409
|
+
# ------------------------------------------------------------------------------------
|
|
410
|
+
def patch_bottle(routes_to_skip: Optional[List[str]] = None):
|
|
411
|
+
"""
|
|
412
|
+
• Adds before_request header propagation + body/header capture.
|
|
413
|
+
• Installs NetworkHop plugin (covers all current & future routes).
|
|
414
|
+
• Installs after_request hook for OTEL-style network hop emission.
|
|
415
|
+
• Wraps default_error_handler so exceptions (incl. HTTPError 500) are captured.
|
|
416
|
+
Safe no-op if Bottle is not installed or already patched.
|
|
417
|
+
"""
|
|
418
|
+
global _ROUTES_TO_SKIP
|
|
419
|
+
_ROUTES_TO_SKIP = routes_to_skip or []
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
import bottle
|
|
423
|
+
except ImportError: # Bottle absent
|
|
424
|
+
return
|
|
425
|
+
|
|
426
|
+
if getattr(bottle.Bottle, "__sf_tracing_patched__", False):
|
|
427
|
+
return
|
|
428
|
+
|
|
429
|
+
# ---- patch Bottle.__init__ ----------------------------------------------------
|
|
430
|
+
original_init = bottle.Bottle.__init__
|
|
431
|
+
|
|
432
|
+
def patched_init(self, *args, **kwargs):
|
|
433
|
+
original_init(self, *args, **kwargs)
|
|
434
|
+
|
|
435
|
+
# OTEL-STYLE: Install request hooks (before + after)
|
|
436
|
+
_install_request_hooks(self)
|
|
437
|
+
|
|
438
|
+
# Install hop plugin (Plugin API v2 ― applies to all routes, past & future)
|
|
439
|
+
self.install(_SFTracingPlugin())
|
|
440
|
+
|
|
441
|
+
# Exception capture (HTTPError 500 or any uncaught Exception)
|
|
442
|
+
_install_error_handler(self)
|
|
443
|
+
|
|
444
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
445
|
+
print(
|
|
446
|
+
"[[patch_bottle]] OTEL-style hooks + plugin + error handler installed",
|
|
447
|
+
log=False,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
bottle.Bottle.__init__ = patched_init
|
|
451
|
+
bottle.Bottle.__sf_tracing_patched__ = True
|
|
452
|
+
|
|
453
|
+
# ---- CORS patching --------------------------------------------------------
|
|
454
|
+
patch_bottle_cors()
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def patch_bottle_cors():
|
|
458
|
+
"""
|
|
459
|
+
Patch Bottle's Response to automatically inject Sailfish headers into CORS.
|
|
460
|
+
|
|
461
|
+
SAFE: Only modifies Access-Control-Allow-Headers if the application sets it.
|
|
462
|
+
Bottle doesn't have a standard CORS library, so we patch Response.set_header
|
|
463
|
+
to intercept and modify CORS headers.
|
|
464
|
+
"""
|
|
465
|
+
try:
|
|
466
|
+
import bottle
|
|
467
|
+
except ImportError:
|
|
468
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
469
|
+
print("[[patch_bottle_cors]] Bottle not available, skipping", log=False)
|
|
470
|
+
return
|
|
471
|
+
|
|
472
|
+
# Check if already patched
|
|
473
|
+
if hasattr(bottle.Response, "_sf_cors_patched"):
|
|
474
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
475
|
+
print("[[patch_bottle_cors]] Already patched, skipping", log=False)
|
|
476
|
+
return
|
|
477
|
+
|
|
478
|
+
# Patch Response.set_header to intercept and modify Access-Control-Allow-Headers
|
|
479
|
+
original_set_header = bottle.Response.set_header
|
|
480
|
+
|
|
481
|
+
def patched_set_header(self, name, value):
|
|
482
|
+
# Intercept Access-Control-Allow-Headers header
|
|
483
|
+
if name.lower() == "access-control-allow-headers":
|
|
484
|
+
if should_inject_headers(value):
|
|
485
|
+
value = inject_sailfish_headers(value)
|
|
486
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
487
|
+
print(
|
|
488
|
+
"[[patch_bottle_cors]] Injected Sailfish headers into Access-Control-Allow-Headers",
|
|
489
|
+
log=False,
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# Call original set_header
|
|
493
|
+
return original_set_header(self, name, value)
|
|
494
|
+
|
|
495
|
+
bottle.Response.set_header = patched_set_header
|
|
496
|
+
bottle.Response._sf_cors_patched = True
|
|
497
|
+
|
|
498
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
499
|
+
print(
|
|
500
|
+
"[[patch_bottle_cors]] Successfully patched Bottle Response.set_header",
|
|
501
|
+
log=False,
|
|
502
|
+
)
|