sf-veritas 0.10.3__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.

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