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,874 @@
1
+ """
2
+ OTEL-STYLE PURE ASYNC PATTERN:
3
+ • Call C extension directly AFTER response sent
4
+ • C queues to lock-free ring buffer and returns in ~1µs
5
+ • ASGI event loop returns instantly (doesn't wait)
6
+ • C background thread does ALL work with GIL released
7
+ • This should MATCH OTEL performance (identical pattern)
8
+
9
+ KEY INSIGHT: No Python threads! C extension handles everything.
10
+ """
11
+
12
+ import gc
13
+ import inspect
14
+ import os
15
+ import threading
16
+ from collections import defaultdict
17
+ from threading import Lock
18
+ from typing import List, Optional
19
+
20
+ from ... import _sffuncspan, app_config
21
+ from ...constants import (
22
+ FUNCSPAN_OVERRIDE_HEADER_BYTES,
23
+ SAILFISH_TRACING_HEADER,
24
+ SAILFISH_TRACING_HEADER_BYTES,
25
+ )
26
+ from ...custom_excepthook import custom_excepthook
27
+ from ...env_vars import (
28
+ SF_DEBUG,
29
+ SF_NETWORKHOP_CAPTURE_ENABLED,
30
+ SF_NETWORKHOP_CAPTURE_REQUEST_BODY,
31
+ SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS,
32
+ SF_NETWORKHOP_CAPTURE_RESPONSE_BODY,
33
+ SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS,
34
+ SF_NETWORKHOP_REQUEST_LIMIT_MB,
35
+ SF_NETWORKHOP_RESPONSE_LIMIT_MB,
36
+ )
37
+ from ...fast_network_hop import fast_send_network_hop_fast, register_endpoint
38
+ from ...thread_local import (
39
+ clear_c_tls_parent_trace_id,
40
+ clear_current_request_path,
41
+ clear_funcspan_override,
42
+ clear_outbound_header_base,
43
+ clear_trace_id,
44
+ generate_new_trace_id,
45
+ get_or_set_sf_trace_id,
46
+ get_sf_trace_id,
47
+ set_current_request_path,
48
+ set_funcspan_override,
49
+ set_outbound_header_base,
50
+ )
51
+ from .cors_utils import inject_sailfish_headers, should_inject_headers
52
+ from .utils import _is_user_code, _unwrap_user_func, should_skip_route, reinitialize_log_print_capture_for_worker
53
+
54
+ _SKIP_TRACING_ATTR = "_sf_skip_tracing"
55
+
56
+ # Size limits in bytes
57
+ _REQUEST_LIMIT_BYTES = SF_NETWORKHOP_REQUEST_LIMIT_MB * 1024 * 1024
58
+ _RESPONSE_LIMIT_BYTES = SF_NETWORKHOP_RESPONSE_LIMIT_MB * 1024 * 1024
59
+
60
+ # OTEL-STYLE: No thread pool! Pure async, call C extension directly
61
+ # C extension has its own background worker thread
62
+
63
+ # Pre-registered endpoint IDs (maps endpoint function id -> endpoint_id from C extension)
64
+ _ENDPOINT_REGISTRY: dict[int, int] = {}
65
+
66
+ # Track which Sanic app instances have been registered (to support multiple apps)
67
+ _REGISTERED_APPS: set[int] = set()
68
+
69
+ # Request data storage (keyed by request id)
70
+ _request_data_lock = Lock()
71
+ _request_data = defaultdict(dict)
72
+
73
+
74
+ def _should_trace_endpoint(endpoint_fn) -> bool:
75
+ """Check if endpoint should be traced."""
76
+ if getattr(endpoint_fn, _SKIP_TRACING_ATTR, False):
77
+ return False
78
+
79
+ code = getattr(endpoint_fn, "__code__", None)
80
+ if not code:
81
+ return False
82
+
83
+ filename = code.co_filename
84
+ if not _is_user_code(filename):
85
+ return False
86
+
87
+ # Skip Strawberry GraphQL handlers
88
+ if getattr(endpoint_fn, "__module__", "").startswith("strawberry"):
89
+ return False
90
+
91
+ return True
92
+
93
+
94
+ def _pre_register_endpoints(app, routes_to_skip: Optional[List[str]] = None):
95
+ """Pre-register all endpoints at startup."""
96
+ routes_to_skip = routes_to_skip or []
97
+ count = 0
98
+ skipped = 0
99
+
100
+ app_id = id(app)
101
+
102
+ # Check if this app has already been registered
103
+ if app_id in _REGISTERED_APPS:
104
+ if SF_DEBUG and app_config._interceptors_initialized:
105
+ print(
106
+ f"[[_pre_register_endpoints]] App {app_id} already registered, skipping",
107
+ log=False,
108
+ )
109
+ return
110
+
111
+ if SF_DEBUG and app_config._interceptors_initialized:
112
+ print(
113
+ f"[[_pre_register_endpoints]] Starting registration for app {app_id}, app has {len(app.router.routes)} routes",
114
+ log=False,
115
+ )
116
+
117
+ # Iterate through all routes in the Sanic router
118
+ for route in app.router.routes:
119
+ if not hasattr(route, "handler"):
120
+ skipped += 1
121
+ if SF_DEBUG and app_config._interceptors_initialized:
122
+ print(
123
+ f"[[_pre_register_endpoints]] Skipping route (no handler): {route}",
124
+ log=False,
125
+ )
126
+ continue
127
+
128
+ original_endpoint = route.handler
129
+ endpoint_fn_id = id(original_endpoint)
130
+
131
+ # Check if this specific endpoint function is already registered
132
+ if endpoint_fn_id in _ENDPOINT_REGISTRY:
133
+ if SF_DEBUG and app_config._interceptors_initialized:
134
+ print(
135
+ f"[[_pre_register_endpoints]] Endpoint function {original_endpoint.__name__ if hasattr(original_endpoint, '__name__') else original_endpoint} already registered (id={_ENDPOINT_REGISTRY[endpoint_fn_id]}), skipping",
136
+ log=False,
137
+ )
138
+ continue
139
+
140
+ # Check for @skip_network_tracing on the wrapped function BEFORE unwrapping
141
+ if getattr(original_endpoint, _SKIP_TRACING_ATTR, False):
142
+ skipped += 1
143
+ if SF_DEBUG and app_config._interceptors_initialized:
144
+ print(
145
+ f"[[_pre_register_endpoints]] Skipping endpoint (marked with @skip_network_tracing): {original_endpoint.__name__ if hasattr(original_endpoint, '__name__') else original_endpoint}",
146
+ log=False,
147
+ )
148
+ continue
149
+
150
+ unwrapped = _unwrap_user_func(original_endpoint)
151
+
152
+ if not _should_trace_endpoint(unwrapped):
153
+ skipped += 1
154
+ if SF_DEBUG and app_config._interceptors_initialized:
155
+ print(
156
+ f"[[_pre_register_endpoints]] Skipping endpoint (not user code): {unwrapped.__name__ if hasattr(unwrapped, '__name__') else unwrapped}",
157
+ log=False,
158
+ )
159
+ continue
160
+
161
+ code = unwrapped.__code__
162
+ line_no_str = str(code.co_firstlineno)
163
+ name = unwrapped.__name__
164
+ filename = code.co_filename
165
+
166
+ # Extract route pattern from Sanic route (e.g., "/log/<n>")
167
+ route_pattern = getattr(route, "path", None)
168
+
169
+ # Check if route should be skipped based on wildcard patterns
170
+ if should_skip_route(route_pattern, routes_to_skip):
171
+ skipped += 1
172
+ if SF_DEBUG and app_config._interceptors_initialized:
173
+ print(
174
+ f"[[_pre_register_endpoints]] Skipping endpoint (route matches skip pattern): {route_pattern}",
175
+ log=False,
176
+ )
177
+ continue
178
+
179
+ endpoint_id = register_endpoint(
180
+ line=line_no_str,
181
+ column="0",
182
+ name=name,
183
+ entrypoint=filename,
184
+ route=route_pattern,
185
+ )
186
+
187
+ if endpoint_id < 0:
188
+ if SF_DEBUG and app_config._interceptors_initialized:
189
+ print(
190
+ f"[[_pre_register_endpoints]] Failed to register {name} (endpoint_id={endpoint_id})",
191
+ log=False,
192
+ )
193
+ continue
194
+
195
+ _ENDPOINT_REGISTRY[endpoint_fn_id] = endpoint_id
196
+ count += 1
197
+
198
+ if SF_DEBUG and app_config._interceptors_initialized:
199
+ print(
200
+ f"[[patch_sanic]] Registered: {name} @ {filename}:{line_no_str} route={route_pattern} (endpoint_fn_id={endpoint_fn_id}, endpoint_id={endpoint_id})",
201
+ log=False,
202
+ )
203
+
204
+ if SF_DEBUG and app_config._interceptors_initialized:
205
+ print(
206
+ f"[[patch_sanic]] Total endpoints registered: {count}, skipped: {skipped}",
207
+ log=False,
208
+ )
209
+
210
+ # Only mark this app as registered if we actually registered user endpoints
211
+ # This allows retry on startup event if routes weren't ready yet
212
+ if count > 0:
213
+ _REGISTERED_APPS.add(app_id)
214
+ if SF_DEBUG and app_config._interceptors_initialized:
215
+ print(f"[[patch_sanic]] App {app_id} marked as registered", log=False)
216
+ print(
217
+ f"[[patch_sanic]] OTEL-STYLE: Pure async, direct C call (no Python threads)",
218
+ log=False,
219
+ )
220
+ print(
221
+ f"[[patch_sanic]] Request headers capture: {'ENABLED' if SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS else 'DISABLED'}",
222
+ log=False,
223
+ )
224
+ print(
225
+ f"[[patch_sanic]] Request body capture: {'ENABLED' if SF_NETWORKHOP_CAPTURE_REQUEST_BODY else 'DISABLED'}",
226
+ log=False,
227
+ )
228
+ print(
229
+ f"[[patch_sanic]] Response headers capture: {'ENABLED' if SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS else 'DISABLED'}",
230
+ log=False,
231
+ )
232
+ print(
233
+ f"[[patch_sanic]] Response body capture: {'ENABLED' if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY else 'DISABLED'}",
234
+ log=False,
235
+ )
236
+ print(
237
+ f"[[patch_sanic]] C extension queues to background worker with GIL released",
238
+ log=False,
239
+ )
240
+ else:
241
+ if SF_DEBUG and app_config._interceptors_initialized:
242
+ print(
243
+ f"[[patch_sanic]] No user endpoints registered yet for app {app_id}, will retry on startup",
244
+ log=False,
245
+ )
246
+
247
+
248
+ def patch_sanic(routes_to_skip: Optional[List[str]] = None):
249
+ """
250
+ OTEL-STYLE PURE ASYNC:
251
+ • Direct C call IMMEDIATELY after response completes
252
+ • Sanic returns instantly (C queues and returns in ~1µs)
253
+ • C background thread does all network work with GIL released
254
+ • This is TRUE zero overhead!
255
+ """
256
+ routes_to_skip = routes_to_skip or []
257
+
258
+ if SF_DEBUG and app_config._interceptors_initialized:
259
+ print("[[patch_sanic]] patching sanic...", log=False)
260
+
261
+ try:
262
+ from sanic import Sanic
263
+ except ImportError:
264
+ return
265
+
266
+ if SF_DEBUG and app_config._interceptors_initialized:
267
+ print("[[patch_sanic]] Patching Sanic.__init__", log=False)
268
+
269
+ orig_init = Sanic.__init__
270
+
271
+ def patched_init(self, *args, **kwargs):
272
+ """
273
+ After the original Sanic app is created we attach:
274
+ • request middleware – capture header propagation + request headers/body
275
+ • response middleware – emit NetworkHop with response headers/body
276
+ • universal exception handler – capture all exceptions
277
+ """
278
+ # Let Sanic build the app normally
279
+ orig_init(self, *args, **kwargs)
280
+
281
+ if SF_DEBUG and app_config._interceptors_initialized:
282
+ print(
283
+ f"[[patch_sanic]] patched_init called - about to register middleware",
284
+ log=False,
285
+ )
286
+
287
+ # 1. Request middleware: header propagation + request capture
288
+ async def _capture_request(request):
289
+ # Set current request path for route-based suppression (SF_DISABLE_INBOUND_NETWORK_TRACING_ON_ROUTES)
290
+ set_current_request_path(request.path)
291
+
292
+ if SF_DEBUG and app_config._interceptors_initialized:
293
+ print(
294
+ f"[[Sanic]] _capture_request called for {request.method} {request.path}",
295
+ log=False,
296
+ )
297
+ try:
298
+ # PERFORMANCE: Single-pass bytes-level header scan (no dict allocation until needed)
299
+ # Scan headers once, only decode what we need, use latin-1 (fast 1:1 byte map)
300
+ incoming_trace_raw = None # bytes
301
+ funcspan_raw = None # bytes
302
+ req_headers = None # dict[str,str] only if capture enabled
303
+
304
+ capture_req_headers = (
305
+ SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS # local cache
306
+ )
307
+
308
+ # Sanic headers are already decoded strings, so we work with them directly
309
+ # Convert to lowercase for comparison
310
+ if capture_req_headers:
311
+ req_headers = dict(request.headers)
312
+ # Extract special headers
313
+ for k, v in request.headers.items():
314
+ k_lower = k.lower()
315
+ if k_lower == SAILFISH_TRACING_HEADER.lower():
316
+ incoming_trace_raw = v
317
+ elif k_lower == "x-sf3-functionspancaptureoverride":
318
+ funcspan_raw = v
319
+ else:
320
+ # Just extract special headers
321
+ for k, v in request.headers.items():
322
+ k_lower = k.lower()
323
+ if k_lower == SAILFISH_TRACING_HEADER.lower():
324
+ incoming_trace_raw = v
325
+ elif k_lower == "x-sf3-functionspancaptureoverride":
326
+ funcspan_raw = v
327
+
328
+ # CRITICAL: Seed/ensure trace_id immediately (BEFORE any outbound work)
329
+ if incoming_trace_raw:
330
+ # Incoming X-Sf3-Rid header provided - use it
331
+ get_or_set_sf_trace_id(
332
+ incoming_trace_raw, is_associated_with_inbound_request=True
333
+ )
334
+ else:
335
+ # No incoming X-Sf3-Rid header - generate fresh trace_id for this request
336
+ generate_new_trace_id()
337
+
338
+ # Optional funcspan override
339
+ funcspan_override_header = funcspan_raw
340
+ if funcspan_override_header:
341
+ try:
342
+ set_funcspan_override(funcspan_override_header)
343
+ if SF_DEBUG and app_config._interceptors_initialized:
344
+ print(
345
+ f"[[Sanic]] Set function span override from header: {funcspan_override_header}",
346
+ log=False,
347
+ )
348
+ except Exception as e:
349
+ if SF_DEBUG and app_config._interceptors_initialized:
350
+ print(
351
+ f"[[Sanic]] Failed to set function span override: {e}",
352
+ log=False,
353
+ )
354
+
355
+ # Initialize outbound base without list/allocs from split()
356
+ try:
357
+ trace_id = get_sf_trace_id()
358
+ if trace_id:
359
+ s = str(trace_id)
360
+ i = s.find("/") # session
361
+ j = s.find("/", i + 1) if i != -1 else -1 # page
362
+ if j != -1:
363
+ base_trace = s[:j] # "session/page"
364
+ set_outbound_header_base(
365
+ base_trace=base_trace,
366
+ parent_trace_id=s, # "session/page/uuid"
367
+ funcspan=funcspan_override_header,
368
+ )
369
+ if SF_DEBUG and app_config._interceptors_initialized:
370
+ print(
371
+ f"[[Sanic]] Initialized outbound header base (base={base_trace[:16]}...)",
372
+ log=False,
373
+ )
374
+ except Exception as e:
375
+ if SF_DEBUG and app_config._interceptors_initialized:
376
+ print(
377
+ f"[[Sanic]] Failed to initialize outbound header base: {e}",
378
+ log=False,
379
+ )
380
+
381
+ # OPTIMIZATION: Skip ALL capture infrastructure if not capturing network hops
382
+ # We still needed to set up trace_id and outbound header base above (for outbound call tracing)
383
+ # but we can skip all request/response capture overhead
384
+ if not SF_NETWORKHOP_CAPTURE_ENABLED:
385
+ return
386
+
387
+ # Use request id as key for storing data
388
+ req_id = id(request)
389
+
390
+ # Store captured request headers (already captured in single-pass scan above if enabled)
391
+ if req_headers:
392
+ with _request_data_lock:
393
+ _request_data[req_id]["headers"] = req_headers
394
+ if SF_DEBUG and app_config._interceptors_initialized:
395
+ print(
396
+ f"[[Sanic]] Captured request headers: {len(req_headers)} headers",
397
+ log=False,
398
+ )
399
+
400
+ # Capture request body if enabled
401
+ if SF_NETWORKHOP_CAPTURE_REQUEST_BODY:
402
+ try:
403
+ # Sanic request.body is available as bytes
404
+ body = request.body
405
+ if body:
406
+ req_body = body[:_REQUEST_LIMIT_BYTES]
407
+ with _request_data_lock:
408
+ _request_data[req_id]["body"] = req_body
409
+ if SF_DEBUG and app_config._interceptors_initialized:
410
+ print(
411
+ f"[[Sanic]] Request body capture: {len(req_body)} bytes (method={request.method})",
412
+ log=False,
413
+ )
414
+ except Exception as e:
415
+ if SF_DEBUG and app_config._interceptors_initialized:
416
+ print(
417
+ f"[[Sanic]] Failed to capture request body: {e}",
418
+ log=False,
419
+ )
420
+ except Exception as e:
421
+ if SF_DEBUG and app_config._interceptors_initialized:
422
+ print(f"[[Sanic]] Request middleware error: {e}", log=False)
423
+
424
+ try:
425
+ self.register_middleware(_capture_request, attach_to="request")
426
+ if SF_DEBUG and app_config._interceptors_initialized:
427
+ print(
428
+ f"[[patch_sanic]] Successfully registered request middleware",
429
+ log=False,
430
+ )
431
+ except TypeError: # Sanic<22 compatibility
432
+ self.register_middleware(_capture_request, "request")
433
+ if SF_DEBUG and app_config._interceptors_initialized:
434
+ print(
435
+ f"[[patch_sanic]] Successfully registered request middleware (legacy)",
436
+ log=False,
437
+ )
438
+
439
+ # 2. Response middleware: OTEL-STYLE NetworkHop emission with response capture
440
+ async def _emit_hop(request, response):
441
+ """
442
+ OTEL-STYLE: Emit network hop in response middleware.
443
+ In Sanic, response middleware runs after handler but before send.
444
+ """
445
+ if SF_DEBUG and app_config._interceptors_initialized:
446
+ print(
447
+ f"[[Sanic]] _emit_hop called for {request.method} {request.path}",
448
+ log=False,
449
+ )
450
+ req_id = id(request)
451
+ try:
452
+ handler = getattr(request, "route", None)
453
+ if not handler:
454
+ return
455
+ fn = getattr(handler, "handler", None)
456
+ if not fn:
457
+ return
458
+
459
+ # Use the pre-registered endpoint ID
460
+ endpoint_id = _ENDPOINT_REGISTRY.get(id(fn))
461
+ if endpoint_id is None or endpoint_id < 0:
462
+ if SF_DEBUG and app_config._interceptors_initialized:
463
+ print(
464
+ f"[[Sanic]] Skipping NetworkHop (endpoint not registered or endpoint_id={endpoint_id})",
465
+ log=False,
466
+ )
467
+ return
468
+
469
+ if endpoint_id is not None and endpoint_id >= 0:
470
+ try:
471
+ # OPTIMIZATION: Use get_sf_trace_id() directly instead of get_or_set_sf_trace_id()
472
+ # Trace ID is GUARANTEED to be set at request start
473
+ # This saves ~11-12μs by avoiding tuple unpacking and conditional logic
474
+ session_id = get_sf_trace_id()
475
+
476
+ # Get captured request data
477
+ req_headers = None
478
+ req_body = None
479
+ with _request_data_lock:
480
+ req_data = _request_data.get(req_id, {})
481
+ req_headers = req_data.get("headers")
482
+ req_body = req_data.get("body")
483
+
484
+ # Capture response headers if enabled
485
+ resp_headers = None
486
+ if SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS and response:
487
+ try:
488
+ # Sanic response.headers is a dict-like object
489
+ resp_headers = dict(response.headers)
490
+ if SF_DEBUG and app_config._interceptors_initialized:
491
+ print(
492
+ f"[[Sanic]] Captured response headers: {len(resp_headers)} headers",
493
+ log=False,
494
+ )
495
+ except Exception as e:
496
+ if SF_DEBUG and app_config._interceptors_initialized:
497
+ print(
498
+ f"[[Sanic]] Failed to capture response headers: {e}",
499
+ log=False,
500
+ )
501
+
502
+ # Capture response body if enabled
503
+ resp_body = None
504
+ if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY and response:
505
+ try:
506
+ # Sanic response.body is bytes
507
+ if hasattr(response, "body") and response.body:
508
+ if isinstance(response.body, bytes):
509
+ resp_body = response.body[
510
+ :_RESPONSE_LIMIT_BYTES
511
+ ]
512
+ if (
513
+ SF_DEBUG
514
+ and app_config._interceptors_initialized
515
+ ):
516
+ print(
517
+ f"[[Sanic]] Captured response body: {len(resp_body)} bytes",
518
+ log=False,
519
+ )
520
+ elif isinstance(response.body, str):
521
+ resp_body = response.body.encode("utf-8")[
522
+ :_RESPONSE_LIMIT_BYTES
523
+ ]
524
+ if (
525
+ SF_DEBUG
526
+ and app_config._interceptors_initialized
527
+ ):
528
+ print(
529
+ f"[[Sanic]] Captured response body (str): {len(resp_body)} bytes",
530
+ log=False,
531
+ )
532
+ except Exception as e:
533
+ if SF_DEBUG and app_config._interceptors_initialized:
534
+ print(
535
+ f"[[Sanic]] Failed to capture response body: {e}",
536
+ log=False,
537
+ )
538
+
539
+ if SF_DEBUG and app_config._interceptors_initialized:
540
+ print(
541
+ f"[[Sanic]] About to emit network hop: endpoint_id={endpoint_id}, "
542
+ f"req_headers={'present' if req_headers else 'None'}, "
543
+ f"req_body={len(req_body) if req_body else 0} bytes, "
544
+ f"resp_headers={'present' if resp_headers else 'None'}, "
545
+ f"resp_body={len(resp_body) if resp_body else 0} bytes",
546
+ log=False,
547
+ )
548
+
549
+ # Direct C call - queues to background worker, returns instantly
550
+ # Extract route and query params from request
551
+ raw_path = request.path
552
+ raw_query = (
553
+ request.query_string.encode("utf-8")
554
+ if request.query_string
555
+ else b""
556
+ )
557
+
558
+ fast_send_network_hop_fast(
559
+ session_id=session_id,
560
+ endpoint_id=endpoint_id,
561
+ raw_path=raw_path,
562
+ raw_query_string=raw_query,
563
+ request_headers=req_headers,
564
+ request_body=req_body,
565
+ response_headers=resp_headers,
566
+ response_body=resp_body,
567
+ )
568
+
569
+ if SF_DEBUG and app_config._interceptors_initialized:
570
+ print(
571
+ f"[[Sanic]] Emitted network hop: endpoint_id={endpoint_id} "
572
+ f"session={session_id}",
573
+ log=False,
574
+ )
575
+ except Exception as e: # noqa: BLE001 S110
576
+ if SF_DEBUG and app_config._interceptors_initialized:
577
+ print(
578
+ f"[[Sanic]] Failed to emit network hop: {e}", log=False
579
+ )
580
+ finally:
581
+ # Clean up request data to prevent memory leak
582
+ with _request_data_lock:
583
+ _request_data.pop(req_id, None)
584
+
585
+ # CRITICAL: Clear C TLS to prevent stale data in thread pools
586
+ clear_c_tls_parent_trace_id()
587
+
588
+ # CRITICAL: Clear outbound header base to prevent stale cached headers
589
+ # ContextVar does NOT automatically clean up in thread pools - must clear explicitly
590
+ clear_outbound_header_base()
591
+
592
+ # CRITICAL: Clear trace_id to ensure fresh generation for next request
593
+ # Without this, get_or_set_sf_trace_id() reuses trace_id from previous request
594
+ # causing X-Sf4-Prid to stay constant when no incoming X-Sf3-Rid header
595
+ clear_trace_id()
596
+
597
+ # CRITICAL: Clear current request path to prevent stale data in thread pools
598
+ clear_current_request_path()
599
+
600
+ # Clear function span override for this request (ContextVar cleanup - also syncs C thread-local)
601
+ try:
602
+ clear_funcspan_override()
603
+ except Exception:
604
+ pass
605
+ except Exception as e:
606
+ if SF_DEBUG and app_config._interceptors_initialized:
607
+ print(f"[[Sanic]] Response middleware error: {e}", log=False)
608
+ # Clean up on error too
609
+ with _request_data_lock:
610
+ _request_data.pop(req_id, None)
611
+
612
+ # CRITICAL: Clear C TLS to prevent stale data in thread pools
613
+ clear_c_tls_parent_trace_id()
614
+
615
+ # CRITICAL: Clear outbound header base to prevent stale cached headers
616
+ clear_outbound_header_base()
617
+
618
+ # CRITICAL: Clear trace_id to ensure fresh generation for next request
619
+ clear_trace_id()
620
+
621
+ # CRITICAL: Clear current request path to prevent stale data in thread pools
622
+ clear_current_request_path()
623
+
624
+ # Clear function span override for this request (ContextVar cleanup - also syncs C thread-local)
625
+ try:
626
+ clear_funcspan_override()
627
+ except Exception:
628
+ pass
629
+
630
+ try:
631
+ self.register_middleware(_emit_hop, attach_to="response")
632
+ if SF_DEBUG and app_config._interceptors_initialized:
633
+ print(
634
+ f"[[patch_sanic]] Successfully registered response middleware",
635
+ log=False,
636
+ )
637
+ except TypeError:
638
+ self.register_middleware(_emit_hop, "response")
639
+ if SF_DEBUG and app_config._interceptors_initialized:
640
+ print(
641
+ f"[[patch_sanic]] Successfully registered response middleware (legacy)",
642
+ log=False,
643
+ )
644
+
645
+ # 3. Universal exception handler
646
+ async def _capture_exception(request, exception):
647
+ """
648
+ Called for any exception – user errors, abort/HTTPException,
649
+ or Sanic-specific errors. Forward to custom_excepthook
650
+ and then fall back to Sanic's default error handler.
651
+ """
652
+ try:
653
+ custom_excepthook(type(exception), exception, exception.__traceback__)
654
+ except Exception:
655
+ pass # Don't let exception handler crash
656
+
657
+ # Delegate to default handler to keep standard 4xx/5xx payload
658
+ try:
659
+ response = request.app.error_handler.default(request, exception)
660
+ if inspect.isawaitable(response):
661
+ response = await response
662
+ return response
663
+ except Exception:
664
+ # If default handler fails, just re-raise
665
+ raise exception
666
+
667
+ # Register for base Exception class to catch everything
668
+ try:
669
+ self.error_handler.add(Exception, _capture_exception)
670
+ except Exception as e:
671
+ if SF_DEBUG and app_config._interceptors_initialized:
672
+ print(f"[[Sanic]] Failed to register exception handler: {e}", log=False)
673
+
674
+ # Try to register endpoints immediately if routes are already defined
675
+ # This handles apps where routes are defined before __init__ completes
676
+ if (
677
+ hasattr(self, "router")
678
+ and hasattr(self.router, "routes")
679
+ and self.router.routes
680
+ ):
681
+ try:
682
+ if SF_DEBUG and app_config._interceptors_initialized:
683
+ print(
684
+ f"[[patch_sanic]] Routes already defined ({len(self.router.routes)} routes), registering immediately",
685
+ log=False,
686
+ )
687
+ _pre_register_endpoints(self, routes_to_skip)
688
+ except Exception as e:
689
+ if SF_DEBUG and app_config._interceptors_initialized:
690
+ print(
691
+ f"[[patch_sanic]] Immediate registration failed: {e}",
692
+ log=False,
693
+ )
694
+
695
+ # Register on after_server_start event as a fallback (for routes added after __init__)
696
+ @self.listener("after_server_start")
697
+ async def _sf_startup(app, loop):
698
+ # Note: Profiler is already installed by unified_interceptor.py
699
+
700
+ if SF_DEBUG and app_config._interceptors_initialized:
701
+ print(
702
+ "[[patch_sanic]] After server start event fired, registering endpoints",
703
+ log=False,
704
+ )
705
+ # _pre_register_endpoints now checks if this app was already registered
706
+ _pre_register_endpoints(self, routes_to_skip)
707
+ if SF_DEBUG and app_config._interceptors_initialized:
708
+ print(
709
+ "[[patch_sanic]] ZERO-OVERHEAD pattern activated (truly async, no blocking)",
710
+ log=False,
711
+ )
712
+
713
+ if SF_DEBUG and app_config._interceptors_initialized:
714
+ print(
715
+ "[[patch_sanic]] OTEL-style middlewares + exception handler installed",
716
+ log=False,
717
+ )
718
+
719
+ Sanic.__init__ = patched_init
720
+
721
+ # Also patch any existing Sanic instances that were created before patching
722
+ # This handles the case where app = Sanic() happens before setup_interceptors()
723
+ for obj in gc.get_objects():
724
+ try:
725
+ # Wrap in try-except to safely handle lazy objects (e.g., Django settings)
726
+ # that trigger initialization on attribute access
727
+ if isinstance(obj, Sanic):
728
+ # Check if this app already has our middleware
729
+ # In Sanic, middlewares are stored in a list
730
+ has_our_middleware = False
731
+ if hasattr(obj, "middlewares"):
732
+ # Check if any middleware matches our function names
733
+ for mw in obj.middlewares:
734
+ if hasattr(mw, "__name__") and mw.__name__ in (
735
+ "_capture_request",
736
+ "_emit_hop",
737
+ ):
738
+ has_our_middleware = True
739
+ break
740
+
741
+ if not has_our_middleware:
742
+ if SF_DEBUG and app_config._interceptors_initialized:
743
+ print(
744
+ f"[[patch_sanic]] Retroactively patching existing Sanic app",
745
+ log=False,
746
+ )
747
+
748
+ # We can't easily retroactively add middleware to an already-created Sanic app
749
+ # Just try to register endpoints if they exist
750
+ if (
751
+ hasattr(obj, "router")
752
+ and hasattr(obj.router, "routes")
753
+ and obj.router.routes
754
+ ):
755
+ try:
756
+ if SF_DEBUG and app_config._interceptors_initialized:
757
+ print(
758
+ f"[[patch_sanic]] Retroactive immediate registration ({len(obj.router.routes)} routes)",
759
+ log=False,
760
+ )
761
+ _pre_register_endpoints(obj, routes_to_skip)
762
+ except Exception as e:
763
+ if SF_DEBUG and app_config._interceptors_initialized:
764
+ print(
765
+ f"[[patch_sanic]] Retroactive immediate registration failed: {e}",
766
+ log=False,
767
+ )
768
+ except Exception:
769
+ # Silently skip objects that fail isinstance checks (e.g., Django lazy settings)
770
+ pass
771
+
772
+ if SF_DEBUG and app_config._interceptors_initialized:
773
+ print("[[patch_sanic]] OTEL-style patch applied", log=False)
774
+
775
+
776
+ def patch_sanic_cors():
777
+ """
778
+ Patch Sanic CORS middleware to automatically inject Sailfish headers.
779
+
780
+ SAFE: Only modifies CORS if CORSMiddleware is used by the application.
781
+ This works for both sanic-cors and sanic-ext CORS implementations.
782
+ """
783
+ # Try to patch sanic-ext CORS first
784
+ try:
785
+ from sanic_ext.extensions.http.cors import cors
786
+
787
+ # Check if already patched
788
+ if hasattr(cors, "_sf_cors_patched"):
789
+ if SF_DEBUG and app_config._interceptors_initialized:
790
+ print(
791
+ "[[patch_sanic_cors]] sanic-ext already patched, skipping",
792
+ log=False,
793
+ )
794
+ return
795
+
796
+ # Patch the cors configuration function
797
+ original_cors_init = cors.__init__ if hasattr(cors, "__init__") else None
798
+
799
+ if original_cors_init:
800
+
801
+ def patched_cors_init(self, *args, allow_headers=None, **kwargs):
802
+ # Intercept allow_headers parameter
803
+ if should_inject_headers(allow_headers):
804
+ allow_headers = inject_sailfish_headers(allow_headers)
805
+ if SF_DEBUG and app_config._interceptors_initialized:
806
+ print(
807
+ "[[patch_sanic_cors]] Injected Sailfish headers into sanic-ext CORS",
808
+ log=False,
809
+ )
810
+
811
+ # Call original init with potentially modified headers
812
+ original_cors_init(self, *args, allow_headers=allow_headers, **kwargs)
813
+
814
+ cors.__init__ = patched_cors_init
815
+ cors._sf_cors_patched = True
816
+
817
+ if SF_DEBUG and app_config._interceptors_initialized:
818
+ print(
819
+ "[[patch_sanic_cors]] Successfully patched sanic-ext CORS",
820
+ log=False,
821
+ )
822
+ except ImportError:
823
+ if SF_DEBUG and app_config._interceptors_initialized:
824
+ print(
825
+ "[[patch_sanic_cors]] sanic-ext not found, trying sanic-cors",
826
+ log=False,
827
+ )
828
+
829
+ # Try to patch sanic-cors
830
+ try:
831
+ from sanic_cors import CORS
832
+
833
+ # Check if already patched
834
+ if hasattr(CORS, "_sf_cors_patched"):
835
+ if SF_DEBUG and app_config._interceptors_initialized:
836
+ print(
837
+ "[[patch_sanic_cors]] sanic-cors already patched, skipping",
838
+ log=False,
839
+ )
840
+ return
841
+
842
+ original_cors_init = CORS.__init__
843
+
844
+ def patched_cors_init(self, app=None, *args, allow_headers=None, **kwargs):
845
+ # Intercept allow_headers parameter
846
+ if should_inject_headers(allow_headers):
847
+ allow_headers = inject_sailfish_headers(allow_headers)
848
+ if SF_DEBUG and app_config._interceptors_initialized:
849
+ print(
850
+ "[[patch_sanic_cors]] Injected Sailfish headers into sanic-cors",
851
+ log=False,
852
+ )
853
+
854
+ # Call original init with potentially modified headers
855
+ original_cors_init(self, app, *args, allow_headers=allow_headers, **kwargs)
856
+
857
+ CORS.__init__ = patched_cors_init
858
+ CORS._sf_cors_patched = True
859
+
860
+ if SF_DEBUG and app_config._interceptors_initialized:
861
+ print(
862
+ "[[patch_sanic_cors]] Successfully patched sanic-cors",
863
+ log=False,
864
+ )
865
+ except ImportError:
866
+ if SF_DEBUG and app_config._interceptors_initialized:
867
+ print(
868
+ "[[patch_sanic_cors]] sanic-cors not found, skipping CORS patching",
869
+ log=False,
870
+ )
871
+
872
+
873
+ # Call CORS patching
874
+ patch_sanic_cors()