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