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,526 @@
1
+ """
2
+ Adds:
3
+ • before_request hook → ContextVar propagation (unchanged).
4
+ • global add_url_rule / Blueprint.add_url_rule patch →
5
+ wraps every endpoint in a hop-emitting closure.
6
+ """
7
+
8
+ import os
9
+ import threading
10
+ from functools import wraps
11
+ from types import MethodType
12
+ from typing import Callable, List, Optional, Set, Tuple
13
+
14
+ from ... import _sffuncspan, _sffuncspan_config, app_config
15
+ from ...constants import (
16
+ FUNCSPAN_OVERRIDE_HEADER_BYTES,
17
+ SAILFISH_TRACING_HEADER,
18
+ SAILFISH_TRACING_HEADER_BYTES,
19
+ )
20
+ from ...custom_excepthook import custom_excepthook
21
+ from ...env_vars import (
22
+ SF_DEBUG,
23
+ SF_NETWORKHOP_CAPTURE_REQUEST_BODY,
24
+ SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS,
25
+ SF_NETWORKHOP_CAPTURE_RESPONSE_BODY,
26
+ SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS,
27
+ SF_NETWORKHOP_REQUEST_LIMIT_MB,
28
+ SF_NETWORKHOP_RESPONSE_LIMIT_MB,
29
+ )
30
+ from ...fast_network_hop import fast_send_network_hop_fast, register_endpoint
31
+ from ...thread_local import (
32
+ clear_c_tls_parent_trace_id,
33
+ clear_current_request_path,
34
+ clear_funcspan_override,
35
+ clear_outbound_header_base,
36
+ clear_trace_id,
37
+ generate_new_trace_id,
38
+ get_or_set_sf_trace_id,
39
+ get_sf_trace_id,
40
+ set_current_request_path,
41
+ set_funcspan_override,
42
+ set_outbound_header_base,
43
+ )
44
+ from .cors_utils import inject_sailfish_headers, should_inject_headers
45
+ from .utils import should_skip_route
46
+
47
+ # Size limits in bytes
48
+ _REQUEST_LIMIT_BYTES = SF_NETWORKHOP_REQUEST_LIMIT_MB * 1024 * 1024
49
+ _RESPONSE_LIMIT_BYTES = SF_NETWORKHOP_RESPONSE_LIMIT_MB * 1024 * 1024
50
+
51
+ # Pre-registered endpoint IDs (filename:line -> endpoint_id)
52
+ _ENDPOINT_REGISTRY: dict[tuple, int] = {}
53
+
54
+ # Module-level variable for routes to skip (set by patch_flask)
55
+ _ROUTES_TO_SKIP = []
56
+
57
+ # ────────────────────────────────────────────────────────────────
58
+ # shared helpers
59
+ # ────────────────────────────────────────────────────────────────
60
+ from .utils import _is_user_code, _unwrap_user_func, reinitialize_log_print_capture_for_worker # cached helpers
61
+
62
+
63
+ def _make_hop_wrapper(
64
+ fn: Callable,
65
+ hop_key: Tuple[str, int],
66
+ fn_name: str,
67
+ filename: str,
68
+ route: str = None,
69
+ ):
70
+ """
71
+ OTEL-STYLE: Store endpoint metadata in flask.g during request.
72
+ Emission happens in @after_request hook for zero-overhead.
73
+ Pre-register endpoint on first wrap.
74
+ """
75
+ from flask import g
76
+
77
+ # Check if route should be skipped
78
+ if should_skip_route(route, _ROUTES_TO_SKIP):
79
+ if SF_DEBUG and app_config._interceptors_initialized:
80
+ print(
81
+ f"[[Flask]] Skipping endpoint (route matches skip pattern): {route}",
82
+ log=False,
83
+ )
84
+ # Return original function unwrapped - no telemetry
85
+ return fn
86
+
87
+ # Pre-register endpoint if not already registered
88
+ endpoint_id = _ENDPOINT_REGISTRY.get(hop_key)
89
+ if endpoint_id is None:
90
+ endpoint_id = register_endpoint(
91
+ line=str(hop_key[1]),
92
+ column="0",
93
+ name=fn_name,
94
+ entrypoint=filename,
95
+ route=route,
96
+ )
97
+ if endpoint_id >= 0:
98
+ _ENDPOINT_REGISTRY[hop_key] = endpoint_id
99
+ if SF_DEBUG and app_config._interceptors_initialized:
100
+ print(
101
+ f"[[Flask]] Registered endpoint: {fn_name} @ {filename}:{hop_key[1]} (id={endpoint_id})",
102
+ log=False,
103
+ )
104
+
105
+ @wraps(fn)
106
+ def _wrapped(*args, **kwargs): # noqa: ANN001
107
+ # OTEL-STYLE: Store endpoint_id for after_request to emit
108
+ if not hasattr(g, "_sf_endpoint_id"):
109
+ g._sf_endpoint_id = endpoint_id
110
+
111
+ if SF_DEBUG and app_config._interceptors_initialized:
112
+ print(
113
+ f"[[Flask]] Captured endpoint: {fn_name} ({filename}:{hop_key[1]}) endpoint_id={endpoint_id}",
114
+ log=False,
115
+ )
116
+
117
+ return fn(*args, **kwargs)
118
+
119
+ return _wrapped
120
+
121
+
122
+ def _wrap_if_user_view(endpoint_fn: Callable, route: str = None):
123
+ """
124
+ Decide whether to wrap `endpoint_fn`. Returns the (possibly wrapped)
125
+ callable. Suppress wrapping for library code or Strawberry handlers.
126
+ """
127
+ real_fn = _unwrap_user_func(endpoint_fn)
128
+
129
+ # Skip Strawberry GraphQL views – Strawberry extension owns them
130
+ if real_fn.__module__.startswith("strawberry"):
131
+ return endpoint_fn
132
+
133
+ code = getattr(real_fn, "__code__", None)
134
+ if not code or not _is_user_code(code.co_filename):
135
+ return endpoint_fn
136
+
137
+ hop_key = (code.co_filename, code.co_firstlineno)
138
+ return _make_hop_wrapper(
139
+ endpoint_fn, hop_key, real_fn.__name__, code.co_filename, route=route
140
+ )
141
+
142
+
143
+ # ────────────────────────────────────────────────────────────────
144
+ # Request hooks: before (header capture) + after (OTEL-style emission)
145
+ # ────────────────────────────────────────────────────────────────
146
+ def _install_request_hooks(app):
147
+ from flask import g, request
148
+
149
+ @app.before_request
150
+ def _extract_sf_trace():
151
+ # Set current request path for route-based suppression (SF_DISABLE_INBOUND_NETWORK_TRACING_ON_ROUTES)
152
+ set_current_request_path(request.path)
153
+
154
+ # 1. CRITICAL: Seed/ensure trace_id immediately (BEFORE any outbound work)
155
+ hdr = request.headers.get(SAILFISH_TRACING_HEADER)
156
+ if hdr:
157
+ get_or_set_sf_trace_id(hdr, is_associated_with_inbound_request=True)
158
+ else:
159
+ # No incoming X-Sf3-Rid header - generate fresh trace_id for this request
160
+ generate_new_trace_id()
161
+
162
+ # Optional funcspan override (highest priority!)
163
+ funcspan_override_header = request.headers.get(
164
+ "X-Sf3-FunctionSpanCaptureOverride"
165
+ )
166
+ if funcspan_override_header:
167
+ try:
168
+ set_funcspan_override(funcspan_override_header)
169
+ if SF_DEBUG and app_config._interceptors_initialized:
170
+ print(
171
+ f"[[Flask.before_request]] Set function span override from header: {funcspan_override_header}",
172
+ log=False,
173
+ )
174
+ except Exception as e:
175
+ if SF_DEBUG and app_config._interceptors_initialized:
176
+ print(
177
+ f"[[Flask.before_request]] Failed to set function span override: {e}",
178
+ log=False,
179
+ )
180
+
181
+ # Initialize outbound base without list/allocs from split()
182
+ try:
183
+ trace_id = get_sf_trace_id()
184
+ if trace_id:
185
+ s = str(trace_id)
186
+ i = s.find("/") # session
187
+ j = s.find("/", i + 1) if i != -1 else -1 # page
188
+ if j != -1:
189
+ base_trace = s[:j] # "session/page"
190
+ set_outbound_header_base(
191
+ base_trace=base_trace,
192
+ parent_trace_id=s, # "session/page/uuid"
193
+ funcspan=funcspan_override_header,
194
+ )
195
+ if SF_DEBUG and app_config._interceptors_initialized:
196
+ print(
197
+ f"[[Flask.before_request]] Initialized outbound header base (base={base_trace[:16]}...)",
198
+ log=False,
199
+ )
200
+ except Exception as e:
201
+ if SF_DEBUG and app_config._interceptors_initialized:
202
+ print(
203
+ f"[[Flask.before_request]] Failed to initialize outbound header base: {e}",
204
+ log=False,
205
+ )
206
+
207
+ @app.after_request
208
+ def _emit_network_hop(response):
209
+ """
210
+ OTEL-STYLE: Emit network hop AFTER response is built.
211
+ This ensures telemetry doesn't impact response time to client.
212
+ Captures request/response headers and bodies if enabled.
213
+ """
214
+ endpoint_id = getattr(g, "_sf_endpoint_id", None)
215
+ if endpoint_id is not None and endpoint_id >= 0:
216
+ try:
217
+ _, session_id = get_or_set_sf_trace_id()
218
+
219
+ # Capture request headers if enabled
220
+ req_headers = None
221
+ if SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS:
222
+ try:
223
+ req_headers = dict(request.headers)
224
+ except Exception:
225
+ pass
226
+
227
+ # Capture request body if enabled
228
+ req_body = None
229
+ if SF_NETWORKHOP_CAPTURE_REQUEST_BODY:
230
+ try:
231
+ # Flask caches request.data, so this is safe
232
+ body = request.get_data()
233
+ if body and len(body) > 0:
234
+ req_body = body[:_REQUEST_LIMIT_BYTES]
235
+ except Exception:
236
+ pass
237
+
238
+ # Capture response headers if enabled
239
+ resp_headers = None
240
+ if SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS:
241
+ try:
242
+ resp_headers = dict(response.headers)
243
+ except Exception:
244
+ pass
245
+
246
+ # Capture response body if enabled
247
+ resp_body = None
248
+ if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY:
249
+ try:
250
+ if hasattr(response, "get_data"):
251
+ body = response.get_data()
252
+ if body and len(body) > 0:
253
+ resp_body = body[:_RESPONSE_LIMIT_BYTES]
254
+ except Exception:
255
+ pass
256
+
257
+ # Extract raw path and query string for C to parse
258
+ raw_path = request.path # e.g., "/log"
259
+ raw_query = request.query_string # e.g., b"foo=5"
260
+
261
+ # Direct C call - queues to background worker, returns instantly
262
+ # C will parse route and query_params from raw data
263
+ fast_send_network_hop_fast(
264
+ session_id=session_id,
265
+ endpoint_id=endpoint_id,
266
+ raw_path=raw_path,
267
+ raw_query_string=raw_query,
268
+ request_headers=req_headers,
269
+ request_body=req_body,
270
+ response_headers=resp_headers,
271
+ response_body=resp_body,
272
+ )
273
+
274
+ if SF_DEBUG and app_config._interceptors_initialized:
275
+ print(
276
+ f"[[Flask]] Emitted network hop: endpoint_id={endpoint_id} "
277
+ f"session={session_id}",
278
+ log=False,
279
+ )
280
+ except Exception as e: # noqa: BLE001 S110
281
+ if SF_DEBUG and app_config._interceptors_initialized:
282
+ print(f"[[Flask]] Failed to emit network hop: {e}", log=False)
283
+
284
+ # CRITICAL: Clear C TLS to prevent stale data in thread pools
285
+ clear_c_tls_parent_trace_id()
286
+
287
+ # CRITICAL: Clear outbound header base to prevent stale cached headers
288
+ # ContextVar does NOT automatically clean up in thread pools - must clear explicitly
289
+ clear_outbound_header_base()
290
+
291
+ # CRITICAL: Clear trace_id to ensure fresh generation for next request
292
+ # Without this, get_or_set_sf_trace_id() reuses trace_id from previous request
293
+ # causing X-Sf4-Prid to stay constant when no incoming X-Sf3-Rid header
294
+ clear_trace_id()
295
+
296
+ # CRITICAL: Clear current request path to prevent stale data in thread pools
297
+ clear_current_request_path()
298
+
299
+ # Clear function span override for this request (ContextVar cleanup - also syncs C thread-local)
300
+ try:
301
+ clear_funcspan_override()
302
+ except Exception:
303
+ pass
304
+
305
+ return response
306
+
307
+
308
+ # ────────────────────────────────────────────────────────────────
309
+ # Monkey-patch Flask & Blueprint
310
+ # ────────────────────────────────────────────────────────────────
311
+ try:
312
+ import flask
313
+ from flask import Blueprint
314
+
315
+ def _patch_add_url_rule(cls):
316
+ """
317
+ Patch *cls*.add_url_rule (cls is Flask or Blueprint) so the final
318
+ stored view function is wrapped after Flask registers it. Works for:
319
+ • view_func positional
320
+ • endpoint string lookup
321
+ • CBV's as_view()
322
+ """
323
+ original_add = cls.add_url_rule
324
+
325
+ def patched_add(
326
+ self, rule, endpoint=None, view_func=None, **options
327
+ ): # noqa: ANN001
328
+ # Resolve endpoint name
329
+ ep_name = endpoint or (view_func and view_func.__name__)
330
+
331
+ # Check if endpoint already registered
332
+ already_registered = ep_name and ep_name in self.view_functions
333
+
334
+ # If already registered, use existing function to avoid Flask's
335
+ # "overwriting endpoint" assertion when same function is used for multiple routes
336
+ if already_registered:
337
+ view_func = self.view_functions[ep_name]
338
+
339
+ # 1. Let Flask register the route
340
+ original_add(self, rule, endpoint=endpoint, view_func=view_func, **options)
341
+
342
+ # 2. Wrap only if this is a new endpoint registration
343
+ if already_registered:
344
+ return # Already wrapped during first registration
345
+
346
+ # This is a new endpoint, wrap it
347
+ if not ep_name:
348
+ return
349
+
350
+ target = self.view_functions.get(ep_name)
351
+ if not callable(target):
352
+ return
353
+
354
+ # 3. Wrap if user code (pass route pattern)
355
+ wrapped = _wrap_if_user_view(target, route=rule)
356
+ self.view_functions[ep_name] = wrapped
357
+
358
+ cls.add_url_rule = patched_add
359
+
360
+ def patch_flask(routes_to_skip: Optional[List[str]] = None):
361
+ """
362
+ OTEL-STYLE PURE ASYNC:
363
+ • Installs before_request header propagation
364
+ • Installs after_request for OTEL-style network hop emission
365
+ • Wraps every endpoint to capture metadata (not emit)
366
+ • Patches exception handlers for custom_excepthook
367
+ """
368
+ global _ROUTES_TO_SKIP
369
+ _ROUTES_TO_SKIP = routes_to_skip or []
370
+
371
+ if getattr(flask.Flask, "__sf_tracing_patched__", False):
372
+ return # idempotent
373
+
374
+ # --- 1. Patch Flask.__init__ to add request hooks -----------
375
+ original_flask_init = flask.Flask.__init__
376
+
377
+ def patched_init(self, *args, **kwargs):
378
+ original_flask_init(self, *args, **kwargs)
379
+
380
+ # Note: Profiler is already installed by unified_interceptor.py
381
+
382
+ _install_request_hooks(self)
383
+
384
+ flask.Flask.__init__ = patched_init
385
+
386
+ # --- 2. Patch add_url_rule for both Flask and Blueprint -----------------
387
+ _patch_add_url_rule(flask.Flask)
388
+ _patch_add_url_rule(Blueprint)
389
+
390
+ # --- 3. Patch exception handlers once on the class ----------------------
391
+ _mw_path = "sf_veritas_exception_patch_applied"
392
+ if not getattr(flask.Flask, _mw_path, False):
393
+ orig_handle_exc = flask.Flask.handle_exception
394
+ orig_handle_user_exc = flask.Flask.handle_user_exception
395
+
396
+ def _patched_handle_exception(self, e):
397
+ custom_excepthook(type(e), e, e.__traceback__)
398
+ return orig_handle_exc(self, e)
399
+
400
+ def _patched_handle_user_exception(self, e):
401
+ custom_excepthook(type(e), e, e.__traceback__)
402
+ return orig_handle_user_exc(self, e)
403
+
404
+ flask.Flask.handle_exception = _patched_handle_exception # 500 errors
405
+ flask.Flask.handle_user_exception = (
406
+ _patched_handle_user_exception # HTTPExc.
407
+ )
408
+
409
+ setattr(flask.Flask, _mw_path, True)
410
+
411
+ flask.Flask.__sf_tracing_patched__ = True
412
+
413
+ if SF_DEBUG and app_config._interceptors_initialized:
414
+ print(
415
+ "[[patch_flask]] tracing hooks + exception capture installed", log=False
416
+ )
417
+
418
+ def patch_flask_cors():
419
+ """
420
+ Patch flask-cors to automatically inject Sailfish headers into CORS allow_headers.
421
+
422
+ Patches:
423
+ 1. CORS class __init__ method to intercept allow_headers parameter
424
+ 2. CORS class init_app method for factory pattern support
425
+ 3. cross_origin decorator for route-specific CORS
426
+
427
+ SAFE: Only modifies CORS if flask-cors is installed and used.
428
+ Reference: https://corydolphin.com/flask-cors/extension/
429
+ """
430
+ # Import guard: only patch if flask-cors is installed
431
+ try:
432
+ from flask_cors import CORS, cross_origin
433
+ except ImportError:
434
+ # flask-cors not installed, skip patching
435
+ if SF_DEBUG and app_config._interceptors_initialized:
436
+ print(
437
+ "[[patch_flask_cors]] flask-cors not installed, skipping", log=False
438
+ )
439
+ return
440
+
441
+ # Check if already patched to avoid double-patching
442
+ if hasattr(CORS, "_sf_cors_patched"):
443
+ if SF_DEBUG and app_config._interceptors_initialized:
444
+ print("[[patch_flask_cors]] Already patched, skipping", log=False)
445
+ return
446
+
447
+ # Save original methods
448
+ original_cors_init = CORS.__init__
449
+ original_init_app = getattr(CORS, 'init_app', None)
450
+
451
+ # Patch CORS.__init__ (handles: CORS(app, allow_headers=["foo"]))
452
+ def patched_cors_init(self, app=None, **kwargs):
453
+ # Intercept and inject Sailfish headers into allow_headers
454
+ if "allow_headers" in kwargs:
455
+ original_headers = kwargs["allow_headers"]
456
+ if should_inject_headers(original_headers):
457
+ injected_headers = inject_sailfish_headers(original_headers)
458
+ kwargs["allow_headers"] = injected_headers
459
+ if SF_DEBUG and app_config._interceptors_initialized:
460
+ print(
461
+ f"[[patch_flask_cors]] Injected Sailfish headers into CORS.__init__: {injected_headers}",
462
+ log=False,
463
+ )
464
+
465
+ # Call original init
466
+ original_cors_init(self, app, **kwargs)
467
+
468
+ # Patch CORS.init_app (handles factory pattern: cors = CORS(); cors.init_app(app, allow_headers=["foo"]))
469
+ def patched_init_app(self, app, **kwargs):
470
+ # Intercept and inject Sailfish headers into allow_headers
471
+ if "allow_headers" in kwargs:
472
+ original_headers = kwargs["allow_headers"]
473
+ if should_inject_headers(original_headers):
474
+ injected_headers = inject_sailfish_headers(original_headers)
475
+ kwargs["allow_headers"] = injected_headers
476
+ if SF_DEBUG and app_config._interceptors_initialized:
477
+ print(
478
+ f"[[patch_flask_cors]] Injected Sailfish headers into CORS.init_app: {injected_headers}",
479
+ log=False,
480
+ )
481
+
482
+ # Call original init_app
483
+ if original_init_app:
484
+ original_init_app(self, app, **kwargs)
485
+
486
+ # Apply patches to CORS class
487
+ CORS.__init__ = patched_cors_init
488
+ if original_init_app:
489
+ CORS.init_app = patched_init_app
490
+ CORS._sf_cors_patched = True
491
+
492
+ # Patch cross_origin decorator (handles: @cross_origin(allow_headers=["foo"]))
493
+ original_cross_origin = cross_origin
494
+
495
+ def patched_cross_origin(*args, **kwargs):
496
+ # Intercept and inject Sailfish headers into allow_headers
497
+ if "allow_headers" in kwargs:
498
+ original_headers = kwargs["allow_headers"]
499
+ if should_inject_headers(original_headers):
500
+ injected_headers = inject_sailfish_headers(original_headers)
501
+ kwargs["allow_headers"] = injected_headers
502
+ if SF_DEBUG and app_config._interceptors_initialized:
503
+ print(
504
+ f"[[patch_flask_cors]] Injected Sailfish headers into @cross_origin: {injected_headers}",
505
+ log=False,
506
+ )
507
+
508
+ # Call original decorator
509
+ return original_cross_origin(*args, **kwargs)
510
+
511
+ # Replace the cross_origin function in the flask_cors module
512
+ import flask_cors
513
+
514
+ flask_cors.cross_origin = patched_cross_origin
515
+
516
+ if SF_DEBUG and app_config._interceptors_initialized:
517
+ print("[[patch_flask_cors]] Successfully patched flask-cors (CORS, init_app, cross_origin)", log=False)
518
+
519
+ # Apply CORS patching when module is loaded
520
+ patch_flask_cors()
521
+
522
+ except ImportError: # Flask not installed
523
+
524
+ def patch_flask(routes_to_skip: Optional[List[str]] = None): # noqa: D401
525
+ """No-op when Flask is absent."""
526
+ return