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