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