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