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,616 @@
1
+ """
2
+ Litestar ASGI framework patch for OTEL-style network hop capture.
3
+ Captures request/response headers and bodies when enabled via env vars.
4
+ Uses sys.setprofile tracer to reliably capture endpoint metadata.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import threading
10
+ from functools import lru_cache
11
+ from typing import Any, Callable, List, Optional, Set
12
+
13
+ from ... import _sffuncspan, app_config
14
+ from ...constants import (
15
+ FUNCSPAN_OVERRIDE_HEADER_BYTES,
16
+ SAILFISH_TRACING_HEADER,
17
+ SAILFISH_TRACING_HEADER_BYTES,
18
+ )
19
+ from ...custom_excepthook import custom_excepthook
20
+ from ...custom_log_handler import CustomLogHandler
21
+ from ...env_vars import (
22
+ SF_DEBUG,
23
+ SF_NETWORKHOP_CAPTURE_ENABLED,
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 _is_user_code, should_skip_route, reinitialize_log_print_capture_for_worker
47
+
48
+ # Size limits in bytes
49
+ _REQUEST_LIMIT_BYTES = SF_NETWORKHOP_REQUEST_LIMIT_MB * 1024 * 1024
50
+ _RESPONSE_LIMIT_BYTES = SF_NETWORKHOP_RESPONSE_LIMIT_MB * 1024 * 1024
51
+
52
+ # Pre-registered endpoint IDs
53
+ _ENDPOINT_REGISTRY: dict[tuple, int] = {}
54
+
55
+ # Routes to skip (set by patch_litestar)
56
+ _ROUTES_TO_SKIP = []
57
+
58
+
59
+ def _sf_tracing_factory(app: Callable) -> Callable:
60
+ """
61
+ OTEL-STYLE ASGI middleware that:
62
+ • propagates the inbound SAILFISH_TRACING_HEADER header
63
+ • captures request headers/body if enabled
64
+ • uses sys.setprofile tracer to capture first user code frame
65
+ • emits NetworkHop AFTER response sent (zero overhead)
66
+ • captures response headers/body if enabled
67
+ • reports any unhandled exception via `custom_excepthook`
68
+ """
69
+
70
+ async def _middleware(scope, receive, send):
71
+ if scope.get("type") != "http":
72
+ await app(scope, receive, send)
73
+ return
74
+
75
+ # Set current request path for route-based suppression (SF_DISABLE_INBOUND_NETWORK_TRACING_ON_ROUTES)
76
+ request_path = scope.get("path", "")
77
+ set_current_request_path(request_path)
78
+
79
+ # Always print to verify middleware is being called
80
+ if SF_DEBUG and app_config._interceptors_initialized:
81
+ print(
82
+ f"[[Litestar._middleware]] HTTP request to {request_path}, type={scope.get('type')}",
83
+ log=False,
84
+ )
85
+
86
+ # PERFORMANCE: Single-pass bytes-level header scan (no dict allocation until needed)
87
+ # Scan headers once on bytes, only decode what we need, use latin-1 (fast 1:1 byte map)
88
+ hdr_tuples = scope.get("headers") or ()
89
+ incoming_trace_raw = None # bytes
90
+ funcspan_raw = None # bytes
91
+ req_headers = None # dict[str,str] only if capture enabled
92
+
93
+ capture_req_headers = SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS # local cache
94
+
95
+ if capture_req_headers:
96
+ # decode once using latin-1 (1:1 bytes, faster than utf-8 and never throws)
97
+ tmp = {}
98
+ for k, v in hdr_tuples:
99
+ kl = k.lower()
100
+ if kl == SAILFISH_TRACING_HEADER_BYTES:
101
+ incoming_trace_raw = v
102
+ elif kl == FUNCSPAN_OVERRIDE_HEADER_BYTES:
103
+ funcspan_raw = v
104
+ # build the dict while we're here
105
+ tmp[k.decode("latin-1")] = v.decode("latin-1")
106
+ req_headers = tmp
107
+ else:
108
+ for k, v in hdr_tuples:
109
+ kl = k.lower()
110
+ if kl == SAILFISH_TRACING_HEADER_BYTES:
111
+ incoming_trace_raw = v
112
+ elif kl == FUNCSPAN_OVERRIDE_HEADER_BYTES:
113
+ funcspan_raw = v
114
+ # no dict build
115
+
116
+ # CRITICAL: Seed/ensure trace_id immediately (BEFORE any outbound work)
117
+ if incoming_trace_raw:
118
+ # Incoming X-Sf3-Rid header provided - use it
119
+ incoming_trace = incoming_trace_raw.decode("latin-1")
120
+ get_or_set_sf_trace_id(
121
+ incoming_trace, is_associated_with_inbound_request=True
122
+ )
123
+ else:
124
+ # No incoming X-Sf3-Rid header - generate fresh trace_id for this request
125
+ generate_new_trace_id()
126
+
127
+ # Optional funcspan override (decode only if present)
128
+ funcspan_override_header = (
129
+ funcspan_raw.decode("latin-1") if funcspan_raw else None
130
+ )
131
+ if funcspan_override_header:
132
+ try:
133
+ set_funcspan_override(funcspan_override_header)
134
+ if SF_DEBUG and app_config._interceptors_initialized:
135
+ print(
136
+ f"[[Litestar._middleware]] Set function span override from header: {funcspan_override_header}",
137
+ log=False,
138
+ )
139
+ except Exception as e:
140
+ if SF_DEBUG and app_config._interceptors_initialized:
141
+ print(
142
+ f"[[Litestar._middleware]] Failed to set function span override: {e}",
143
+ log=False,
144
+ )
145
+
146
+ # Initialize outbound base without list/allocs from split()
147
+ try:
148
+ trace_id = get_sf_trace_id()
149
+ if trace_id:
150
+ s = str(trace_id)
151
+ i = s.find("/") # session
152
+ j = s.find("/", i + 1) if i != -1 else -1 # page
153
+ if j != -1:
154
+ base_trace = s[:j] # "session/page"
155
+ set_outbound_header_base(
156
+ base_trace=base_trace,
157
+ parent_trace_id=s, # "session/page/uuid"
158
+ funcspan=funcspan_override_header,
159
+ )
160
+ if SF_DEBUG and app_config._interceptors_initialized:
161
+ print(
162
+ f"[[Litestar._middleware]] Initialized outbound header base (base={base_trace[:16]}...)",
163
+ log=False,
164
+ )
165
+ except Exception as e:
166
+ if SF_DEBUG and app_config._interceptors_initialized:
167
+ print(
168
+ f"[[Litestar._middleware]] Failed to initialize outbound header base: {e}",
169
+ log=False,
170
+ )
171
+
172
+ # OPTIMIZATION: Skip ALL capture infrastructure if not capturing network hops
173
+ # We still needed to set up trace_id and outbound header base above (for outbound call tracing)
174
+ # but we can skip all request/response capture overhead
175
+ if not SF_NETWORKHOP_CAPTURE_ENABLED:
176
+ try:
177
+ await app(scope, receive, send)
178
+ except Exception as exc: # noqa: BLE001
179
+ custom_excepthook(type(exc), exc, exc.__traceback__)
180
+ raise
181
+ finally:
182
+ # CRITICAL: Clear C TLS to prevent stale data in thread pools
183
+ clear_c_tls_parent_trace_id()
184
+
185
+ # CRITICAL: Clear outbound header base to prevent stale cached headers
186
+ clear_outbound_header_base()
187
+
188
+ # CRITICAL: Clear trace_id to ensure fresh generation for next request
189
+ clear_trace_id()
190
+
191
+ # CRITICAL: Clear current request path to prevent stale data in thread pools
192
+ clear_current_request_path()
193
+
194
+ # Clear function span override for this request (ContextVar cleanup - also syncs C thread-local)
195
+ try:
196
+ clear_funcspan_override()
197
+ except Exception:
198
+ pass
199
+ return
200
+
201
+ # NOTE: req_headers already captured in single-pass scan above (if enabled)
202
+
203
+ # 2. Capture request body if enabled (must intercept receive)
204
+ req_body_chunks = []
205
+
206
+ # OPTIMIZATION: Only wrap receive if we need to capture request body
207
+ if SF_NETWORKHOP_CAPTURE_REQUEST_BODY:
208
+
209
+ async def wrapped_receive():
210
+ message = await receive()
211
+ if message["type"] == "http.request":
212
+ body = message.get("body", b"")
213
+ if body:
214
+ req_body_chunks.append(body)
215
+ return message
216
+
217
+ else:
218
+ wrapped_receive = receive
219
+
220
+ # 3. Use sys.setprofile to capture first user code frame
221
+ endpoint_info = {}
222
+ previous_profiler = sys.getprofile()
223
+
224
+ def _tracer(frame, event, arg):
225
+ # Chain to previous profiler first
226
+ if previous_profiler is not None:
227
+ try:
228
+ previous_profiler(frame, event, arg)
229
+ except Exception: # noqa: BLE001 S110
230
+ pass
231
+
232
+ # Capture first user code frame (skip Strawberry GraphQL handlers)
233
+ if event == "call" and _is_user_code(frame.f_code.co_filename):
234
+ if not endpoint_info: # Only capture once
235
+ # Skip Strawberry GraphQL handlers - they're handled by separate Strawberry extension
236
+ module_name = frame.f_globals.get("__name__", "")
237
+ if module_name.startswith("strawberry"):
238
+ if SF_DEBUG and app_config._interceptors_initialized:
239
+ print(
240
+ f"[[Litestar]] Skipping Strawberry GraphQL handler: {frame.f_code.co_name}",
241
+ log=False,
242
+ )
243
+ sys.setprofile(previous_profiler)
244
+ return _tracer
245
+
246
+ endpoint_info["filename"] = frame.f_code.co_filename
247
+ endpoint_info["line"] = frame.f_lineno
248
+ endpoint_info["name"] = frame.f_code.co_name
249
+
250
+ if SF_DEBUG and app_config._interceptors_initialized:
251
+ print(
252
+ f"[[Litestar]] Tracer captured endpoint: {frame.f_code.co_name} "
253
+ f"({frame.f_code.co_filename}:{frame.f_lineno})",
254
+ log=False,
255
+ )
256
+ # Restore previous profiler
257
+ sys.setprofile(previous_profiler)
258
+ return _tracer
259
+
260
+ sys.setprofile(_tracer)
261
+
262
+ # 4. Capture response headers and body if enabled
263
+ resp_headers = None
264
+ resp_body_chunks = []
265
+
266
+ # OPTIMIZATION: Cache debug flag check (avoid repeated lookups)
267
+ _debug_enabled = SF_DEBUG and app_config._interceptors_initialized
268
+
269
+ # OPTIMIZATION: Cache capture flags (avoid repeated global lookups in hot path)
270
+ _capture_resp_headers = SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS
271
+ _capture_resp_body = SF_NETWORKHOP_CAPTURE_RESPONSE_BODY
272
+
273
+ async def wrapped_send(message):
274
+ nonlocal resp_headers
275
+
276
+ # ULTRA-FAST PATH: Most messages just pass through without any processing
277
+ # Only http.response.body (final) triggers network hop collection
278
+ msg_type = message.get("type")
279
+
280
+ # FAST PATH: Early exit for non-body messages (http.response.start, etc.)
281
+ if msg_type != "http.response.body":
282
+ # Capture response headers if needed (only on http.response.start)
283
+ if _capture_resp_headers and msg_type == "http.response.start":
284
+ try:
285
+ resp_headers = {
286
+ k.decode(): v.decode()
287
+ for k, v in message.get("headers", [])
288
+ }
289
+ except Exception:
290
+ pass
291
+ await send(message)
292
+ return
293
+
294
+ # BODY PATH: Capture body chunks if needed
295
+ if _capture_resp_body:
296
+ body = message.get("body", b"")
297
+ if body:
298
+ resp_body_chunks.append(body)
299
+
300
+ # Send the actual message first
301
+ await send(message)
302
+
303
+ # OPTIMIZATION: Early exit if there's more body chunks coming
304
+ if message.get("more_body", False):
305
+ return
306
+
307
+ # 5. OTEL-STYLE: Emit network hop AFTER final response body sent
308
+ # Ensure profiler is restored
309
+ sys.setprofile(previous_profiler)
310
+
311
+ if endpoint_info:
312
+ try:
313
+ filename = endpoint_info["filename"]
314
+ line_no = endpoint_info["line"]
315
+ fn_name = endpoint_info["name"]
316
+ hop_key = (filename, line_no)
317
+
318
+ # Get route pattern if available
319
+ route_pattern = scope["path"]
320
+
321
+ # Check if route should be skipped
322
+ if route_pattern and should_skip_route(
323
+ route_pattern, _ROUTES_TO_SKIP
324
+ ):
325
+ if _debug_enabled:
326
+ print(
327
+ f"[[Litestar]] Skipping endpoint (route matches skip pattern): {route_pattern}",
328
+ log=False,
329
+ )
330
+ return
331
+
332
+ # Get or register endpoint
333
+ endpoint_id = _ENDPOINT_REGISTRY.get(hop_key)
334
+ if endpoint_id is None:
335
+ endpoint_id = register_endpoint(
336
+ line=str(line_no),
337
+ column="0",
338
+ name=fn_name,
339
+ entrypoint=filename,
340
+ route=route_pattern,
341
+ )
342
+ if endpoint_id >= 0:
343
+ _ENDPOINT_REGISTRY[hop_key] = endpoint_id
344
+ if _debug_enabled:
345
+ print(
346
+ f"[[Litestar]] Registered endpoint: {fn_name} @ {filename}:{line_no} (id={endpoint_id})",
347
+ log=False,
348
+ )
349
+
350
+ if endpoint_id is not None and endpoint_id >= 0:
351
+ # OPTIMIZATION: Use get_sf_trace_id() directly instead of get_or_set_sf_trace_id()
352
+ # Trace ID is GUARANTEED to be set at request start (lines 105-112)
353
+ # This saves ~11-12μs by avoiding tuple unpacking and conditional logic
354
+ session_id = get_sf_trace_id()
355
+
356
+ # OPTIMIZATION: Consolidate body chunks efficiently
357
+ req_body = None
358
+ if req_body_chunks:
359
+ joined = b"".join(req_body_chunks)
360
+ req_body = (
361
+ joined
362
+ if len(joined) <= _REQUEST_LIMIT_BYTES
363
+ else joined[:_REQUEST_LIMIT_BYTES]
364
+ )
365
+
366
+ resp_body = None
367
+ if resp_body_chunks:
368
+ joined = b"".join(resp_body_chunks)
369
+ resp_body = (
370
+ joined
371
+ if len(joined) <= _RESPONSE_LIMIT_BYTES
372
+ else joined[:_RESPONSE_LIMIT_BYTES]
373
+ )
374
+
375
+ # Direct C call - it queues to background worker, returns instantly
376
+ fast_send_network_hop_fast(
377
+ session_id=session_id,
378
+ endpoint_id=endpoint_id,
379
+ raw_path=scope["path"],
380
+ raw_query_string=scope["query_string"],
381
+ request_headers=req_headers,
382
+ request_body=req_body,
383
+ response_headers=resp_headers,
384
+ response_body=resp_body,
385
+ )
386
+ if _debug_enabled:
387
+ print(
388
+ f"[[Litestar]] Emitted NetworkHop for endpoint_id={endpoint_id}",
389
+ log=False,
390
+ )
391
+ except Exception as e: # noqa: BLE001 S110
392
+ if _debug_enabled:
393
+ print(
394
+ f"[[Litestar]] Failed to emit NetworkHop: {e}",
395
+ log=False,
396
+ )
397
+
398
+ # Exception capture
399
+ try:
400
+ await app(scope, wrapped_receive, wrapped_send)
401
+ except Exception as exc: # noqa: BLE001
402
+ # Ensure profiler is restored even on exception
403
+ sys.setprofile(previous_profiler)
404
+ custom_excepthook(type(exc), exc, exc.__traceback__)
405
+ raise
406
+ finally:
407
+ # Ensure profiler is always restored
408
+ sys.setprofile(previous_profiler)
409
+
410
+ # CRITICAL: Clear C TLS to prevent stale data in thread pools
411
+ clear_c_tls_parent_trace_id()
412
+
413
+ # CRITICAL: Clear outbound header base to prevent stale cached headers
414
+ # ContextVar does NOT automatically clean up in thread pools - must clear explicitly
415
+ clear_outbound_header_base()
416
+
417
+ # CRITICAL: Clear trace_id to ensure fresh generation for next request
418
+ # Without this, get_or_set_sf_trace_id() reuses trace_id from previous request
419
+ # causing X-Sf4-Prid to stay constant when no incoming X-Sf3-Rid header
420
+ clear_trace_id()
421
+
422
+ # CRITICAL: Clear current request path to prevent stale data in thread pools
423
+ clear_current_request_path()
424
+
425
+ # Clear function span override for this request (ContextVar cleanup - also syncs C thread-local)
426
+ try:
427
+ clear_funcspan_override()
428
+ except Exception:
429
+ pass
430
+
431
+ return _middleware
432
+
433
+
434
+ def patch_litestar(routes_to_skip: Optional[List[str]] = None) -> None:
435
+ """
436
+ OTEL-STYLE Litestar patch:
437
+ • Uses sys.setprofile tracer to capture endpoint metadata
438
+ • Captures request headers/body if enabled
439
+ • Emits network hop AFTER handler completes (zero-overhead)
440
+ • Captures response headers/body if enabled
441
+ • Universal exception handler for all exceptions
442
+ Safe no-op if Litestar is not installed.
443
+ """
444
+ global _ROUTES_TO_SKIP
445
+ _ROUTES_TO_SKIP = routes_to_skip or []
446
+
447
+ try:
448
+ import litestar
449
+ from litestar import Litestar
450
+ from litestar.middleware import DefineMiddleware
451
+ except ImportError:
452
+ return
453
+
454
+ original_init = Litestar.__init__
455
+
456
+ def patched_init(self, *args, **kwargs):
457
+ """
458
+ Injects Sailfish into every Litestar app instance by:
459
+ 1. Pre-pending ASGI middleware for header propagation + network hop capture
460
+ 2. Adding a generic exception handler for all exceptions
461
+ 3. Adding profiler reinstallation on startup
462
+ 4. Disabling Litestar's default logging config (uses our custom log handler instead)
463
+ """
464
+
465
+ # 0. Disable Litestar's default logging config to use our custom log handler
466
+ # Litestar by default uses StructLoggingConfig which overrides standard Python logging
467
+ # We need to disable it so that our CustomLogHandler (installed in setup_interceptors) works
468
+ if "logging_config" not in kwargs:
469
+ kwargs["logging_config"] = None
470
+ if SF_DEBUG and app_config._interceptors_initialized:
471
+ print(
472
+ "[[patch_litestar]] Disabled Litestar's default logging config to use Sailfish custom log handler",
473
+ log=False,
474
+ )
475
+
476
+ # Note: Profiler is already installed by unified_interceptor.py
477
+
478
+ async def _sf_reinstall_log_handler():
479
+ """
480
+ Reinstall CustomLogHandler after Litestar's logging config runs.
481
+ This ensures Sailfish log capture works even if user provides custom logging_config.
482
+ """
483
+ try:
484
+ import logging
485
+
486
+ root_logger = logging.getLogger()
487
+
488
+ # Check if CustomLogHandler is already present (avoid duplicates)
489
+ has_custom_handler = any(
490
+ isinstance(h, CustomLogHandler) for h in root_logger.handlers
491
+ )
492
+
493
+ if not has_custom_handler:
494
+ custom_handler = CustomLogHandler()
495
+ root_logger.addHandler(custom_handler)
496
+ if SF_DEBUG or True:
497
+ print(f"[Litestar] Added CustomLogHandler to root logger after Litestar config (PID={os.getpid()})", log=False)
498
+ else:
499
+ if SF_DEBUG or True:
500
+ print(f"[Litestar] CustomLogHandler already present in root logger (PID={os.getpid()})", log=False)
501
+
502
+ except Exception as e:
503
+ print(f"[Litestar] Failed to reinstall log handler: {e}", log=False)
504
+
505
+ # Add to on_startup handlers for log handler
506
+ existing_startup = kwargs.get("on_startup") or []
507
+ if not isinstance(existing_startup, list):
508
+ existing_startup = list(existing_startup)
509
+ existing_startup.insert(0, _sf_reinstall_log_handler)
510
+ kwargs["on_startup"] = existing_startup
511
+
512
+ # 2. Middleware injection
513
+ mw = list(kwargs.get("middleware") or [])
514
+ mw.insert(0, DefineMiddleware(_sf_tracing_factory))
515
+ kwargs["middleware"] = mw
516
+
517
+ # 3. Universal exception handler
518
+ def _sf_exception_handler(request, exc): # type: ignore[valid-type]
519
+ """
520
+ Litestar calls this for any Exception once routing/dep-resolution is done.
521
+ Forward to custom_excepthook and re-raise so builtin handler produces response.
522
+ """
523
+ custom_excepthook(type(exc), exc, exc.__traceback__)
524
+ raise exc # let Litestar fall back to its default logic
525
+
526
+ # Merge with user-supplied handlers
527
+ existing_handlers = kwargs.get("exception_handlers") or {}
528
+ if isinstance(existing_handlers, dict):
529
+ existing_handlers.setdefault(Exception, _sf_exception_handler)
530
+ else: # Litestar also accepts list[tuple[Exception, Handler]]
531
+ existing_handlers = list(existing_handlers) # type: ignore[arg-type]
532
+ existing_handlers.append((Exception, _sf_exception_handler))
533
+ kwargs["exception_handlers"] = existing_handlers
534
+
535
+ if SF_DEBUG and app_config._interceptors_initialized:
536
+ print(
537
+ "[[patch_litestar]] OTEL-style middleware + exception handler installed",
538
+ log=False,
539
+ )
540
+
541
+ return original_init(self, *args, **kwargs)
542
+
543
+ Litestar.__init__ = patched_init
544
+
545
+ if SF_DEBUG and app_config._interceptors_initialized:
546
+ print("[[patch_litestar]] OTEL-style patch applied", log=False)
547
+
548
+ # ── CORS patching ──────────────────────────────────────────────────
549
+ patch_litestar_cors()
550
+
551
+
552
+ def patch_litestar_cors():
553
+ """
554
+ Patch Litestar's CORSConfig to automatically inject Sailfish headers.
555
+
556
+ SAFE: Only modifies allow_headers if CORS is already configured.
557
+ Litestar uses CORSConfig dataclass for CORS configuration.
558
+ """
559
+ try:
560
+ from litestar.config.cors import CORSConfig
561
+ except ImportError:
562
+ if SF_DEBUG and app_config._interceptors_initialized:
563
+ print(
564
+ "[[patch_litestar_cors]] Litestar CORSConfig not available, skipping",
565
+ log=False,
566
+ )
567
+ return
568
+
569
+ # Check if already patched
570
+ if hasattr(CORSConfig, "_sf_cors_patched"):
571
+ if SF_DEBUG and app_config._interceptors_initialized:
572
+ print("[[patch_litestar_cors]] Already patched, skipping", log=False)
573
+ return
574
+
575
+ # Patch CORSConfig.__init__ to intercept and modify allow_headers
576
+ original_init = CORSConfig.__init__
577
+
578
+ def patched_init(
579
+ self,
580
+ allow_origins=("*",),
581
+ allow_methods=("*",),
582
+ allow_headers=("*",),
583
+ allow_credentials=False,
584
+ allow_origin_regex=None,
585
+ expose_headers=(),
586
+ max_age=600,
587
+ ):
588
+ # Intercept allow_headers parameter
589
+ if should_inject_headers(allow_headers):
590
+ allow_headers = inject_sailfish_headers(allow_headers)
591
+ if SF_DEBUG and app_config._interceptors_initialized:
592
+ print(
593
+ "[[patch_litestar_cors]] Injected Sailfish headers into CORSConfig",
594
+ log=False,
595
+ )
596
+
597
+ # Call original init with potentially modified headers
598
+ original_init(
599
+ self,
600
+ allow_origins=allow_origins,
601
+ allow_methods=allow_methods,
602
+ allow_headers=allow_headers,
603
+ allow_credentials=allow_credentials,
604
+ allow_origin_regex=allow_origin_regex,
605
+ expose_headers=expose_headers,
606
+ max_age=max_age,
607
+ )
608
+
609
+ CORSConfig.__init__ = patched_init
610
+ CORSConfig._sf_cors_patched = True
611
+
612
+ if SF_DEBUG and app_config._interceptors_initialized:
613
+ print(
614
+ "[[patch_litestar_cors]] Successfully patched Litestar CORSConfig",
615
+ log=False,
616
+ )