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,824 @@
1
+ """
2
+ • SFTracingQuartASGIMiddleware: pulls SAILFISH_TRACING_HEADER into your ContextVar.
3
+ • patch_quart(): wraps Quart.__init__, installs middleware and
4
+ redefines .route so that each user-land view emits one NetworkHop.
5
+ """
6
+
7
+ import asyncio
8
+ import inspect
9
+ import sysconfig
10
+ from functools import lru_cache, wraps
11
+ from typing import Any, Callable, List, Optional, Set, Tuple
12
+
13
+ from ... import 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_outbound_header_base,
34
+ clear_trace_id,
35
+ generate_new_trace_id,
36
+ get_or_set_sf_trace_id,
37
+ get_sf_trace_id,
38
+ set_current_request_path,
39
+ set_funcspan_override,
40
+ set_outbound_header_base,
41
+ )
42
+ from .cors_utils import inject_sailfish_headers, should_inject_headers
43
+ from .utils import _unwrap_user_func # your cached helpers
44
+ from .utils import _is_user_code, should_skip_route
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
+ # Module-level variable for routes to skip (set by patch_quart)
54
+ _ROUTES_TO_SKIP = []
55
+
56
+ try:
57
+ import quart
58
+ from quart.app import Quart
59
+ from quart.wrappers import Response
60
+ except ImportError:
61
+ # Quart not installed → no-op
62
+ def patch_quart(routes_to_skip: Optional[List[str]] = None):
63
+ return
64
+
65
+ else:
66
+ # ──────────────────────────────────────────────────────────
67
+ # OTEL-STYLE: Request hooks (before + after) - Flask-style
68
+ # ──────────────────────────────────────────────────────────
69
+ def _install_request_hooks(app):
70
+ """Install Flask-style before/after request hooks for Quart."""
71
+ from quart import g, request
72
+
73
+ @app.before_request
74
+ async def _extract_sf_header():
75
+ """OTEL-STYLE: Extract trace header and capture request data before handler."""
76
+ # Set current request path for route-based suppression (SF_DISABLE_INBOUND_NETWORK_TRACING_ON_ROUTES)
77
+ set_current_request_path(request.path)
78
+
79
+ rid = request.headers.get(SAILFISH_TRACING_HEADER)
80
+ if rid:
81
+ get_or_set_sf_trace_id(rid, is_associated_with_inbound_request=True)
82
+ else:
83
+ # No incoming header - generate fresh trace_id for this request
84
+ generate_new_trace_id()
85
+
86
+ # Check for function span capture override header (highest priority!)
87
+ funcspan_override_header = request.headers.get(
88
+ "X-Sf3-FunctionSpanCaptureOverride"
89
+ )
90
+ if funcspan_override_header:
91
+ try:
92
+ set_funcspan_override(funcspan_override_header)
93
+ if SF_DEBUG and app_config._interceptors_initialized:
94
+ print(
95
+ f"[[Quart.before_request]] Set function span override from header: {funcspan_override_header}",
96
+ log=False,
97
+ )
98
+ except Exception as e:
99
+ if SF_DEBUG and app_config._interceptors_initialized:
100
+ print(
101
+ f"[[Quart.before_request]] Failed to set function span override: {e}",
102
+ log=False,
103
+ )
104
+
105
+ # Initialize outbound base without list/allocs from split()
106
+ try:
107
+ trace_id = get_sf_trace_id()
108
+ if trace_id:
109
+ s = str(trace_id)
110
+ i = s.find("/") # session
111
+ j = s.find("/", i + 1) if i != -1 else -1 # page
112
+ if j != -1:
113
+ base_trace = s[:j] # "session/page"
114
+ set_outbound_header_base(
115
+ base_trace=base_trace,
116
+ parent_trace_id=s, # "session/page/uuid"
117
+ funcspan=funcspan_override_header,
118
+ )
119
+ if SF_DEBUG and app_config._interceptors_initialized:
120
+ print(
121
+ f"[[Quart.before_request]] Initialized outbound header base (base={base_trace[:16]}...)",
122
+ log=False,
123
+ )
124
+ except Exception as e:
125
+ if SF_DEBUG and app_config._interceptors_initialized:
126
+ print(
127
+ f"[[Quart.before_request]] Failed to initialize outbound header base: {e}",
128
+ log=False,
129
+ )
130
+
131
+ # Capture request headers if enabled
132
+ if SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS:
133
+ try:
134
+ req_headers = dict(request.headers)
135
+ g._sf_request_headers = req_headers
136
+ if SF_DEBUG and app_config._interceptors_initialized:
137
+ print(
138
+ f"[[Quart]] Captured request headers: {len(req_headers)} headers",
139
+ log=False,
140
+ )
141
+ except Exception as e:
142
+ if SF_DEBUG and app_config._interceptors_initialized:
143
+ print(
144
+ f"[[Quart]] Failed to capture request headers: {e}",
145
+ log=False,
146
+ )
147
+
148
+ # Capture request body if enabled
149
+ if SF_NETWORKHOP_CAPTURE_REQUEST_BODY:
150
+ try:
151
+ # Quart: await request.get_data() gets raw bytes
152
+ body = await request.get_data()
153
+ if body:
154
+ req_body = body[:_REQUEST_LIMIT_BYTES]
155
+ g._sf_request_body = req_body
156
+ if SF_DEBUG and app_config._interceptors_initialized:
157
+ print(
158
+ f"[[Quart]] Request body capture: {len(req_body)} bytes",
159
+ log=False,
160
+ )
161
+ except Exception as e:
162
+ if SF_DEBUG and app_config._interceptors_initialized:
163
+ print(
164
+ f"[[Quart]] Failed to capture request body: {e}", log=False
165
+ )
166
+
167
+ @app.after_request
168
+ async def _emit_network_hop(response):
169
+ """
170
+ OTEL-STYLE: Emit network hop AFTER response is built.
171
+ Quart is Flask-based, so we use the same @after_request pattern.
172
+ """
173
+ endpoint_id = getattr(g, "_sf_endpoint_id", None)
174
+ if endpoint_id is not None and endpoint_id >= 0:
175
+ try:
176
+ _, session_id = get_or_set_sf_trace_id()
177
+
178
+ # Get captured request data
179
+ req_headers = getattr(g, "_sf_request_headers", None)
180
+ req_body = getattr(g, "_sf_request_body", None)
181
+
182
+ # Capture response headers if enabled
183
+ resp_headers = None
184
+ if SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS:
185
+ try:
186
+ resp_headers = dict(response.headers)
187
+ if SF_DEBUG and app_config._interceptors_initialized:
188
+ print(
189
+ f"[[Quart]] Captured response headers: {len(resp_headers)} headers",
190
+ log=False,
191
+ )
192
+ except Exception as e:
193
+ if SF_DEBUG and app_config._interceptors_initialized:
194
+ print(
195
+ f"[[Quart]] Failed to capture response headers: {e}",
196
+ log=False,
197
+ )
198
+
199
+ # Capture response body if enabled
200
+ resp_body = None
201
+ if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY:
202
+ try:
203
+ # Quart response: get_data() returns bytes (async)
204
+ body = await response.get_data()
205
+ if body:
206
+ resp_body = body[:_RESPONSE_LIMIT_BYTES]
207
+ if SF_DEBUG and app_config._interceptors_initialized:
208
+ print(
209
+ f"[[Quart]] Captured response body: {len(resp_body)} bytes",
210
+ log=False,
211
+ )
212
+ except Exception as e:
213
+ if SF_DEBUG and app_config._interceptors_initialized:
214
+ print(
215
+ f"[[Quart]] Failed to capture response body: {e}",
216
+ log=False,
217
+ )
218
+
219
+ # Extract raw path and query string for C to parse
220
+ raw_path = request.path # e.g., "/log"
221
+ raw_query = request.query_string # Already bytes (e.g., b"foo=5")
222
+
223
+ if SF_DEBUG and app_config._interceptors_initialized:
224
+ print(
225
+ f"[[Quart]] About to emit network hop: endpoint_id={endpoint_id}, "
226
+ f"req_headers={'present' if req_headers else 'None'}, "
227
+ f"req_body={len(req_body) if req_body else 0} bytes, "
228
+ f"resp_headers={'present' if resp_headers else 'None'}, "
229
+ f"resp_body={len(resp_body) if resp_body else 0} bytes",
230
+ log=False,
231
+ )
232
+
233
+ # Direct C call - queues to background worker, returns instantly
234
+ # C will parse route and query_params from raw data
235
+ fast_send_network_hop_fast(
236
+ session_id=session_id,
237
+ endpoint_id=endpoint_id,
238
+ raw_path=raw_path,
239
+ raw_query_string=raw_query,
240
+ request_headers=req_headers,
241
+ request_body=req_body,
242
+ response_headers=resp_headers,
243
+ response_body=resp_body,
244
+ )
245
+
246
+ if SF_DEBUG and app_config._interceptors_initialized:
247
+ print(
248
+ f"[[Quart]] Emitted network hop: endpoint_id={endpoint_id} "
249
+ f"session={session_id}",
250
+ log=False,
251
+ )
252
+ except Exception as e:
253
+ if SF_DEBUG and app_config._interceptors_initialized:
254
+ print(f"[[Quart]] Failed to emit network hop: {e}", log=False)
255
+
256
+ # CRITICAL: Clear C TLS to prevent stale data in thread pools
257
+ clear_c_tls_parent_trace_id()
258
+
259
+ # CRITICAL: Clear outbound header base to prevent stale cached headers
260
+ # ContextVar does NOT automatically clean up in thread pools - must clear explicitly
261
+ clear_outbound_header_base()
262
+
263
+ # CRITICAL: Clear trace_id to ensure fresh generation for next request
264
+ # Without this, get_or_set_sf_trace_id() reuses trace_id from previous request
265
+ # causing X-Sf4-Prid to stay constant when no incoming X-Sf3-Rid header
266
+ clear_trace_id()
267
+
268
+ # CRITICAL: Clear current request path to prevent stale data in thread pools
269
+ clear_current_request_path()
270
+
271
+ return response
272
+
273
+ # ──────────────────────────────────────────────────────────
274
+ # OTEL-STYLE: Per-view endpoint metadata capture
275
+ # ──────────────────────────────────────────────────────────
276
+ def _hop_wrapper(view_fn: Callable, route: str = None):
277
+ """
278
+ OTEL-STYLE: Pre-register endpoint and store endpoint_id in quart.g.
279
+ Emission happens in @after_request hook with captured body/headers.
280
+ """
281
+ from quart import g
282
+
283
+ real_fn = _unwrap_user_func(view_fn)
284
+
285
+ code = getattr(real_fn, "__code__", None)
286
+ if not code or not _is_user_code(code.co_filename):
287
+ return view_fn
288
+
289
+ # Skip Strawberry GraphQL handlers
290
+ if getattr(real_fn, "__module__", "").startswith("strawberry"):
291
+ return view_fn
292
+
293
+ # Check if route should be skipped
294
+ if should_skip_route(route, _ROUTES_TO_SKIP):
295
+ if SF_DEBUG and app_config._interceptors_initialized:
296
+ print(
297
+ f"[[Quart]] Skipping endpoint (route matches skip pattern): {route}",
298
+ log=False,
299
+ )
300
+ return view_fn # Return original function unwrapped - no telemetry
301
+
302
+ hop_key = (code.co_filename, code.co_firstlineno)
303
+ fn_name = real_fn.__name__
304
+ filename = code.co_filename
305
+ line_no = code.co_firstlineno
306
+
307
+ # Pre-register endpoint if user code
308
+ endpoint_id = _ENDPOINT_REGISTRY.get(hop_key)
309
+ if endpoint_id is None:
310
+ endpoint_id = register_endpoint(
311
+ line=str(line_no),
312
+ column="0",
313
+ name=fn_name,
314
+ entrypoint=filename,
315
+ route=route,
316
+ )
317
+ if endpoint_id >= 0:
318
+ _ENDPOINT_REGISTRY[hop_key] = endpoint_id
319
+ if SF_DEBUG and app_config._interceptors_initialized:
320
+ print(
321
+ f"[[Quart]] Registered endpoint: {fn_name} @ {filename}:{line_no} (id={endpoint_id})",
322
+ log=False,
323
+ )
324
+
325
+ @wraps(view_fn)
326
+ async def _wrapped(*args, **kwargs):
327
+ # OTEL-STYLE: Store endpoint_id for after_request to emit
328
+ if not hasattr(g, "_sf_endpoint_id"):
329
+ g._sf_endpoint_id = endpoint_id
330
+
331
+ if SF_DEBUG and app_config._interceptors_initialized:
332
+ print(
333
+ f"[[Quart]] Captured endpoint: {fn_name} ({filename}:{line_no}) endpoint_id={endpoint_id}",
334
+ log=False,
335
+ )
336
+
337
+ return await view_fn(*args, **kwargs)
338
+
339
+ return _wrapped
340
+
341
+ def _patch_add_route(cls):
342
+ """
343
+ Patch add_url_rule on Quart so that the final stored endpoint function
344
+ is wrapped after Quart has done its own bookkeeping.
345
+ """
346
+ original_add = cls.add_url_rule
347
+
348
+ def patched_add(self, rule, endpoint=None, view_func=None, **options):
349
+ # let Quart register the route first
350
+ original_add(self, rule, endpoint=endpoint, view_func=view_func, **options)
351
+
352
+ ep = endpoint or (view_func and view_func.__name__)
353
+ if not ep: # defensive
354
+ return
355
+
356
+ target = self.view_functions.get(ep)
357
+ if callable(target):
358
+ self.view_functions[ep] = _hop_wrapper(target, route=rule)
359
+
360
+ cls.add_url_rule = patched_add
361
+
362
+ # ──────────────────────────────────────────────────────────
363
+ # ASGI middleware - TRUE ZERO OVERHEAD (emits AFTER response sent)
364
+ # ──────────────────────────────────────────────────────────
365
+ class SFZeroOverheadQuartMiddleware:
366
+ """
367
+ OTEL-STYLE ZERO-OVERHEAD network hop capture middleware.
368
+
369
+ - Propagates inbound SAILFISH_TRACING_HEADER → ContextVar
370
+ - Pre-registers endpoints at startup for ultra-fast emission
371
+ - Captures request/response headers and body when enabled
372
+ - Emits NetworkHop AFTER response sent (pure async, no blocking)
373
+ - Funnels all exceptions through custom_excepthook
374
+ """
375
+
376
+ def __init__(self, app):
377
+ self.app = app
378
+ self._endpoint_cache = {} # Cache endpoint_id by function id
379
+
380
+ async def __call__(self, scope, receive, send):
381
+ if scope.get("type") != "http":
382
+ await self.app(scope, receive, send)
383
+ return
384
+
385
+ # PERFORMANCE: Single-pass bytes-level header scan (no dict allocation until needed)
386
+ # Scan headers once on bytes, only decode what we need, use latin-1 (fast 1:1 byte map)
387
+ hdr_tuples = scope.get("headers") or ()
388
+ incoming_trace_raw = None # bytes
389
+ funcspan_raw = None # bytes
390
+ req_headers = None # dict[str,str] only if capture enabled
391
+
392
+ capture_req_headers = SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS # local cache
393
+
394
+ if capture_req_headers:
395
+ # decode once using latin-1 (1:1 bytes, faster than utf-8 and never throws)
396
+ tmp = {}
397
+ for k, v in hdr_tuples:
398
+ kl = k.lower()
399
+ if kl == SAILFISH_TRACING_HEADER_BYTES:
400
+ incoming_trace_raw = v
401
+ elif kl == FUNCSPAN_OVERRIDE_HEADER_BYTES:
402
+ funcspan_raw = v
403
+ # build the dict while we're here
404
+ tmp[k.decode("latin-1")] = v.decode("latin-1")
405
+ req_headers = tmp
406
+ else:
407
+ for k, v in hdr_tuples:
408
+ kl = k.lower()
409
+ if kl == SAILFISH_TRACING_HEADER_BYTES:
410
+ incoming_trace_raw = v
411
+ elif kl == FUNCSPAN_OVERRIDE_HEADER_BYTES:
412
+ funcspan_raw = v
413
+ # no dict build
414
+
415
+ # CRITICAL: Seed/ensure trace_id immediately (BEFORE any outbound work)
416
+ if incoming_trace_raw:
417
+ # Incoming X-Sf3-Rid header provided - use it
418
+ incoming_trace = incoming_trace_raw.decode("latin-1")
419
+ get_or_set_sf_trace_id(
420
+ incoming_trace, is_associated_with_inbound_request=True
421
+ )
422
+ else:
423
+ # No incoming X-Sf3-Rid header - generate fresh trace_id for this request
424
+ generate_new_trace_id()
425
+
426
+ # Optional funcspan override (decode only if present)
427
+ funcspan_override_header = (
428
+ funcspan_raw.decode("latin-1") if funcspan_raw else None
429
+ )
430
+ if funcspan_override_header:
431
+ try:
432
+ set_funcspan_override(funcspan_override_header)
433
+ if SF_DEBUG and app_config._interceptors_initialized:
434
+ print(
435
+ f"[[Quart.middleware]] Set function span override from header: {funcspan_override_header}",
436
+ log=False,
437
+ )
438
+ except Exception as e:
439
+ if SF_DEBUG and app_config._interceptors_initialized:
440
+ print(
441
+ f"[[Quart.middleware]] Failed to set function span override: {e}",
442
+ log=False,
443
+ )
444
+
445
+ # Initialize outbound base without list/allocs from split()
446
+ try:
447
+ trace_id = get_sf_trace_id()
448
+ if trace_id:
449
+ s = str(trace_id)
450
+ i = s.find("/") # session
451
+ j = s.find("/", i + 1) if i != -1 else -1 # page
452
+ if j != -1:
453
+ base_trace = s[:j] # "session/page"
454
+ set_outbound_header_base(
455
+ base_trace=base_trace,
456
+ parent_trace_id=s, # "session/page/uuid"
457
+ funcspan=funcspan_override_header,
458
+ )
459
+ if SF_DEBUG and app_config._interceptors_initialized:
460
+ print(
461
+ f"[[Quart.middleware]] Initialized outbound header base (base={base_trace[:16]}...)",
462
+ log=False,
463
+ )
464
+ except Exception as e:
465
+ if SF_DEBUG and app_config._interceptors_initialized:
466
+ print(
467
+ f"[[Quart.middleware]] Failed to initialize outbound header base: {e}",
468
+ log=False,
469
+ )
470
+
471
+ # Pre-register endpoint and get endpoint_id
472
+ endpoint_id = None
473
+ endpoint_fn = scope.get("endpoint")
474
+
475
+ if SF_DEBUG and app_config._interceptors_initialized:
476
+ print(
477
+ f"[[Quart]] endpoint_fn={endpoint_fn}, type={type(endpoint_fn) if endpoint_fn else None}",
478
+ log=False,
479
+ )
480
+
481
+ if endpoint_fn:
482
+ # Check cache first
483
+ fn_id = id(endpoint_fn)
484
+ if fn_id in self._endpoint_cache:
485
+ endpoint_id = self._endpoint_cache[fn_id]
486
+ if SF_DEBUG and app_config._interceptors_initialized:
487
+ print(
488
+ f"[[Quart]] Using cached endpoint_id={endpoint_id}",
489
+ log=False,
490
+ )
491
+ else:
492
+ # Extract metadata and register
493
+ user_fn = _unwrap_user_func(endpoint_fn)
494
+ code = getattr(user_fn, "__code__", None)
495
+
496
+ if SF_DEBUG and app_config._interceptors_initialized:
497
+ print(
498
+ f"[[Quart]] user_fn={user_fn.__name__ if hasattr(user_fn, '__name__') else user_fn}, code={code}, is_user_code={_is_user_code(code.co_filename) if code else False}",
499
+ log=False,
500
+ )
501
+
502
+ # Skip Strawberry GraphQL handlers
503
+ if getattr(user_fn, "__module__", "").startswith("strawberry"):
504
+ if SF_DEBUG and app_config._interceptors_initialized:
505
+ print(
506
+ f"[[Quart]] Skipping Strawberry GraphQL endpoint",
507
+ log=False,
508
+ )
509
+ # Don't register, don't cache
510
+ elif code and _is_user_code(code.co_filename):
511
+ hop_key = (code.co_filename, code.co_firstlineno)
512
+
513
+ # Check global registry first
514
+ endpoint_id = _ENDPOINT_REGISTRY.get(hop_key)
515
+ if endpoint_id is None:
516
+ endpoint_id = register_endpoint(
517
+ line=str(code.co_firstlineno),
518
+ column="0",
519
+ name=user_fn.__name__,
520
+ entrypoint=code.co_filename,
521
+ route=None,
522
+ )
523
+ if endpoint_id >= 0:
524
+ _ENDPOINT_REGISTRY[hop_key] = endpoint_id
525
+ if SF_DEBUG and app_config._interceptors_initialized:
526
+ print(
527
+ f"[[Quart]] Registered endpoint: {user_fn.__name__} @ {code.co_filename}:{code.co_firstlineno} (id={endpoint_id})",
528
+ log=False,
529
+ )
530
+ else:
531
+ if SF_DEBUG and app_config._interceptors_initialized:
532
+ print(
533
+ f"[[Quart]] Failed to register endpoint (returned {endpoint_id})",
534
+ log=False,
535
+ )
536
+ else:
537
+ if SF_DEBUG and app_config._interceptors_initialized:
538
+ print(
539
+ f"[[Quart]] Using pre-registered endpoint_id={endpoint_id}",
540
+ log=False,
541
+ )
542
+
543
+ # Cache by function id for fast lookup
544
+ self._endpoint_cache[fn_id] = endpoint_id
545
+
546
+ if SF_DEBUG and app_config._interceptors_initialized:
547
+ print(f"[[Quart]] Final endpoint_id={endpoint_id}", log=False)
548
+
549
+ # NOTE: req_headers already captured in single-pass scan above (if enabled)
550
+
551
+ # Capture request body if enabled
552
+ body_parts = []
553
+ body_size = 0
554
+ if SF_NETWORKHOP_CAPTURE_REQUEST_BODY:
555
+ try:
556
+ # Save original receive before wrapping
557
+ original_receive = receive
558
+
559
+ async def receive_with_body():
560
+ nonlocal body_size
561
+ message = await original_receive()
562
+ if message["type"] == "http.request":
563
+ body_part = message.get("body", b"")
564
+ if body_part and body_size < _REQUEST_LIMIT_BYTES:
565
+ remaining = _REQUEST_LIMIT_BYTES - body_size
566
+ body_parts.append(body_part[:remaining])
567
+ body_size += len(body_part)
568
+ return message
569
+
570
+ receive = receive_with_body
571
+
572
+ except Exception as e:
573
+ if SF_DEBUG and app_config._interceptors_initialized:
574
+ print(
575
+ f"[[Quart]] Failed to setup request body capture: {e}",
576
+ log=False,
577
+ )
578
+
579
+ # Capture response headers and body
580
+ resp_headers = None
581
+ resp_body_parts = []
582
+ resp_body_size = 0
583
+
584
+ # OTEL-STYLE: Wrap send to capture response data and emit AFTER response sent
585
+ async def wrapped_send(message):
586
+ nonlocal resp_headers, resp_body_size
587
+
588
+ # Capture response headers
589
+ if (
590
+ message["type"] == "http.response.start"
591
+ and SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS
592
+ ):
593
+ try:
594
+ headers = message.get("headers", [])
595
+ resp_headers = {
596
+ name.decode("utf-8"): val.decode("utf-8")
597
+ for name, val in headers
598
+ }
599
+ if SF_DEBUG and app_config._interceptors_initialized:
600
+ print(
601
+ f"[[Quart]] Captured response headers: {len(resp_headers)} headers",
602
+ log=False,
603
+ )
604
+ except Exception as e:
605
+ if SF_DEBUG and app_config._interceptors_initialized:
606
+ print(
607
+ f"[[Quart]] Failed to capture response headers: {e}",
608
+ log=False,
609
+ )
610
+
611
+ # Capture response body
612
+ if (
613
+ message["type"] == "http.response.body"
614
+ and SF_NETWORKHOP_CAPTURE_RESPONSE_BODY
615
+ ):
616
+ try:
617
+ body_part = message.get("body", b"")
618
+ if body_part and resp_body_size < _RESPONSE_LIMIT_BYTES:
619
+ remaining = _RESPONSE_LIMIT_BYTES - resp_body_size
620
+ resp_body_parts.append(body_part[:remaining])
621
+ resp_body_size += len(body_part)
622
+ except Exception as e:
623
+ if SF_DEBUG and app_config._interceptors_initialized:
624
+ print(
625
+ f"[[Quart]] Failed to capture response body chunk: {e}",
626
+ log=False,
627
+ )
628
+
629
+ await send(message)
630
+
631
+ # After final response body sent, emit network hop
632
+ if SF_DEBUG and app_config._interceptors_initialized:
633
+ print(
634
+ f"[[Quart]] Message type: {message.get('type')}, more_body: {message.get('more_body', False)}, endpoint_id: {endpoint_id}",
635
+ log=False,
636
+ )
637
+
638
+ if message["type"] == "http.response.body" and not message.get(
639
+ "more_body", False
640
+ ):
641
+ if SF_DEBUG and app_config._interceptors_initialized:
642
+ print(
643
+ f"[[Quart]] Final response body message, checking endpoint_id={endpoint_id}",
644
+ log=False,
645
+ )
646
+
647
+ if endpoint_id is not None and endpoint_id >= 0:
648
+ try:
649
+ _, session_id = get_or_set_sf_trace_id()
650
+
651
+ # Finalize request body
652
+ final_req_body = (
653
+ b"".join(body_parts) if body_parts else None
654
+ )
655
+ if final_req_body and SF_DEBUG:
656
+ print(
657
+ f"[[Quart]] Request body capture: {len(final_req_body)} bytes",
658
+ log=False,
659
+ )
660
+
661
+ # Finalize response body
662
+ final_resp_body = (
663
+ b"".join(resp_body_parts) if resp_body_parts else None
664
+ )
665
+ if final_resp_body and SF_DEBUG:
666
+ print(
667
+ f"[[Quart]] Captured response body: {len(final_resp_body)} bytes",
668
+ log=False,
669
+ )
670
+
671
+ if SF_DEBUG and app_config._interceptors_initialized:
672
+ print(
673
+ f"[[Quart]] About to emit network hop: endpoint_id={endpoint_id}, "
674
+ f"req_headers={'present' if req_headers else 'None'}, "
675
+ f"req_body={len(final_req_body) if final_req_body else 0} bytes, "
676
+ f"resp_headers={'present' if resp_headers else 'None'}, "
677
+ f"resp_body={len(final_resp_body) if final_resp_body else 0} bytes",
678
+ log=False,
679
+ )
680
+
681
+ fast_send_network_hop_fast(
682
+ session_id=session_id,
683
+ endpoint_id=endpoint_id,
684
+ raw_path=scope.get("path"),
685
+ raw_query_string=scope.get("query_string", b""),
686
+ request_headers=req_headers,
687
+ request_body=final_req_body,
688
+ response_headers=resp_headers,
689
+ response_body=final_resp_body,
690
+ )
691
+
692
+ if SF_DEBUG and app_config._interceptors_initialized:
693
+ print(
694
+ f"[[Quart]] Emitted network hop: endpoint_id={endpoint_id} "
695
+ f"session={session_id}",
696
+ log=False,
697
+ )
698
+ except Exception as e: # noqa: BLE001 S110
699
+ if SF_DEBUG and app_config._interceptors_initialized:
700
+ print(
701
+ f"[[Quart]] Failed to emit network hop: {e}",
702
+ log=False,
703
+ )
704
+
705
+ try:
706
+ await self.app(scope, receive, wrapped_send)
707
+ except Exception as exc: # noqa: BLE001
708
+ custom_excepthook(type(exc), exc, exc.__traceback__)
709
+ raise
710
+ finally:
711
+ # CRITICAL: Clear C TLS to prevent stale data in thread pools
712
+ clear_c_tls_parent_trace_id()
713
+
714
+ # CRITICAL: Clear outbound header base to prevent stale cached headers
715
+ # ContextVar does NOT automatically clean up in thread pools - must clear explicitly
716
+ clear_outbound_header_base()
717
+
718
+ # CRITICAL: Clear trace_id to ensure fresh generation for next request
719
+ # Without this, get_or_set_sf_trace_id() reuses trace_id from previous request
720
+ # causing X-Sf4-Prid to stay constant when no incoming X-Sf3-Rid header
721
+ clear_trace_id()
722
+
723
+ # CRITICAL: Clear current request path to prevent stale data in thread pools
724
+ clear_current_request_path()
725
+
726
+ # ──────────────────────────────────────────────────────────
727
+ # Main patch entry-point (Flask-style hooks for Quart)
728
+ # ──────────────────────────────────────────────────────────
729
+ def patch_quart(routes_to_skip: Optional[List[str]] = None):
730
+ """
731
+ Quart patch using Flask-style before/after request hooks:
732
+ • Wraps view functions to capture endpoint_id
733
+ • Uses @before_request to capture request data
734
+ • Uses @after_request to emit AFTER response built (Flask-style)
735
+ • Captures request/response headers and body when enabled
736
+ • Direct C call with route/query params extracted from request object
737
+
738
+ Note: Quart is Flask-based and doesn't expose endpoints in ASGI scope,
739
+ so we use Flask-style hooks instead of ASGI middleware.
740
+ """
741
+ global _ROUTES_TO_SKIP
742
+ _ROUTES_TO_SKIP = routes_to_skip or []
743
+
744
+ # Guard against double-patching
745
+ if getattr(Quart, "__sf_tracing_patched__", False):
746
+ return
747
+
748
+ original_init = Quart.__init__
749
+
750
+ def patched_init(self, *args, **kwargs):
751
+ original_init(self, *args, **kwargs)
752
+
753
+ # Install Flask-style hooks for request/response capture
754
+ _install_request_hooks(self)
755
+
756
+ # Patch add_url_rule to wrap view functions
757
+ _patch_add_route(self.__class__)
758
+
759
+ if SF_DEBUG and app_config._interceptors_initialized:
760
+ print("[[patch_quart]] Flask-style hooks installed", log=False)
761
+
762
+ Quart.__init__ = patched_init
763
+ Quart.__sf_tracing_patched__ = True
764
+
765
+ if SF_DEBUG and app_config._interceptors_initialized:
766
+ print(
767
+ "[[patch_quart]] Flask-style patch applied (emits in @after_request)",
768
+ log=False,
769
+ )
770
+
771
+ def patch_quart_cors():
772
+ """
773
+ Patch quart-cors to automatically inject Sailfish headers.
774
+
775
+ SAFE: Only modifies CORS if quart-cors is installed and used.
776
+ """
777
+ try:
778
+ from quart_cors import cors
779
+ except ImportError:
780
+ # quart-cors not installed, skip patching
781
+ if SF_DEBUG and app_config._interceptors_initialized:
782
+ print(
783
+ "[[patch_quart_cors]] quart-cors not installed, skipping", log=False
784
+ )
785
+ return
786
+
787
+ # Check if already patched
788
+ if hasattr(cors, "_sf_cors_patched"):
789
+ if SF_DEBUG and app_config._interceptors_initialized:
790
+ print("[[patch_quart_cors]] Already patched, skipping", log=False)
791
+ return
792
+
793
+ # Patch the cors decorator/function
794
+ original_cors = cors
795
+
796
+ def patched_cors(app=None, **kwargs):
797
+ # Intercept allow_headers parameter
798
+ if "allow_headers" in kwargs:
799
+ original_headers = kwargs["allow_headers"]
800
+ if should_inject_headers(original_headers):
801
+ kwargs["allow_headers"] = inject_sailfish_headers(original_headers)
802
+ if SF_DEBUG and app_config._interceptors_initialized:
803
+ print(
804
+ "[[patch_quart_cors]] Injected Sailfish headers into quart-cors",
805
+ log=False,
806
+ )
807
+
808
+ # Call original cors
809
+ return original_cors(app, **kwargs)
810
+
811
+ # Replace the cors function in the module
812
+ import quart_cors as qc_module
813
+
814
+ qc_module.cors = patched_cors
815
+ cors._sf_cors_patched = True
816
+
817
+ if SF_DEBUG and app_config._interceptors_initialized:
818
+ print("[[patch_quart_cors]] Successfully patched quart-cors", log=False)
819
+
820
+ # Call CORS patching
821
+ patch_quart_cors()
822
+
823
+ # Expose the patch function
824
+ __all__ = ["patch_quart"]