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,683 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CherryPy web framework patch for OTEL-style network hop capture.
|
|
3
|
+
Captures request/response headers and bodies when enabled via env vars.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import inspect
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import threading
|
|
10
|
+
import types
|
|
11
|
+
from typing import Any, Callable, Iterable, List, Optional, Set
|
|
12
|
+
|
|
13
|
+
from ... import _sffuncspan, _sffuncspan_config, app_config
|
|
14
|
+
from ...constants import (
|
|
15
|
+
FUNCSPAN_OVERRIDE_HEADER_BYTES,
|
|
16
|
+
SAILFISH_TRACING_HEADER,
|
|
17
|
+
SAILFISH_TRACING_HEADER_BYTES,
|
|
18
|
+
)
|
|
19
|
+
from ...custom_excepthook import custom_excepthook
|
|
20
|
+
from ...env_vars import (
|
|
21
|
+
SF_DEBUG,
|
|
22
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_BODY,
|
|
23
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS,
|
|
24
|
+
SF_NETWORKHOP_CAPTURE_RESPONSE_BODY,
|
|
25
|
+
SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS,
|
|
26
|
+
SF_NETWORKHOP_REQUEST_LIMIT_MB,
|
|
27
|
+
SF_NETWORKHOP_RESPONSE_LIMIT_MB,
|
|
28
|
+
)
|
|
29
|
+
from ...fast_network_hop import fast_send_network_hop_fast, register_endpoint
|
|
30
|
+
from ...thread_local import (
|
|
31
|
+
clear_c_tls_parent_trace_id,
|
|
32
|
+
clear_current_request_path,
|
|
33
|
+
clear_funcspan_override,
|
|
34
|
+
clear_outbound_header_base,
|
|
35
|
+
clear_trace_id,
|
|
36
|
+
generate_new_trace_id,
|
|
37
|
+
get_or_set_sf_trace_id,
|
|
38
|
+
get_sf_trace_id,
|
|
39
|
+
set_current_request_path,
|
|
40
|
+
set_funcspan_override,
|
|
41
|
+
set_outbound_header_base,
|
|
42
|
+
)
|
|
43
|
+
from .cors_utils import inject_sailfish_headers, should_inject_headers
|
|
44
|
+
from .utils import _is_user_code, should_skip_route, reinitialize_log_print_capture_for_worker
|
|
45
|
+
|
|
46
|
+
# Size limits in bytes
|
|
47
|
+
_REQUEST_LIMIT_BYTES = SF_NETWORKHOP_REQUEST_LIMIT_MB * 1024 * 1024
|
|
48
|
+
_RESPONSE_LIMIT_BYTES = SF_NETWORKHOP_RESPONSE_LIMIT_MB * 1024 * 1024
|
|
49
|
+
|
|
50
|
+
# Pre-registered endpoint IDs
|
|
51
|
+
_ENDPOINT_REGISTRY: dict[tuple, int] = {}
|
|
52
|
+
|
|
53
|
+
# Routes to skip (set by patch_cherrypy)
|
|
54
|
+
_ROUTES_TO_SKIP = []
|
|
55
|
+
|
|
56
|
+
# ------------------------------------------------------------------ #
|
|
57
|
+
# Robust un-wrapper (handles LateParamPageHandler, etc.)
|
|
58
|
+
# ------------------------------------------------------------------ #
|
|
59
|
+
_ATTR_CANDIDATES: Iterable[str] = (
|
|
60
|
+
"resolver",
|
|
61
|
+
"func",
|
|
62
|
+
"python_func",
|
|
63
|
+
"_resolver",
|
|
64
|
+
"wrapped_func",
|
|
65
|
+
"__func",
|
|
66
|
+
"callable", # CherryPy handlers
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _unwrap_user_func(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
71
|
+
"""
|
|
72
|
+
Walk through the layers of wrappers/decorators/handler objects around *fn*
|
|
73
|
+
and return the first plain Python *function* object that:
|
|
74
|
+
• lives in user-land code (per _is_user_code)
|
|
75
|
+
• has a real __code__ object.
|
|
76
|
+
The search is breadth-first and robust to cyclic references.
|
|
77
|
+
"""
|
|
78
|
+
seen: Set[int] = set()
|
|
79
|
+
queue = [fn]
|
|
80
|
+
|
|
81
|
+
while queue:
|
|
82
|
+
current = queue.pop()
|
|
83
|
+
cid = id(current)
|
|
84
|
+
if cid in seen:
|
|
85
|
+
continue
|
|
86
|
+
seen.add(cid)
|
|
87
|
+
|
|
88
|
+
# ── 1. Bound methods (types.MethodType) ──────────────────────────
|
|
89
|
+
# CherryPy's LateParamPageHandler.callable is usually a bound method.
|
|
90
|
+
if isinstance(current, types.MethodType):
|
|
91
|
+
queue.append(current.__func__)
|
|
92
|
+
continue # don't inspect the MethodType itself any further
|
|
93
|
+
|
|
94
|
+
# ── 2. Plain user function? ─────────────────────────────────────
|
|
95
|
+
if inspect.isfunction(current) and _is_user_code(
|
|
96
|
+
getattr(current.__code__, "co_filename", "")
|
|
97
|
+
):
|
|
98
|
+
return current
|
|
99
|
+
|
|
100
|
+
# ── 3. CherryPy PageHandler exposes `.callable` ──────────────────
|
|
101
|
+
target = getattr(current, "callable", None)
|
|
102
|
+
if callable(target):
|
|
103
|
+
queue.append(target)
|
|
104
|
+
|
|
105
|
+
# ── 4. functools.wraps chain (`__wrapped__`) ─────────────────────
|
|
106
|
+
wrapped = getattr(current, "__wrapped__", None)
|
|
107
|
+
if callable(wrapped):
|
|
108
|
+
queue.append(wrapped)
|
|
109
|
+
|
|
110
|
+
# ── 5. Other common wrapper attributes ───────────────────────────
|
|
111
|
+
for attr in _ATTR_CANDIDATES:
|
|
112
|
+
val = getattr(current, attr, None)
|
|
113
|
+
if callable(val):
|
|
114
|
+
queue.append(val)
|
|
115
|
+
|
|
116
|
+
# ── 6. Objects with a user-defined __call__ method ───────────────
|
|
117
|
+
call_attr = getattr(current, "__call__", None)
|
|
118
|
+
if (
|
|
119
|
+
callable(call_attr)
|
|
120
|
+
and inspect.isfunction(call_attr)
|
|
121
|
+
and _is_user_code(getattr(call_attr.__code__, "co_filename", ""))
|
|
122
|
+
):
|
|
123
|
+
queue.append(call_attr)
|
|
124
|
+
|
|
125
|
+
# ── 7. Closure cells inside functions / inner scopes ─────────────
|
|
126
|
+
code_obj = getattr(current, "__code__", None)
|
|
127
|
+
clos = getattr(current, "__closure__", None)
|
|
128
|
+
if code_obj and clos:
|
|
129
|
+
for cell in clos:
|
|
130
|
+
cell_val = cell.cell_contents
|
|
131
|
+
if callable(cell_val):
|
|
132
|
+
queue.append(cell_val)
|
|
133
|
+
|
|
134
|
+
# Fallback: return the original callable (likely framework code)
|
|
135
|
+
return fn
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# 2b. Exception-capture tool (runs *after* an error is detected)
|
|
139
|
+
def _exception_capture_tool():
|
|
140
|
+
"""
|
|
141
|
+
CherryPy calls the ‘before_error_response' hook whenever it is about to
|
|
142
|
+
finalise an error page, regardless of whether the error is a framework
|
|
143
|
+
HTTPError/HTTPRedirect or an uncaught Python exception.
|
|
144
|
+
We tap that hook and forward the traceback to Sailfish.
|
|
145
|
+
"""
|
|
146
|
+
exc_type, exc_value, exc_tb = sys.exc_info()
|
|
147
|
+
if exc_value:
|
|
148
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
149
|
+
print(
|
|
150
|
+
f"[[SFTracingCherryPy]] captured exception: {exc_value!r}",
|
|
151
|
+
log=False,
|
|
152
|
+
)
|
|
153
|
+
custom_excepthook(exc_type, exc_value, exc_tb)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ------------------------------------------------------------------ #
|
|
157
|
+
# Main patch entry-point
|
|
158
|
+
# ------------------------------------------------------------------ #
|
|
159
|
+
def patch_cherrypy(routes_to_skip: Optional[List[str]] = None):
|
|
160
|
+
"""
|
|
161
|
+
• Propagate SAILFISH_TRACING_HEADER header → ContextVar.
|
|
162
|
+
• Emit one NetworkHop for the first *user* handler frame in each request.
|
|
163
|
+
• Capture **all** CherryPy exceptions (HTTPError, HTTPRedirect, uncaught
|
|
164
|
+
Python errors) and forward them to `custom_excepthook`.
|
|
165
|
+
"""
|
|
166
|
+
global _ROUTES_TO_SKIP
|
|
167
|
+
_ROUTES_TO_SKIP = routes_to_skip or []
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
import cherrypy # CherryPy may not be installed
|
|
171
|
+
except ImportError:
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
# Note: Profiler is already installed by unified_interceptor.py
|
|
175
|
+
|
|
176
|
+
# ──────────────────────────────────────────────────────────────────
|
|
177
|
+
# 1. Header propagation – monkey-patch Application.__call__
|
|
178
|
+
# ──────────────────────────────────────────────────────────────────
|
|
179
|
+
env_key = "HTTP_" + SAILFISH_TRACING_HEADER.upper().replace("-", "_")
|
|
180
|
+
funcspan_key = "HTTP_X_SF3_FUNCTIONSPANCAPTUREOVERRIDE"
|
|
181
|
+
|
|
182
|
+
if not getattr(cherrypy.Application, "__sf_hdr_patched__", False):
|
|
183
|
+
orig_call = cherrypy.Application.__call__
|
|
184
|
+
|
|
185
|
+
def patched_call(self, environ, start_response):
|
|
186
|
+
# Set current request path for route-based suppression (SF_DISABLE_INBOUND_NETWORK_TRACING_ON_ROUTES)
|
|
187
|
+
request_path = environ.get("PATH_INFO", "")
|
|
188
|
+
set_current_request_path(request_path)
|
|
189
|
+
|
|
190
|
+
# PERFORMANCE: Single-pass header scan (extract both headers in one pass)
|
|
191
|
+
incoming_trace_raw = environ.get(env_key)
|
|
192
|
+
funcspan_override_header = environ.get(funcspan_key)
|
|
193
|
+
|
|
194
|
+
# CRITICAL: Seed/ensure trace_id immediately (BEFORE any outbound work)
|
|
195
|
+
if incoming_trace_raw:
|
|
196
|
+
# Incoming X-Sf3-Rid header provided - use it
|
|
197
|
+
get_or_set_sf_trace_id(
|
|
198
|
+
incoming_trace_raw, is_associated_with_inbound_request=True
|
|
199
|
+
)
|
|
200
|
+
else:
|
|
201
|
+
# No incoming X-Sf3-Rid header - generate fresh trace_id for this request
|
|
202
|
+
generate_new_trace_id()
|
|
203
|
+
|
|
204
|
+
# Optional funcspan override
|
|
205
|
+
if funcspan_override_header:
|
|
206
|
+
try:
|
|
207
|
+
set_funcspan_override(funcspan_override_header)
|
|
208
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
209
|
+
print(
|
|
210
|
+
f"[[CherryPy.application_call]] Set function span override from header: {funcspan_override_header}",
|
|
211
|
+
log=False,
|
|
212
|
+
)
|
|
213
|
+
except Exception as e:
|
|
214
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
215
|
+
print(
|
|
216
|
+
f"[[CherryPy.application_call]] Failed to set function span override: {e}",
|
|
217
|
+
log=False,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Initialize outbound base without list/allocs from split()
|
|
221
|
+
try:
|
|
222
|
+
trace_id = get_sf_trace_id()
|
|
223
|
+
if trace_id:
|
|
224
|
+
s = str(trace_id)
|
|
225
|
+
i = s.find("/") # session
|
|
226
|
+
j = s.find("/", i + 1) if i != -1 else -1 # page
|
|
227
|
+
if j != -1:
|
|
228
|
+
base_trace = s[:j] # "session/page"
|
|
229
|
+
set_outbound_header_base(
|
|
230
|
+
base_trace=base_trace,
|
|
231
|
+
parent_trace_id=s, # "session/page/uuid"
|
|
232
|
+
funcspan=funcspan_override_header,
|
|
233
|
+
)
|
|
234
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
235
|
+
print(
|
|
236
|
+
f"[[CherryPy.application_call]] Initialized outbound header base (base={base_trace[:16]}...)",
|
|
237
|
+
log=False,
|
|
238
|
+
)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
241
|
+
print(
|
|
242
|
+
f"[[CherryPy.application_call]] Failed to initialize outbound header base: {e}",
|
|
243
|
+
log=False,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# NOTE: Cleanup moved to _emit_network_hop_tool() to ensure trace_id is available for emission
|
|
247
|
+
return orig_call(self, environ, start_response)
|
|
248
|
+
|
|
249
|
+
cherrypy.Application.__call__ = patched_call
|
|
250
|
+
cherrypy.Application.__sf_hdr_patched__ = True
|
|
251
|
+
|
|
252
|
+
# ──────────────────────────────────────────────────────────────────
|
|
253
|
+
# 2a. OTEL-STYLE: Capture endpoint metadata and request data before handler
|
|
254
|
+
# ──────────────────────────────────────────────────────────────────
|
|
255
|
+
def _capture_endpoint_tool():
|
|
256
|
+
"""OTEL-STYLE: Capture endpoint metadata and request data before handler runs."""
|
|
257
|
+
req = cherrypy.serving.request # thread-local current request
|
|
258
|
+
handler = getattr(req, "handler", None)
|
|
259
|
+
if not callable(handler):
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
real_fn = _unwrap_user_func(handler)
|
|
263
|
+
# Skip GraphQL (Strawberry) or non-user code
|
|
264
|
+
if real_fn.__module__.startswith("strawberry"):
|
|
265
|
+
return
|
|
266
|
+
code = getattr(real_fn, "__code__", None)
|
|
267
|
+
if not code or not _is_user_code(code.co_filename):
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
hop_key = (code.co_filename, code.co_firstlineno)
|
|
271
|
+
|
|
272
|
+
# Get route pattern if available
|
|
273
|
+
route_pattern = req.path_info
|
|
274
|
+
|
|
275
|
+
# Check if route should be skipped
|
|
276
|
+
if route_pattern and should_skip_route(route_pattern, _ROUTES_TO_SKIP):
|
|
277
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
278
|
+
print(
|
|
279
|
+
f"[[CherryPy]] Skipping endpoint (route matches skip pattern): {route_pattern}",
|
|
280
|
+
log=False,
|
|
281
|
+
)
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
# Get or register endpoint
|
|
285
|
+
endpoint_id = _ENDPOINT_REGISTRY.get(hop_key)
|
|
286
|
+
if endpoint_id is None:
|
|
287
|
+
endpoint_id = register_endpoint(
|
|
288
|
+
line=str(code.co_firstlineno),
|
|
289
|
+
column="0",
|
|
290
|
+
name=real_fn.__name__,
|
|
291
|
+
entrypoint=code.co_filename,
|
|
292
|
+
route=route_pattern,
|
|
293
|
+
)
|
|
294
|
+
if endpoint_id >= 0:
|
|
295
|
+
_ENDPOINT_REGISTRY[hop_key] = endpoint_id
|
|
296
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
297
|
+
print(
|
|
298
|
+
f"[[CherryPy]] Registered endpoint: {real_fn.__name__} @ {code.co_filename}:{code.co_firstlineno} (id={endpoint_id})",
|
|
299
|
+
log=False,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Store endpoint_id for emission after handler
|
|
303
|
+
req._sf_endpoint_id = endpoint_id
|
|
304
|
+
|
|
305
|
+
# Capture request headers if enabled
|
|
306
|
+
if SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS:
|
|
307
|
+
try:
|
|
308
|
+
req_headers = dict(req.headers)
|
|
309
|
+
req._sf_request_headers = req_headers
|
|
310
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
311
|
+
print(
|
|
312
|
+
f"[[CherryPy]] Captured request headers: {len(req_headers)} headers",
|
|
313
|
+
log=False,
|
|
314
|
+
)
|
|
315
|
+
except Exception as e:
|
|
316
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
317
|
+
print(
|
|
318
|
+
f"[[CherryPy]] Failed to capture request headers: {e}",
|
|
319
|
+
log=False,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Capture request body if enabled
|
|
323
|
+
if SF_NETWORKHOP_CAPTURE_REQUEST_BODY:
|
|
324
|
+
try:
|
|
325
|
+
# CherryPy: request.body is a RequestBody object with a read() method
|
|
326
|
+
# Reading the body consumes it, but CherryPy caches it in request.body.fp
|
|
327
|
+
# First try to get cached body, otherwise read it
|
|
328
|
+
if hasattr(req.body, "fp") and hasattr(req.body.fp, "read"):
|
|
329
|
+
# Save current position
|
|
330
|
+
current_pos = req.body.fp.tell()
|
|
331
|
+
req.body.fp.seek(0)
|
|
332
|
+
body = req.body.fp.read(_REQUEST_LIMIT_BYTES)
|
|
333
|
+
# Restore position so handler can read it
|
|
334
|
+
req.body.fp.seek(current_pos)
|
|
335
|
+
else:
|
|
336
|
+
# Fallback: read directly (this consumes the body)
|
|
337
|
+
body = req.body.read(_REQUEST_LIMIT_BYTES)
|
|
338
|
+
|
|
339
|
+
if body:
|
|
340
|
+
req._sf_request_body = body
|
|
341
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
342
|
+
print(
|
|
343
|
+
f"[[CherryPy]] Request body capture: {len(body)} bytes (method={req.method})",
|
|
344
|
+
log=False,
|
|
345
|
+
)
|
|
346
|
+
except Exception as e:
|
|
347
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
348
|
+
print(
|
|
349
|
+
f"[[CherryPy]] Failed to capture request body: {e}", log=False
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
353
|
+
print(
|
|
354
|
+
f"[[CherryPy]] Captured endpoint: {real_fn.__name__} "
|
|
355
|
+
f"({code.co_filename}:{code.co_firstlineno}) endpoint_id={endpoint_id}",
|
|
356
|
+
log=False,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# OTEL-STYLE: Emit network hop AFTER handler completes with response data
|
|
360
|
+
def _emit_network_hop_tool():
|
|
361
|
+
"""OTEL-STYLE: Emit network hop after handler completes, capturing response data."""
|
|
362
|
+
req = cherrypy.serving.request
|
|
363
|
+
resp = cherrypy.serving.response
|
|
364
|
+
endpoint_id = getattr(req, "_sf_endpoint_id", None)
|
|
365
|
+
|
|
366
|
+
if endpoint_id is not None and endpoint_id >= 0:
|
|
367
|
+
# try:
|
|
368
|
+
# OPTIMIZATION: Use get_sf_trace_id() directly instead of get_or_set_sf_trace_id()
|
|
369
|
+
# Trace ID is GUARANTEED to be set at request start in patched_call
|
|
370
|
+
# This saves ~11-12μs by avoiding tuple unpacking and conditional logic
|
|
371
|
+
# session_id = get_sf_trace_id() # PREVIOUSLY WAS get_sf_trace_id()
|
|
372
|
+
session_id = get_sf_trace_id()
|
|
373
|
+
if session_id is None:
|
|
374
|
+
return # No trace_id available, skip emission
|
|
375
|
+
# C extension expects string, not UUID object
|
|
376
|
+
session_id = str(session_id)
|
|
377
|
+
|
|
378
|
+
# Get captured request data
|
|
379
|
+
req_headers = getattr(req, "_sf_request_headers", None)
|
|
380
|
+
req_body = getattr(req, "_sf_request_body", None)
|
|
381
|
+
|
|
382
|
+
# Capture response headers if enabled
|
|
383
|
+
resp_headers = None
|
|
384
|
+
if SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS:
|
|
385
|
+
try:
|
|
386
|
+
resp_headers = dict(resp.headers)
|
|
387
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
388
|
+
print(
|
|
389
|
+
f"[[CherryPy]] Captured response headers: {len(resp_headers)} headers",
|
|
390
|
+
log=False,
|
|
391
|
+
)
|
|
392
|
+
except Exception as e:
|
|
393
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
394
|
+
print(
|
|
395
|
+
f"[[CherryPy]] Failed to capture response headers: {e}",
|
|
396
|
+
log=False,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# Capture response body if enabled
|
|
400
|
+
resp_body = None
|
|
401
|
+
if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY:
|
|
402
|
+
try:
|
|
403
|
+
# CherryPy response.body is a list of byte strings
|
|
404
|
+
if resp.body:
|
|
405
|
+
if isinstance(resp.body, list):
|
|
406
|
+
body_bytes = b"".join(resp.body)
|
|
407
|
+
elif isinstance(resp.body, bytes):
|
|
408
|
+
body_bytes = resp.body
|
|
409
|
+
elif isinstance(resp.body, str):
|
|
410
|
+
body_bytes = resp.body.encode("utf-8")
|
|
411
|
+
else:
|
|
412
|
+
# Try to iterate
|
|
413
|
+
body_bytes = b"".join(
|
|
414
|
+
(b if isinstance(b, bytes) else str(b).encode("utf-8"))
|
|
415
|
+
for b in resp.body
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
if body_bytes:
|
|
419
|
+
resp_body = body_bytes[:_RESPONSE_LIMIT_BYTES]
|
|
420
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
421
|
+
print(
|
|
422
|
+
f"[[CherryPy]] Captured response body: {len(resp_body)} bytes",
|
|
423
|
+
log=False,
|
|
424
|
+
)
|
|
425
|
+
except Exception as e:
|
|
426
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
427
|
+
print(
|
|
428
|
+
f"[[CherryPy]] Failed to capture response body: {e}",
|
|
429
|
+
log=False,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# Extract raw path and query string for C to parse
|
|
433
|
+
raw_path = req.path_info # e.g., "/log"
|
|
434
|
+
raw_query = (
|
|
435
|
+
req.query_string.encode("utf-8") if req.query_string else b""
|
|
436
|
+
) # e.g., b"foo=5"
|
|
437
|
+
|
|
438
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
439
|
+
print(
|
|
440
|
+
f"[[CherryPy]] About to emit network hop: endpoint_id={endpoint_id}, "
|
|
441
|
+
f"raw_path={raw_path}, ",
|
|
442
|
+
f"raw_query_string={raw_query}, ",
|
|
443
|
+
f"req_headers={'present' if req_headers else 'None'}, "
|
|
444
|
+
f"req_body={len(req_body) if req_body else 0} bytes, "
|
|
445
|
+
f"resp_headers={'present' if resp_headers else 'None'}, "
|
|
446
|
+
f"resp_body={len(resp_body) if resp_body else 0} bytes",
|
|
447
|
+
log=False,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
# Direct C call - queues to background worker, returns instantly
|
|
451
|
+
# C will parse route and query_params from raw data
|
|
452
|
+
fast_send_network_hop_fast(
|
|
453
|
+
session_id=session_id,
|
|
454
|
+
endpoint_id=endpoint_id,
|
|
455
|
+
raw_path=raw_path,
|
|
456
|
+
raw_query_string=raw_query,
|
|
457
|
+
request_headers=req_headers,
|
|
458
|
+
request_body=req_body,
|
|
459
|
+
response_headers=resp_headers,
|
|
460
|
+
response_body=resp_body,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
464
|
+
print(
|
|
465
|
+
f"[[CherryPy]] Emitted network hop: endpoint_id={endpoint_id} "
|
|
466
|
+
f"session={session_id}",
|
|
467
|
+
log=False,
|
|
468
|
+
)
|
|
469
|
+
# except Exception as e: # noqa: BLE001 S110
|
|
470
|
+
# if SF_DEBUG and app_config._interceptors_initialized:
|
|
471
|
+
# print(f"[[CherryPy]] Failed to emit network hop: {e}", log=False)
|
|
472
|
+
|
|
473
|
+
# CRITICAL: Clear C TLS to prevent stale data in thread pools
|
|
474
|
+
# This cleanup MUST happen AFTER emission, not in patched_call's finally block
|
|
475
|
+
# because on_end_request hook runs AFTER the WSGI __call__ returns
|
|
476
|
+
clear_c_tls_parent_trace_id()
|
|
477
|
+
|
|
478
|
+
# CRITICAL: Clear outbound header base to prevent stale cached headers
|
|
479
|
+
# ContextVar does NOT automatically clean up in thread pools - must clear explicitly
|
|
480
|
+
clear_outbound_header_base()
|
|
481
|
+
|
|
482
|
+
# CRITICAL: Clear trace_id to ensure fresh generation for next request
|
|
483
|
+
# Without this, get_or_set_sf_trace_id() reuses trace_id from previous request
|
|
484
|
+
# causing X-Sf4-Prid to stay constant when no incoming X-Sf3-Rid header
|
|
485
|
+
clear_trace_id()
|
|
486
|
+
|
|
487
|
+
# CRITICAL: Clear current request path to prevent stale data in thread pools
|
|
488
|
+
clear_current_request_path()
|
|
489
|
+
|
|
490
|
+
# Clear function span override for this request (ContextVar cleanup - also syncs C thread-local)
|
|
491
|
+
try:
|
|
492
|
+
clear_funcspan_override()
|
|
493
|
+
except Exception:
|
|
494
|
+
pass
|
|
495
|
+
|
|
496
|
+
if not hasattr(cherrypy.tools, "sf_capture_endpoint"):
|
|
497
|
+
cherrypy.tools.sf_capture_endpoint = cherrypy.Tool(
|
|
498
|
+
"before_handler", _capture_endpoint_tool, priority=5
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
if not hasattr(cherrypy.tools, "sf_emit_network_hop"):
|
|
502
|
+
cherrypy.tools.sf_emit_network_hop = cherrypy.Tool(
|
|
503
|
+
"on_end_resource",
|
|
504
|
+
_emit_network_hop_tool,
|
|
505
|
+
priority=5,
|
|
506
|
+
# "on_end_request", _emit_network_hop_tool, priority=5
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
# ──────────────────────────────────────────────────────────────────
|
|
510
|
+
# 2b. Exception-capture tool (runs before error response)
|
|
511
|
+
# ──────────────────────────────────────────────────────────────────
|
|
512
|
+
def _exception_capture_tool():
|
|
513
|
+
exc_type, exc_value, exc_tb = sys.exc_info()
|
|
514
|
+
if exc_value:
|
|
515
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
516
|
+
print(
|
|
517
|
+
f"[[SFTracingCherryPy]] captured exception: {exc_value!r}",
|
|
518
|
+
log=False,
|
|
519
|
+
)
|
|
520
|
+
custom_excepthook(exc_type, exc_value, exc_tb)
|
|
521
|
+
|
|
522
|
+
if not hasattr(cherrypy.tools, "sf_exception_capture"):
|
|
523
|
+
cherrypy.tools.sf_exception_capture = cherrypy.Tool(
|
|
524
|
+
"before_error_response", _exception_capture_tool, priority=100
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
# ──────────────────────────────────────────────────────────────────
|
|
528
|
+
# 3. Enable all tools globally
|
|
529
|
+
# ──────────────────────────────────────────────────────────────────
|
|
530
|
+
cherrypy.config.update(
|
|
531
|
+
{
|
|
532
|
+
"tools.sf_capture_endpoint.on": True,
|
|
533
|
+
"tools.sf_emit_network_hop.on": True,
|
|
534
|
+
"tools.sf_exception_capture.on": True,
|
|
535
|
+
}
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
# ──────────────────────────────────────────────────────────────────
|
|
539
|
+
# 4️⃣ Ensure every new Application inherits the tool settings
|
|
540
|
+
# ──────────────────────────────────────────────────────────────────
|
|
541
|
+
if not getattr(cherrypy.Application, "__sf_app_patched__", False):
|
|
542
|
+
orig_app_init = cherrypy.Application.__init__
|
|
543
|
+
|
|
544
|
+
def patched_app_init(self, root, script_name="", config=None):
|
|
545
|
+
config = config or {}
|
|
546
|
+
root_conf = config.setdefault("/", {})
|
|
547
|
+
root_conf.setdefault("tools.sf_capture_endpoint.on", True)
|
|
548
|
+
root_conf.setdefault("tools.sf_emit_network_hop.on", True)
|
|
549
|
+
root_conf.setdefault("tools.sf_exception_capture.on", True)
|
|
550
|
+
orig_app_init(self, root, script_name, config)
|
|
551
|
+
|
|
552
|
+
cherrypy.Application.__init__ = patched_app_init
|
|
553
|
+
cherrypy.Application.__sf_app_patched__ = True
|
|
554
|
+
|
|
555
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
556
|
+
print(
|
|
557
|
+
"[[patch_cherrypy]] OTEL-style NetworkHop & Exception tools globally enabled",
|
|
558
|
+
log=False,
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
# ── CORS patching ──────────────────────────────────────────────────
|
|
562
|
+
patch_cherrypy_cors()
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def patch_cherrypy_cors():
|
|
566
|
+
"""
|
|
567
|
+
Patch CherryPy's Response to automatically inject Sailfish headers into CORS.
|
|
568
|
+
|
|
569
|
+
SAFE: Only modifies Access-Control-Allow-Headers if the application sets it.
|
|
570
|
+
CherryPy doesn't have a standard CORS library, so we patch Response to intercept
|
|
571
|
+
header setting.
|
|
572
|
+
"""
|
|
573
|
+
try:
|
|
574
|
+
import cherrypy
|
|
575
|
+
from cherrypy._cprequest import Response
|
|
576
|
+
from cherrypy.lib.httputil import HeaderMap
|
|
577
|
+
except ImportError:
|
|
578
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
579
|
+
print("[[patch_cherrypy_cors]] CherryPy not available, skipping", log=False)
|
|
580
|
+
return
|
|
581
|
+
|
|
582
|
+
# Check if already patched (use Response class directly, not thread-local proxy)
|
|
583
|
+
if hasattr(Response, "_sf_cors_patched"):
|
|
584
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
585
|
+
print("[[patch_cherrypy_cors]] Already patched, skipping", log=False)
|
|
586
|
+
return
|
|
587
|
+
|
|
588
|
+
# Patch HeaderMap.__setitem__ to intercept header setting
|
|
589
|
+
# CherryPy uses HeaderMap for response headers
|
|
590
|
+
original_headers_setitem = HeaderMap.__setitem__
|
|
591
|
+
|
|
592
|
+
def patched_headers_setitem(self, name, value):
|
|
593
|
+
# Intercept Access-Control-Allow-Headers header
|
|
594
|
+
if name.lower() == "access-control-allow-headers":
|
|
595
|
+
if should_inject_headers(value):
|
|
596
|
+
injected = inject_sailfish_headers(value)
|
|
597
|
+
# Convert list back to comma-separated string for CherryPy
|
|
598
|
+
if isinstance(injected, list):
|
|
599
|
+
value = ", ".join(injected)
|
|
600
|
+
else:
|
|
601
|
+
value = injected
|
|
602
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
603
|
+
print(
|
|
604
|
+
f"[[patch_cherrypy_cors]] Injected Sailfish headers into Access-Control-Allow-Headers: {value}",
|
|
605
|
+
log=False,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
# Call original __setitem__
|
|
609
|
+
return original_headers_setitem(self, name, value)
|
|
610
|
+
|
|
611
|
+
HeaderMap.__setitem__ = patched_headers_setitem
|
|
612
|
+
Response._sf_cors_patched = True
|
|
613
|
+
|
|
614
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
615
|
+
print(
|
|
616
|
+
"[[patch_cherrypy_cors]] Successfully patched CherryPy Response headers",
|
|
617
|
+
log=False,
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
# ── Patch cherrypy-cors library if installed ────────────────────────
|
|
621
|
+
try:
|
|
622
|
+
import cherrypy_cors
|
|
623
|
+
except ImportError:
|
|
624
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
625
|
+
print(
|
|
626
|
+
"[[patch_cherrypy_cors]] cherrypy-cors not installed, skipping library patch",
|
|
627
|
+
log=False,
|
|
628
|
+
)
|
|
629
|
+
return
|
|
630
|
+
|
|
631
|
+
# Check if already patched
|
|
632
|
+
if hasattr(cherrypy_cors, "_sf_cors_patched"):
|
|
633
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
634
|
+
print(
|
|
635
|
+
"[[patch_cherrypy_cors]] cherrypy-cors already patched, skipping",
|
|
636
|
+
log=False,
|
|
637
|
+
)
|
|
638
|
+
return
|
|
639
|
+
|
|
640
|
+
# Patch the CORS class's _add_prefligt_headers method
|
|
641
|
+
try:
|
|
642
|
+
from cherrypy_cors import CORS
|
|
643
|
+
|
|
644
|
+
original_add_preflight = CORS._add_prefligt_headers
|
|
645
|
+
|
|
646
|
+
def patched_add_preflight(self, allowed_methods, max_age):
|
|
647
|
+
# Call original to set up basic headers
|
|
648
|
+
original_add_preflight(self, allowed_methods, max_age)
|
|
649
|
+
|
|
650
|
+
# Now intercept and enhance the Access-Control-Allow-Headers if it was set
|
|
651
|
+
rh = self.resp_headers
|
|
652
|
+
CORS_ALLOW_HEADERS = "Access-Control-Allow-Headers"
|
|
653
|
+
|
|
654
|
+
if CORS_ALLOW_HEADERS in rh:
|
|
655
|
+
current_value = rh[CORS_ALLOW_HEADERS]
|
|
656
|
+
if should_inject_headers(current_value):
|
|
657
|
+
injected = inject_sailfish_headers(current_value)
|
|
658
|
+
# Convert list back to comma-separated string
|
|
659
|
+
if isinstance(injected, list):
|
|
660
|
+
rh[CORS_ALLOW_HEADERS] = ", ".join(injected)
|
|
661
|
+
else:
|
|
662
|
+
rh[CORS_ALLOW_HEADERS] = injected
|
|
663
|
+
|
|
664
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
665
|
+
print(
|
|
666
|
+
f"[[patch_cherrypy_cors]] Injected Sailfish headers into cherrypy-cors CORS_ALLOW_HEADERS: {rh[CORS_ALLOW_HEADERS]}",
|
|
667
|
+
log=False,
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
CORS._add_prefligt_headers = patched_add_preflight
|
|
671
|
+
cherrypy_cors._sf_cors_patched = True
|
|
672
|
+
|
|
673
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
674
|
+
print(
|
|
675
|
+
"[[patch_cherrypy_cors]] Successfully patched cherrypy-cors library",
|
|
676
|
+
log=False,
|
|
677
|
+
)
|
|
678
|
+
except Exception as e:
|
|
679
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
680
|
+
print(
|
|
681
|
+
f"[[patch_cherrypy_cors]] Failed to patch cherrypy-cors: {e}",
|
|
682
|
+
log=False,
|
|
683
|
+
)
|