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.

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