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.
Files changed (141) hide show
  1. sf_veritas/__init__.py +46 -0
  2. sf_veritas/_auto_preload.py +73 -0
  3. sf_veritas/_sfconfig.c +162 -0
  4. sf_veritas/_sfconfig.cpython-314-x86_64-linux-gnu.so +0 -0
  5. sf_veritas/_sfcrashhandler.c +267 -0
  6. sf_veritas/_sfcrashhandler.cpython-314-x86_64-linux-gnu.so +0 -0
  7. sf_veritas/_sffastlog.c +953 -0
  8. sf_veritas/_sffastlog.cpython-314-x86_64-linux-gnu.so +0 -0
  9. sf_veritas/_sffastnet.c +994 -0
  10. sf_veritas/_sffastnet.cpython-314-x86_64-linux-gnu.so +0 -0
  11. sf_veritas/_sffastnetworkrequest.c +727 -0
  12. sf_veritas/_sffastnetworkrequest.cpython-314-x86_64-linux-gnu.so +0 -0
  13. sf_veritas/_sffuncspan.c +2791 -0
  14. sf_veritas/_sffuncspan.cpython-314-x86_64-linux-gnu.so +0 -0
  15. sf_veritas/_sffuncspan_config.c +730 -0
  16. sf_veritas/_sffuncspan_config.cpython-314-x86_64-linux-gnu.so +0 -0
  17. sf_veritas/_sfheadercheck.c +341 -0
  18. sf_veritas/_sfheadercheck.cpython-314-x86_64-linux-gnu.so +0 -0
  19. sf_veritas/_sfnetworkhop.c +1454 -0
  20. sf_veritas/_sfnetworkhop.cpython-314-x86_64-linux-gnu.so +0 -0
  21. sf_veritas/_sfservice.c +1223 -0
  22. sf_veritas/_sfservice.cpython-314-x86_64-linux-gnu.so +0 -0
  23. sf_veritas/_sfteepreload.c +6227 -0
  24. sf_veritas/app_config.py +57 -0
  25. sf_veritas/cli.py +336 -0
  26. sf_veritas/constants.py +10 -0
  27. sf_veritas/custom_excepthook.py +304 -0
  28. sf_veritas/custom_log_handler.py +146 -0
  29. sf_veritas/custom_output_wrapper.py +153 -0
  30. sf_veritas/custom_print.py +153 -0
  31. sf_veritas/django_app.py +5 -0
  32. sf_veritas/env_vars.py +186 -0
  33. sf_veritas/exception_handling_middleware.py +18 -0
  34. sf_veritas/exception_metaclass.py +69 -0
  35. sf_veritas/fast_frame_info.py +116 -0
  36. sf_veritas/fast_network_hop.py +293 -0
  37. sf_veritas/frame_tools.py +112 -0
  38. sf_veritas/funcspan_config_loader.py +693 -0
  39. sf_veritas/function_span_profiler.py +1313 -0
  40. sf_veritas/get_preload_path.py +34 -0
  41. sf_veritas/import_hook.py +62 -0
  42. sf_veritas/infra_details/__init__.py +3 -0
  43. sf_veritas/infra_details/get_infra_details.py +24 -0
  44. sf_veritas/infra_details/kubernetes/__init__.py +3 -0
  45. sf_veritas/infra_details/kubernetes/get_cluster_name.py +147 -0
  46. sf_veritas/infra_details/kubernetes/get_details.py +7 -0
  47. sf_veritas/infra_details/running_on/__init__.py +17 -0
  48. sf_veritas/infra_details/running_on/kubernetes.py +11 -0
  49. sf_veritas/interceptors.py +543 -0
  50. sf_veritas/libsfnettee.so +0 -0
  51. sf_veritas/local_env_detect.py +118 -0
  52. sf_veritas/package_metadata.py +6 -0
  53. sf_veritas/patches/__init__.py +0 -0
  54. sf_veritas/patches/_patch_tracker.py +74 -0
  55. sf_veritas/patches/concurrent_futures.py +19 -0
  56. sf_veritas/patches/constants.py +1 -0
  57. sf_veritas/patches/exceptions.py +82 -0
  58. sf_veritas/patches/multiprocessing.py +32 -0
  59. sf_veritas/patches/network_libraries/__init__.py +99 -0
  60. sf_veritas/patches/network_libraries/aiohttp.py +294 -0
  61. sf_veritas/patches/network_libraries/curl_cffi.py +363 -0
  62. sf_veritas/patches/network_libraries/http_client.py +670 -0
  63. sf_veritas/patches/network_libraries/httpcore.py +580 -0
  64. sf_veritas/patches/network_libraries/httplib2.py +315 -0
  65. sf_veritas/patches/network_libraries/httpx.py +557 -0
  66. sf_veritas/patches/network_libraries/niquests.py +218 -0
  67. sf_veritas/patches/network_libraries/pycurl.py +399 -0
  68. sf_veritas/patches/network_libraries/requests.py +595 -0
  69. sf_veritas/patches/network_libraries/ssl_socket.py +822 -0
  70. sf_veritas/patches/network_libraries/tornado.py +360 -0
  71. sf_veritas/patches/network_libraries/treq.py +270 -0
  72. sf_veritas/patches/network_libraries/urllib_request.py +483 -0
  73. sf_veritas/patches/network_libraries/utils.py +598 -0
  74. sf_veritas/patches/os.py +17 -0
  75. sf_veritas/patches/threading.py +231 -0
  76. sf_veritas/patches/web_frameworks/__init__.py +54 -0
  77. sf_veritas/patches/web_frameworks/aiohttp.py +798 -0
  78. sf_veritas/patches/web_frameworks/async_websocket_consumer.py +337 -0
  79. sf_veritas/patches/web_frameworks/blacksheep.py +532 -0
  80. sf_veritas/patches/web_frameworks/bottle.py +513 -0
  81. sf_veritas/patches/web_frameworks/cherrypy.py +683 -0
  82. sf_veritas/patches/web_frameworks/cors_utils.py +122 -0
  83. sf_veritas/patches/web_frameworks/django.py +963 -0
  84. sf_veritas/patches/web_frameworks/eve.py +401 -0
  85. sf_veritas/patches/web_frameworks/falcon.py +931 -0
  86. sf_veritas/patches/web_frameworks/fastapi.py +738 -0
  87. sf_veritas/patches/web_frameworks/flask.py +526 -0
  88. sf_veritas/patches/web_frameworks/klein.py +501 -0
  89. sf_veritas/patches/web_frameworks/litestar.py +616 -0
  90. sf_veritas/patches/web_frameworks/pyramid.py +440 -0
  91. sf_veritas/patches/web_frameworks/quart.py +841 -0
  92. sf_veritas/patches/web_frameworks/robyn.py +708 -0
  93. sf_veritas/patches/web_frameworks/sanic.py +874 -0
  94. sf_veritas/patches/web_frameworks/starlette.py +742 -0
  95. sf_veritas/patches/web_frameworks/strawberry.py +1446 -0
  96. sf_veritas/patches/web_frameworks/tornado.py +485 -0
  97. sf_veritas/patches/web_frameworks/utils.py +170 -0
  98. sf_veritas/print_override.py +13 -0
  99. sf_veritas/regular_data_transmitter.py +444 -0
  100. sf_veritas/request_interceptor.py +401 -0
  101. sf_veritas/request_utils.py +550 -0
  102. sf_veritas/segfault_handler.py +116 -0
  103. sf_veritas/server_status.py +1 -0
  104. sf_veritas/shutdown_flag.py +11 -0
  105. sf_veritas/subprocess_startup.py +3 -0
  106. sf_veritas/test_cli.py +145 -0
  107. sf_veritas/thread_local.py +1319 -0
  108. sf_veritas/timeutil.py +114 -0
  109. sf_veritas/transmit_exception_to_sailfish.py +28 -0
  110. sf_veritas/transmitter.py +132 -0
  111. sf_veritas/types.py +47 -0
  112. sf_veritas/unified_interceptor.py +1678 -0
  113. sf_veritas/utils.py +39 -0
  114. sf_veritas-0.11.10.dist-info/METADATA +97 -0
  115. sf_veritas-0.11.10.dist-info/RECORD +141 -0
  116. sf_veritas-0.11.10.dist-info/WHEEL +5 -0
  117. sf_veritas-0.11.10.dist-info/entry_points.txt +2 -0
  118. sf_veritas-0.11.10.dist-info/top_level.txt +1 -0
  119. sf_veritas.libs/libbrotlicommon-6ce2a53c.so.1.0.6 +0 -0
  120. sf_veritas.libs/libbrotlidec-811d1be3.so.1.0.6 +0 -0
  121. sf_veritas.libs/libcom_err-730ca923.so.2.1 +0 -0
  122. sf_veritas.libs/libcrypt-52aca757.so.1.1.0 +0 -0
  123. sf_veritas.libs/libcrypto-bdaed0ea.so.1.1.1k +0 -0
  124. sf_veritas.libs/libcurl-eaa3cf66.so.4.5.0 +0 -0
  125. sf_veritas.libs/libgssapi_krb5-323bbd21.so.2.2 +0 -0
  126. sf_veritas.libs/libidn2-2f4a5893.so.0.3.6 +0 -0
  127. sf_veritas.libs/libk5crypto-9a74ff38.so.3.1 +0 -0
  128. sf_veritas.libs/libkeyutils-2777d33d.so.1.6 +0 -0
  129. sf_veritas.libs/libkrb5-a55300e8.so.3.3 +0 -0
  130. sf_veritas.libs/libkrb5support-e6594cfc.so.0.1 +0 -0
  131. sf_veritas.libs/liblber-2-d20824ef.4.so.2.10.9 +0 -0
  132. sf_veritas.libs/libldap-2-cea2a960.4.so.2.10.9 +0 -0
  133. sf_veritas.libs/libnghttp2-39367a22.so.14.17.0 +0 -0
  134. sf_veritas.libs/libpcre2-8-516f4c9d.so.0.7.1 +0 -0
  135. sf_veritas.libs/libpsl-99becdd3.so.5.3.1 +0 -0
  136. sf_veritas.libs/libsasl2-7de4d792.so.3.0.0 +0 -0
  137. sf_veritas.libs/libselinux-d0805dcb.so.1 +0 -0
  138. sf_veritas.libs/libssh-c11d285b.so.4.8.7 +0 -0
  139. sf_veritas.libs/libssl-60250281.so.1.1.1k +0 -0
  140. sf_veritas.libs/libunistring-05abdd40.so.2.1.0 +0 -0
  141. 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
+ )