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,485 @@
1
+ import inspect
2
+ import os
3
+ import site
4
+ import sysconfig
5
+ import threading
6
+ from functools import lru_cache
7
+ from typing import Any, Callable, List, Optional, Set, Tuple
8
+
9
+ from ... import _sffuncspan, app_config
10
+ from ...constants import (
11
+ FUNCSPAN_OVERRIDE_HEADER_BYTES,
12
+ SAILFISH_TRACING_HEADER,
13
+ SAILFISH_TRACING_HEADER_BYTES,
14
+ )
15
+ from ...custom_excepthook import custom_excepthook
16
+ from ...env_vars import (
17
+ SF_DEBUG,
18
+ SF_NETWORKHOP_CAPTURE_REQUEST_BODY,
19
+ SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS,
20
+ SF_NETWORKHOP_CAPTURE_RESPONSE_BODY,
21
+ SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS,
22
+ SF_NETWORKHOP_REQUEST_LIMIT_MB,
23
+ SF_NETWORKHOP_RESPONSE_LIMIT_MB,
24
+ )
25
+ from ...fast_network_hop import fast_send_network_hop_fast, register_endpoint
26
+ from ...thread_local import (
27
+ clear_c_tls_parent_trace_id,
28
+ clear_current_request_path,
29
+ clear_funcspan_override,
30
+ clear_outbound_header_base,
31
+ clear_trace_id,
32
+ generate_new_trace_id,
33
+ get_or_set_sf_trace_id,
34
+ get_sf_trace_id,
35
+ set_current_request_path,
36
+ set_funcspan_override,
37
+ set_outbound_header_base,
38
+ )
39
+ from .cors_utils import inject_sailfish_headers, should_inject_headers
40
+ from .utils import _is_user_code, _unwrap_user_func, should_skip_route, reinitialize_log_print_capture_for_worker
41
+
42
+ # Size limits in bytes
43
+ _REQUEST_LIMIT_BYTES = SF_NETWORKHOP_REQUEST_LIMIT_MB * 1024 * 1024
44
+ _RESPONSE_LIMIT_BYTES = SF_NETWORKHOP_RESPONSE_LIMIT_MB * 1024 * 1024
45
+
46
+ # Pre-registered endpoint IDs
47
+ _ENDPOINT_REGISTRY: dict[tuple, int] = {}
48
+
49
+ # Module-level variable for routes to skip (set by patch_tornado)
50
+ _ROUTES_TO_SKIP = []
51
+
52
+
53
+ def patch_tornado(routes_to_skip: Optional[List[str]] = None):
54
+ """
55
+ Monkey-patch tornado.web.RequestHandler so that every request:
56
+
57
+ 1. Propagates SAILFISH_TRACING_HEADER into the ContextVar.
58
+ 2. Emits ONE NetworkHop when user-land verb handler starts.
59
+ 3. Funnels *all* exceptions—including tornado.web.HTTPError—through
60
+ custom_excepthook before Tornado's own error machinery runs.
61
+
62
+ Safe no-op if Tornado isn't installed.
63
+ """
64
+ global _ROUTES_TO_SKIP
65
+ _ROUTES_TO_SKIP = routes_to_skip or []
66
+ try:
67
+ import tornado.web
68
+ except ImportError: # Tornado not installed
69
+ return
70
+
71
+ # Note: Profiler is already installed by unified_interceptor.py
72
+
73
+ # --------------------------------------------------------------- #
74
+ # a) Header capture + endpoint metadata (prepare)
75
+ # --------------------------------------------------------------- #
76
+ original_prepare = tornado.web.RequestHandler.prepare
77
+
78
+ def patched_prepare(self, *args, **kwargs):
79
+ # Set current request path for route-based suppression (SF_DISABLE_INBOUND_NETWORK_TRACING_ON_ROUTES)
80
+ set_current_request_path(self.request.path)
81
+
82
+ # -- 1) PERFORMANCE: Single-pass bytes-level header scan (similar to FastAPI optimization)
83
+ # Tornado stores headers as HTTPHeaders object, iterate once to extract needed headers
84
+ incoming_trace_header = None
85
+ funcspan_override_header = None
86
+
87
+ # Scan headers once
88
+ for name, value in self.request.headers.get_all():
89
+ name_lower = name.lower()
90
+ if name_lower == SAILFISH_TRACING_HEADER.lower():
91
+ incoming_trace_header = value
92
+ elif name_lower == "x-sf3-functionspancaptureoverride":
93
+ funcspan_override_header = value
94
+
95
+ # -- 2) CRITICAL: Seed/ensure trace_id immediately (BEFORE any outbound work)
96
+ if incoming_trace_header:
97
+ # Incoming X-Sf3-Rid header provided - use it
98
+ get_or_set_sf_trace_id(
99
+ incoming_trace_header, is_associated_with_inbound_request=True
100
+ )
101
+ else:
102
+ # No incoming X-Sf3-Rid header - generate fresh trace_id for this request
103
+ generate_new_trace_id()
104
+
105
+ # -- 3) Set function span override if provided
106
+ if funcspan_override_header:
107
+ try:
108
+ set_funcspan_override(funcspan_override_header)
109
+ if SF_DEBUG and app_config._interceptors_initialized:
110
+ print(
111
+ f"[[Tornado.prepare]] Set function span override from header: {funcspan_override_header}",
112
+ log=False,
113
+ )
114
+ except Exception as e:
115
+ if SF_DEBUG and app_config._interceptors_initialized:
116
+ print(
117
+ f"[[Tornado.prepare]] Failed to set function span override: {e}",
118
+ log=False,
119
+ )
120
+
121
+ # -- 4) Initialize outbound base without list/allocs from split()
122
+ try:
123
+ trace_id = get_sf_trace_id()
124
+ if trace_id:
125
+ s = str(trace_id)
126
+ i = s.find("/") # session
127
+ j = s.find("/", i + 1) if i != -1 else -1 # page
128
+ if j != -1:
129
+ base_trace = s[:j] # "session/page"
130
+ set_outbound_header_base(
131
+ base_trace=base_trace,
132
+ parent_trace_id=s, # "session/page/uuid"
133
+ funcspan=funcspan_override_header,
134
+ )
135
+ if SF_DEBUG and app_config._interceptors_initialized:
136
+ print(
137
+ f"[[Tornado.prepare]] Initialized outbound header base (base={base_trace[:16]}...)",
138
+ log=False,
139
+ )
140
+ except Exception as e:
141
+ if SF_DEBUG and app_config._interceptors_initialized:
142
+ print(
143
+ f"[[Tornado.prepare]] Failed to initialize outbound header base: {e}",
144
+ log=False,
145
+ )
146
+
147
+ # -- 5) Capture request headers if enabled
148
+ if SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS:
149
+ try:
150
+ req_headers = dict(self.request.headers)
151
+ self._sf_request_headers = req_headers
152
+ if SF_DEBUG and app_config._interceptors_initialized:
153
+ print(
154
+ f"[[Tornado]] Captured request headers: {len(req_headers)} headers",
155
+ log=False,
156
+ )
157
+ except Exception as e:
158
+ if SF_DEBUG and app_config._interceptors_initialized:
159
+ print(
160
+ f"[[Tornado]] Failed to capture request headers: {e}", log=False
161
+ )
162
+
163
+ # -- 6) Capture request body if enabled
164
+ if SF_NETWORKHOP_CAPTURE_REQUEST_BODY:
165
+ try:
166
+ body = self.request.body
167
+ if body:
168
+ req_body = body[:_REQUEST_LIMIT_BYTES]
169
+ self._sf_request_body = req_body
170
+ if SF_DEBUG and app_config._interceptors_initialized:
171
+ print(
172
+ f"[[Tornado]] Request body capture: {len(req_body)} bytes",
173
+ log=False,
174
+ )
175
+ except Exception as e:
176
+ if SF_DEBUG and app_config._interceptors_initialized:
177
+ print(f"[[Tornado]] Failed to capture request body: {e}", log=False)
178
+
179
+ # -- 7) OTEL-STYLE: Pre-register endpoint and store endpoint_id
180
+ method_name = self.request.method.lower()
181
+ handler_fn = getattr(self, method_name, None)
182
+
183
+ if callable(handler_fn):
184
+ module = getattr(handler_fn, "__module__", "")
185
+ if not module.startswith("strawberry"):
186
+ real_fn = _unwrap_user_func(handler_fn)
187
+ code_obj = getattr(real_fn, "__code__", None)
188
+ if code_obj and _is_user_code(code_obj.co_filename):
189
+ hop_key = (code_obj.co_filename, code_obj.co_firstlineno)
190
+
191
+ # Extract route pattern from the handler's route_spec
192
+ route_pattern = None
193
+ try:
194
+ # Tornado stores the route pattern in the request's path_kwargs
195
+ # We can get the pattern from the application's handlers
196
+ if hasattr(self, "application") and hasattr(
197
+ self.application, "handlers"
198
+ ):
199
+ # Find the matching route spec
200
+ for host_pattern, handlers in self.application.handlers:
201
+ for spec in handlers:
202
+ # spec is a URLSpec with regex, handler_class, kwargs, name
203
+ if spec.handler_class == type(self):
204
+ route_pattern = spec.regex.pattern
205
+ break
206
+ if route_pattern:
207
+ break
208
+ except Exception:
209
+ pass
210
+
211
+ # Check if route should be skipped
212
+ if should_skip_route(
213
+ route_pattern or self.request.path, _ROUTES_TO_SKIP
214
+ ):
215
+ if SF_DEBUG and app_config._interceptors_initialized:
216
+ print(
217
+ f"[[Tornado]] Skipping endpoint (route matches skip pattern): {route_pattern or self.request.path}",
218
+ log=False,
219
+ )
220
+ return original_prepare(self, *args, **kwargs)
221
+
222
+ # Pre-register endpoint if not already registered
223
+ endpoint_id = _ENDPOINT_REGISTRY.get(hop_key)
224
+ if endpoint_id is None:
225
+ endpoint_id = register_endpoint(
226
+ line=str(code_obj.co_firstlineno),
227
+ column="0",
228
+ name=real_fn.__name__,
229
+ entrypoint=code_obj.co_filename,
230
+ route=route_pattern,
231
+ )
232
+ if endpoint_id >= 0:
233
+ _ENDPOINT_REGISTRY[hop_key] = endpoint_id
234
+ if SF_DEBUG and app_config._interceptors_initialized:
235
+ print(
236
+ f"[[Tornado]] Registered endpoint: {real_fn.__name__} @ "
237
+ f"{code_obj.co_filename}:{code_obj.co_firstlineno} (id={endpoint_id})",
238
+ log=False,
239
+ )
240
+
241
+ # Store endpoint_id for on_finish() to emit
242
+ self._sf_endpoint_id = endpoint_id
243
+
244
+ if SF_DEBUG and app_config._interceptors_initialized:
245
+ print(
246
+ f"[[Tornado]] Captured endpoint: {real_fn.__name__} "
247
+ f"({code_obj.co_filename}:{code_obj.co_firstlineno}) endpoint_id={endpoint_id}",
248
+ log=False,
249
+ )
250
+
251
+ return original_prepare(self, *args, **kwargs)
252
+
253
+ tornado.web.RequestHandler.prepare = patched_prepare
254
+
255
+ # --------------------------------------------------------------- #
256
+ # b) Exception capture – patch _execute and write_error
257
+ # --------------------------------------------------------------- #
258
+ original_execute = tornado.web.RequestHandler._execute
259
+ original_write_error = tornado.web.RequestHandler.write_error
260
+
261
+ async def patched_execute(self, *args, **kwargs):
262
+ try:
263
+ return await original_execute(self, *args, **kwargs)
264
+ except Exception as exc: # HTTPError included
265
+ custom_excepthook(type(exc), exc, exc.__traceback__)
266
+ raise # let Tornado handle 500/4xx
267
+
268
+ def patched_write_error(self, status_code, **kwargs):
269
+ """
270
+ Tornado calls write_error for HTTPError and uncaught exceptions.
271
+ Capture the exception (when provided) before rendering.
272
+ """
273
+ exc_info = kwargs.get("exc_info")
274
+ if exc_info and isinstance(exc_info, tuple) and exc_info[1]:
275
+ exc_type, exc_val, exc_tb = exc_info
276
+ custom_excepthook(exc_type, exc_val, exc_tb)
277
+ # Fallback: still call original renderer
278
+ return original_write_error(self, status_code, **kwargs)
279
+
280
+ tornado.web.RequestHandler._execute = patched_execute
281
+ tornado.web.RequestHandler.write_error = patched_write_error
282
+
283
+ # --------------------------------------------------------------- #
284
+ # c) CORS patching – inject Sailfish headers
285
+ # --------------------------------------------------------------- #
286
+ patch_tornado_cors()
287
+
288
+
289
+ def patch_tornado_cors():
290
+ """
291
+ Patch Tornado's RequestHandler to automatically inject Sailfish headers into CORS.
292
+
293
+ SAFE: Only modifies Access-Control-Allow-Headers if the handler sets it.
294
+ Tornado doesn't have a standard CORS library, so we patch the common patterns:
295
+ 1. set_default_headers() - called for every request
296
+ 2. options() - called for preflight requests
297
+ """
298
+ try:
299
+ import tornado.web
300
+ except ImportError:
301
+ if SF_DEBUG and app_config._interceptors_initialized:
302
+ print("[[patch_tornado_cors]] Tornado not available, skipping", log=False)
303
+ return
304
+
305
+ # Check if already patched
306
+ if hasattr(tornado.web.RequestHandler, "_sf_cors_patched"):
307
+ if SF_DEBUG and app_config._interceptors_initialized:
308
+ print("[[patch_tornado_cors]] Already patched, skipping", log=False)
309
+ return
310
+
311
+ # Patch set_default_headers to intercept and modify Access-Control-Allow-Headers
312
+ original_set_header = tornado.web.RequestHandler.set_header
313
+
314
+ def patched_set_header(self, name, value):
315
+ # Intercept Access-Control-Allow-Headers header
316
+ if name.lower() == "access-control-allow-headers":
317
+ if should_inject_headers(value):
318
+ injected = inject_sailfish_headers(value)
319
+ # Convert list back to comma-separated string for Tornado
320
+ # (inject_sailfish_headers returns a list, but Tornado expects a string)
321
+ if isinstance(injected, list):
322
+ value = ", ".join(injected)
323
+ else:
324
+ value = injected
325
+ if SF_DEBUG and app_config._interceptors_initialized:
326
+ print(
327
+ f"[[patch_tornado_cors]] Injected Sailfish headers: {value}",
328
+ log=False,
329
+ )
330
+
331
+ # Call original set_header
332
+ return original_set_header(self, name, value)
333
+
334
+ tornado.web.RequestHandler.set_header = patched_set_header
335
+ tornado.web.RequestHandler._sf_cors_patched = True
336
+
337
+ if SF_DEBUG and app_config._interceptors_initialized:
338
+ print(
339
+ "[[patch_tornado_cors]] Successfully patched Tornado RequestHandler.set_header",
340
+ log=False,
341
+ )
342
+
343
+
344
+ # Hook into on_finish for OTEL-style post-response emission
345
+ try:
346
+ import tornado.web
347
+
348
+ original_on_finish = tornado.web.RequestHandler.on_finish
349
+
350
+ def patched_on_finish(self):
351
+ """
352
+ OTEL-STYLE: Emit network hop AFTER response is sent.
353
+ Tornado calls on_finish() after the response is fully sent to client.
354
+ """
355
+ # Emit network hop if we captured endpoint_id
356
+ endpoint_id = getattr(self, "_sf_endpoint_id", None)
357
+ if endpoint_id is not None and endpoint_id >= 0:
358
+ try:
359
+ _, session_id = get_or_set_sf_trace_id()
360
+
361
+ # Get captured request data
362
+ req_headers = getattr(self, "_sf_request_headers", None)
363
+ req_body = getattr(self, "_sf_request_body", None)
364
+
365
+ # Capture response headers if enabled
366
+ resp_headers = None
367
+ if SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS:
368
+ try:
369
+ resp_headers = dict(self._headers)
370
+ if SF_DEBUG and app_config._interceptors_initialized:
371
+ print(
372
+ f"[[Tornado]] Captured response headers: {len(resp_headers)} headers",
373
+ log=False,
374
+ )
375
+ except Exception as e:
376
+ if SF_DEBUG and app_config._interceptors_initialized:
377
+ print(
378
+ f"[[Tornado]] Failed to capture response headers: {e}",
379
+ log=False,
380
+ )
381
+
382
+ # Capture response body if enabled
383
+ resp_body = None
384
+ if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY:
385
+ try:
386
+ # Tornado stores written chunks in self._write_buffer
387
+ if hasattr(self, "_write_buffer") and self._write_buffer:
388
+ body_parts = [
389
+ chunk
390
+ for chunk in self._write_buffer
391
+ if isinstance(chunk, bytes)
392
+ ]
393
+ if body_parts:
394
+ full_body = b"".join(body_parts)
395
+ resp_body = full_body[:_RESPONSE_LIMIT_BYTES]
396
+ if SF_DEBUG and app_config._interceptors_initialized:
397
+ print(
398
+ f"[[Tornado]] Captured response body: {len(resp_body)} bytes",
399
+ log=False,
400
+ )
401
+ except Exception as e:
402
+ if SF_DEBUG and app_config._interceptors_initialized:
403
+ print(
404
+ f"[[Tornado]] Failed to capture response body: {e}",
405
+ log=False,
406
+ )
407
+
408
+ # Extract raw path and query string for C to parse
409
+ raw_path = self.request.path # e.g., "/log"
410
+ # Tornado's request.query is the query string without '?'
411
+ raw_query = (
412
+ self.request.query.encode("utf-8") if self.request.query else b""
413
+ ) # e.g., b"foo=5"
414
+
415
+ if SF_DEBUG and app_config._interceptors_initialized:
416
+ print(
417
+ f"[[Tornado]] About to emit network hop: endpoint_id={endpoint_id}, "
418
+ f"req_headers={'present' if req_headers else 'None'}, "
419
+ f"req_body={len(req_body) if req_body else 0} bytes, "
420
+ f"resp_headers={'present' if resp_headers else 'None'}, "
421
+ f"resp_body={len(resp_body) if resp_body else 0} bytes",
422
+ log=False,
423
+ )
424
+
425
+ # Direct C call - queues to background worker, returns instantly
426
+ # C will parse route and query_params from raw data
427
+ fast_send_network_hop_fast(
428
+ session_id=session_id,
429
+ endpoint_id=endpoint_id,
430
+ raw_path=raw_path,
431
+ raw_query_string=raw_query,
432
+ request_headers=req_headers,
433
+ request_body=req_body,
434
+ response_headers=resp_headers,
435
+ response_body=resp_body,
436
+ )
437
+
438
+ if SF_DEBUG and app_config._interceptors_initialized:
439
+ print(
440
+ f"[[Tornado]] Emitted network hop: endpoint_id={endpoint_id} "
441
+ f"session={session_id}",
442
+ log=False,
443
+ )
444
+ except Exception as e: # noqa: BLE001 S110
445
+ if SF_DEBUG and app_config._interceptors_initialized:
446
+ print(f"[[Tornado]] Failed to emit network hop: {e}", log=False)
447
+
448
+ # Clear function span override for this request (ContextVar cleanup)
449
+ try:
450
+ clear_funcspan_override()
451
+ except Exception:
452
+ pass
453
+
454
+ # CRITICAL: Clear C TLS to prevent stale data in thread pools
455
+ try:
456
+ clear_c_tls_parent_trace_id()
457
+ except Exception:
458
+ pass
459
+
460
+ # CRITICAL: Clear outbound header base to prevent stale cached headers
461
+ # ContextVar does NOT automatically clean up in thread pools - must clear explicitly
462
+ try:
463
+ clear_outbound_header_base()
464
+ except Exception:
465
+ pass
466
+
467
+ # CRITICAL: Clear trace_id to ensure fresh generation for next request
468
+ # Without this, get_or_set_sf_trace_id() reuses trace_id from previous request
469
+ # causing X-Sf4-Prid to stay constant when no incoming X-Sf3-Rid header
470
+ try:
471
+ clear_trace_id()
472
+ except Exception:
473
+ pass
474
+
475
+ # CRITICAL: Clear current request path to prevent stale data in thread pools
476
+ try:
477
+ clear_current_request_path()
478
+ except Exception:
479
+ pass
480
+
481
+ return original_on_finish(self)
482
+
483
+ tornado.web.RequestHandler.on_finish = patched_on_finish
484
+ except ImportError:
485
+ pass
@@ -0,0 +1,170 @@
1
+ import fnmatch
2
+ import functools
3
+ import inspect
4
+ import os
5
+ import sysconfig
6
+ from typing import Any, Callable, List, Optional, Set
7
+
8
+ try:
9
+ from ... import _sffastlog
10
+ from ... import app_config
11
+ from ...custom_output_wrapper import _ensure_fast_print_initialized
12
+ from ...interceptors import _COLLECT_LOGS_MUTATION
13
+ import sf_veritas.custom_output_wrapper as output_wrapper
14
+ from ...env_vars import PRINT_CONFIGURATION_STATUSES, SF_DEBUG
15
+
16
+ _SFFASTLOG_OK = True
17
+ except ImportError:
18
+ _sffastlog = None
19
+ _SFFASTLOG_OK = False
20
+
21
+ _stdlib = sysconfig.get_paths()["stdlib"]
22
+
23
+
24
+ _ATTR_CANDIDATES = (
25
+ "resolver",
26
+ "func",
27
+ "python_func",
28
+ "_resolver",
29
+ "wrapped_func",
30
+ "__func",
31
+ )
32
+
33
+
34
+ def _is_user_code(path: Optional[str] = None) -> bool:
35
+ return (
36
+ bool(path)
37
+ and not path.startswith(_stdlib)
38
+ and "site-packages" not in path
39
+ and "dist-packages" not in path
40
+ and not path.startswith("<")
41
+ )
42
+
43
+
44
+ def _unwrap_user_func(fn: Callable[..., Any]) -> Callable[..., Any]:
45
+ """Unwrap decorators & closures until we find your user function."""
46
+ seen: Set[int] = set()
47
+ queue = [fn]
48
+ while queue:
49
+ current = queue.pop()
50
+ if id(current) in seen:
51
+ continue
52
+ seen.add(id(current))
53
+
54
+ # Handle functools.partial explicitly
55
+ if isinstance(current, functools.partial):
56
+ if current.func:
57
+ queue.append(current.func)
58
+ continue
59
+
60
+ if inspect.isfunction(current) and _is_user_code(current.__code__.co_filename):
61
+ return current
62
+
63
+ inner = getattr(current, "__wrapped__", None)
64
+ if inner:
65
+ queue.append(inner)
66
+
67
+ for attr in _ATTR_CANDIDATES:
68
+ attr_val = getattr(current, attr, None)
69
+ if inspect.isfunction(attr_val):
70
+ queue.append(attr_val)
71
+
72
+ for cell in getattr(current, "__closure__", []) or []:
73
+ cc = cell.cell_contents
74
+ if inspect.isfunction(cc):
75
+ queue.append(cc)
76
+
77
+ return fn # fallback
78
+
79
+
80
+ def should_skip_route(route_pattern: str, routes_to_skip: List[str]) -> bool:
81
+ """
82
+ Check if route should be skipped based on wildcard patterns.
83
+
84
+ Supports Unix shell-style wildcards:
85
+ - Exact match: "/healthz" matches "/healthz"
86
+ - Wildcard *: "/he*" matches "/health", "/healthz", "/healthz/foo"
87
+ - Wildcard ?: "/health?" matches "/healthz" but not "/health"
88
+ - Character sets: "/health[z12]" matches "/healthz", "/health1", "/health2"
89
+
90
+ Examples:
91
+ - "/he*" → matches "/health", "/healthz", "/healthz/foo"
92
+ - "/metrics*" → matches "/metrics", "/metrics/detailed"
93
+ - "/api/internal/*" → matches "/api/internal/status", "/api/internal/debug"
94
+ - "*/admin" → matches "/foo/admin", "/bar/admin"
95
+
96
+ Args:
97
+ route_pattern: Route pattern to check (e.g., "/healthz", "/log/{n}")
98
+ routes_to_skip: List of patterns to skip (can contain wildcards)
99
+
100
+ Returns:
101
+ True if route should be skipped, False otherwise
102
+ """
103
+ if not routes_to_skip or not route_pattern:
104
+ return False
105
+
106
+ for skip_pattern in routes_to_skip:
107
+ # Use fnmatch for Unix shell-style wildcards
108
+ # This supports * (matches anything) and ? (matches single char)
109
+ if fnmatch.fnmatch(route_pattern, skip_pattern):
110
+ return True
111
+
112
+ return False
113
+
114
+
115
+ def reinitialize_log_print_capture_for_worker() -> None:
116
+ """
117
+ Reinitialize log/print capture for worker processes.
118
+
119
+ CRITICAL: When web servers fork workers (e.g., Supervisor with numprocs=2, Gunicorn),
120
+ daemon threads don't survive the fork but global flags do. This function:
121
+ 1. Resets global initialization flags
122
+ 2. Creates new daemon threads for log/print capture in each worker
123
+
124
+ Called by web framework middleware __init__ methods to ensure each worker
125
+ has its own sender threads.
126
+ """
127
+ if not _SFFASTLOG_OK or not _sffastlog:
128
+ if SF_DEBUG or PRINT_CONFIGURATION_STATUSES:
129
+ print(f"[WebFramework] Worker PID={os.getpid()} _sffastlog not available", log=False)
130
+ return
131
+
132
+ try:
133
+ if SF_DEBUG or PRINT_CONFIGURATION_STATUSES:
134
+ print(f"[WebFramework] Worker PID={os.getpid()} startup - reinitializing log/print capture", log=False)
135
+
136
+ # Force reset of global flags to trigger re-initialization
137
+ output_wrapper._FAST_PRINT_READY = False
138
+
139
+ # Reinitialize print capture (creates new daemon threads for this worker)
140
+ print_ok = _ensure_fast_print_initialized()
141
+
142
+ # Reinitialize log capture (creates new daemon threads for this worker)
143
+ endpoint = getattr(app_config, "_sailfish_graphql_endpoint", None)
144
+ api_key = getattr(app_config, "_sailfish_api_key", None)
145
+ service_uuid = getattr(app_config, "_service_uuid", None)
146
+ library = getattr(app_config, "library", "sailfish-python")
147
+ version = getattr(app_config, "version", "0.0.0")
148
+ http2 = 1 if os.getenv("SF_NBPOST_HTTP2", "0") == "1" else 0
149
+
150
+ if endpoint and api_key and service_uuid:
151
+ log_ok = _sffastlog.init(
152
+ url=endpoint,
153
+ query=_COLLECT_LOGS_MUTATION,
154
+ api_key=str(api_key),
155
+ service_uuid=str(service_uuid),
156
+ library=str(library),
157
+ version=str(version),
158
+ http2=http2,
159
+ )
160
+
161
+ if SF_DEBUG or PRINT_CONFIGURATION_STATUSES:
162
+ print(f"[WebFramework] Worker PID={os.getpid()} log/print capture initialized: print={print_ok}, log={log_ok}", log=False)
163
+ else:
164
+ if SF_DEBUG or PRINT_CONFIGURATION_STATUSES:
165
+ print(f"[WebFramework] Worker PID={os.getpid()} log/print capture skipped (missing config)", log=False)
166
+
167
+ except Exception as e:
168
+ print(f"[WebFramework] Worker PID={os.getpid()} failed to reinitialize log/print capture: {e}", log=False)
169
+ import traceback
170
+ traceback.print_exc()
@@ -0,0 +1,13 @@
1
+ import builtins
2
+
3
+ from .custom_print import SF_DEBUG, custom_print
4
+
5
+
6
+ def override_print():
7
+ if hasattr(builtins, "_original_print"):
8
+ return
9
+ # Save the original print function
10
+ builtins._original_print = builtins.print
11
+
12
+ # Override the built-in print function
13
+ builtins.print = custom_print