sf-veritas 0.10.3__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.

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-314-x86_64-linux-gnu.so +0 -0
  4. sf_veritas/_sffastnet.c +924 -0
  5. sf_veritas/_sffastnet.cpython-314-x86_64-linux-gnu.so +0 -0
  6. sf_veritas/_sffastnetworkrequest.c +730 -0
  7. sf_veritas/_sffastnetworkrequest.cpython-314-x86_64-linux-gnu.so +0 -0
  8. sf_veritas/_sffuncspan.c +2155 -0
  9. sf_veritas/_sffuncspan.cpython-314-x86_64-linux-gnu.so +0 -0
  10. sf_veritas/_sffuncspan_config.c +617 -0
  11. sf_veritas/_sffuncspan_config.cpython-314-x86_64-linux-gnu.so +0 -0
  12. sf_veritas/_sfheadercheck.c +341 -0
  13. sf_veritas/_sfheadercheck.cpython-314-x86_64-linux-gnu.so +0 -0
  14. sf_veritas/_sfnetworkhop.c +1451 -0
  15. sf_veritas/_sfnetworkhop.cpython-314-x86_64-linux-gnu.so +0 -0
  16. sf_veritas/_sfservice.c +1175 -0
  17. sf_veritas/_sfservice.cpython-314-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,395 @@
1
+ from functools import wraps
2
+ from typing import Callable, List, Optional, Set, Tuple
3
+
4
+ from ... import _sffuncspan_config, app_config
5
+ from ...constants import (
6
+ FUNCSPAN_OVERRIDE_HEADER_BYTES,
7
+ SAILFISH_TRACING_HEADER,
8
+ SAILFISH_TRACING_HEADER_BYTES,
9
+ )
10
+ from ...env_vars import (
11
+ SF_DEBUG,
12
+ SF_NETWORKHOP_CAPTURE_REQUEST_BODY,
13
+ SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS,
14
+ SF_NETWORKHOP_CAPTURE_RESPONSE_BODY,
15
+ SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS,
16
+ SF_NETWORKHOP_REQUEST_LIMIT_MB,
17
+ SF_NETWORKHOP_RESPONSE_LIMIT_MB,
18
+ )
19
+ from ...fast_network_hop import fast_send_network_hop_fast, register_endpoint
20
+ from ...thread_local import (
21
+ clear_c_tls_parent_trace_id,
22
+ clear_current_request_path,
23
+ clear_outbound_header_base,
24
+ clear_trace_id,
25
+ generate_new_trace_id,
26
+ get_or_set_sf_trace_id,
27
+ get_sf_trace_id,
28
+ set_current_request_path,
29
+ set_outbound_header_base,
30
+ )
31
+ from .cors_utils import inject_sailfish_headers, should_inject_headers
32
+ from .utils import _is_user_code, _unwrap_user_func, should_skip_route # shared helpers
33
+
34
+ # Size limits in bytes
35
+ _REQUEST_LIMIT_BYTES = SF_NETWORKHOP_REQUEST_LIMIT_MB * 1024 * 1024
36
+ _RESPONSE_LIMIT_BYTES = SF_NETWORKHOP_RESPONSE_LIMIT_MB * 1024 * 1024
37
+
38
+ # Pre-registered endpoint IDs
39
+ _ENDPOINT_REGISTRY: dict[tuple, int] = {}
40
+
41
+ # Routes to skip (set by patch_eve)
42
+ _ROUTES_TO_SKIP = []
43
+
44
+
45
+ # ──────────────────────────────────────────────────────────────
46
+ # OTEL-STYLE: Request hooks (before + after)
47
+ # ──────────────────────────────────────────────────────────────
48
+ def _install_request_hooks(app):
49
+ from flask import g, request
50
+
51
+ @app.before_request
52
+ def _extract_sf_header():
53
+ """OTEL-STYLE: Extract trace header and capture request data before handler."""
54
+ # Set current request path for route-based suppression (SF_DISABLE_INBOUND_NETWORK_TRACING_ON_ROUTES)
55
+ set_current_request_path(request.path)
56
+
57
+ # CRITICAL: Seed/ensure trace_id immediately (BEFORE any outbound work)
58
+ rid = request.headers.get(SAILFISH_TRACING_HEADER)
59
+ if rid:
60
+ # Incoming X-Sf3-Rid header provided - use it
61
+ get_or_set_sf_trace_id(rid, is_associated_with_inbound_request=True)
62
+ else:
63
+ # No incoming X-Sf3-Rid header - generate fresh trace_id for this request
64
+ generate_new_trace_id()
65
+
66
+ # Check for function span capture override header (highest priority!)
67
+ funcspan_override_header = request.headers.get(
68
+ "X-Sf3-FunctionSpanCaptureOverride"
69
+ )
70
+ if funcspan_override_header:
71
+ try:
72
+ _sffuncspan_config.set_thread_override(funcspan_override_header)
73
+ if SF_DEBUG and app_config._interceptors_initialized:
74
+ print(
75
+ f"[[Eve.before_request]] Set function span override from header: {funcspan_override_header}",
76
+ log=False,
77
+ )
78
+ except Exception as e:
79
+ if SF_DEBUG and app_config._interceptors_initialized:
80
+ print(
81
+ f"[[Eve.before_request]] Failed to set function span override: {e}",
82
+ log=False,
83
+ )
84
+
85
+ # Initialize outbound base without list/allocs from split()
86
+ try:
87
+ trace_id = get_sf_trace_id()
88
+ if trace_id:
89
+ s = str(trace_id)
90
+ i = s.find("/") # session
91
+ j = s.find("/", i + 1) if i != -1 else -1 # page
92
+ if j != -1:
93
+ base_trace = s[:j] # "session/page"
94
+ set_outbound_header_base(
95
+ base_trace=base_trace,
96
+ parent_trace_id=s, # "session/page/uuid"
97
+ funcspan=funcspan_override_header,
98
+ )
99
+ if SF_DEBUG and app_config._interceptors_initialized:
100
+ print(
101
+ f"[[Eve.before_request]] Initialized outbound header base (base={base_trace[:16]}...)",
102
+ log=False,
103
+ )
104
+ except Exception as e:
105
+ if SF_DEBUG and app_config._interceptors_initialized:
106
+ print(
107
+ f"[[Eve.before_request]] Failed to initialize outbound header base: {e}",
108
+ log=False,
109
+ )
110
+
111
+ # Capture request headers if enabled
112
+ if SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS:
113
+ try:
114
+ req_headers = dict(request.headers)
115
+ g._sf_request_headers = req_headers
116
+ if SF_DEBUG and app_config._interceptors_initialized:
117
+ print(
118
+ f"[[Eve]] Captured request headers: {len(req_headers)} headers",
119
+ log=False,
120
+ )
121
+ except Exception as e:
122
+ if SF_DEBUG and app_config._interceptors_initialized:
123
+ print(f"[[Eve]] Failed to capture request headers: {e}", log=False)
124
+
125
+ # Capture request body if enabled
126
+ if SF_NETWORKHOP_CAPTURE_REQUEST_BODY:
127
+ try:
128
+ # Flask/Eve: request.data gives raw bytes
129
+ body = request.get_data(cache=True) # cache=True to not consume it
130
+ if body:
131
+ req_body = body[:_REQUEST_LIMIT_BYTES]
132
+ g._sf_request_body = req_body
133
+ if SF_DEBUG and app_config._interceptors_initialized:
134
+ print(
135
+ f"[[Eve]] Request body capture: {len(req_body)} bytes",
136
+ log=False,
137
+ )
138
+ except Exception as e:
139
+ if SF_DEBUG and app_config._interceptors_initialized:
140
+ print(f"[[Eve]] Failed to capture request body: {e}", log=False)
141
+
142
+ @app.after_request
143
+ def _emit_network_hop(response):
144
+ """
145
+ OTEL-STYLE: Emit network hop AFTER response is built.
146
+ Eve is Flask-based, so we use the same @after_request pattern.
147
+ """
148
+ endpoint_id = getattr(g, "_sf_endpoint_id", None)
149
+ if endpoint_id is not None and endpoint_id >= 0:
150
+ try:
151
+ _, session_id = get_or_set_sf_trace_id()
152
+
153
+ # Get captured request data
154
+ req_headers = getattr(g, "_sf_request_headers", None)
155
+ req_body = getattr(g, "_sf_request_body", None)
156
+
157
+ # Capture response headers if enabled
158
+ resp_headers = None
159
+ if SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS:
160
+ try:
161
+ resp_headers = dict(response.headers)
162
+ if SF_DEBUG and app_config._interceptors_initialized:
163
+ print(
164
+ f"[[Eve]] Captured response headers: {len(resp_headers)} headers",
165
+ log=False,
166
+ )
167
+ except Exception as e:
168
+ if SF_DEBUG and app_config._interceptors_initialized:
169
+ print(
170
+ f"[[Eve]] Failed to capture response headers: {e}",
171
+ log=False,
172
+ )
173
+
174
+ # Capture response body if enabled
175
+ resp_body = None
176
+ if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY:
177
+ try:
178
+ # Flask/Eve response: get_data() returns bytes
179
+ body = response.get_data()
180
+ if body:
181
+ resp_body = body[:_RESPONSE_LIMIT_BYTES]
182
+ if SF_DEBUG and app_config._interceptors_initialized:
183
+ print(
184
+ f"[[Eve]] Captured response body: {len(resp_body)} bytes",
185
+ log=False,
186
+ )
187
+ except Exception as e:
188
+ if SF_DEBUG and app_config._interceptors_initialized:
189
+ print(
190
+ f"[[Eve]] Failed to capture response body: {e}",
191
+ log=False,
192
+ )
193
+
194
+ # Extract raw path and query string for C to parse
195
+ raw_path = request.path # e.g., "/log"
196
+ raw_query = request.query_string # Already bytes (e.g., b"foo=5")
197
+
198
+ if SF_DEBUG and app_config._interceptors_initialized:
199
+ print(
200
+ f"[[Eve]] About to emit network hop: endpoint_id={endpoint_id}, "
201
+ f"req_headers={'present' if req_headers else 'None'}, "
202
+ f"req_body={len(req_body) if req_body else 0} bytes, "
203
+ f"resp_headers={'present' if resp_headers else 'None'}, "
204
+ f"resp_body={len(resp_body) if resp_body else 0} bytes",
205
+ log=False,
206
+ )
207
+
208
+ # Direct C call - queues to background worker, returns instantly
209
+ # C will parse route and query_params from raw data
210
+ fast_send_network_hop_fast(
211
+ session_id=session_id,
212
+ endpoint_id=endpoint_id,
213
+ raw_path=raw_path,
214
+ raw_query_string=raw_query,
215
+ request_headers=req_headers,
216
+ request_body=req_body,
217
+ response_headers=resp_headers,
218
+ response_body=resp_body,
219
+ )
220
+
221
+ if SF_DEBUG and app_config._interceptors_initialized:
222
+ print(
223
+ f"[[Eve]] Emitted network hop: endpoint_id={endpoint_id} "
224
+ f"session={session_id}",
225
+ log=False,
226
+ )
227
+ except Exception as e: # noqa: BLE001 S110
228
+ if SF_DEBUG and app_config._interceptors_initialized:
229
+ print(f"[[Eve]] Failed to emit network hop: {e}", log=False)
230
+
231
+ # Clear function span override for this request (thread-local cleanup)
232
+ try:
233
+ _sffuncspan_config.clear_thread_override()
234
+ except Exception:
235
+ pass
236
+
237
+ # CRITICAL: Clear C TLS to prevent stale data in thread pools
238
+ clear_c_tls_parent_trace_id()
239
+
240
+ # CRITICAL: Clear outbound header base to prevent stale cached headers
241
+ # ContextVar does NOT automatically clean up in thread pools - must clear explicitly
242
+ clear_outbound_header_base()
243
+
244
+ # CRITICAL: Clear trace_id to ensure fresh generation for next request
245
+ # Without this, get_or_set_sf_trace_id() reuses trace_id from previous request
246
+ # causing X-Sf4-Prid to stay constant when no incoming X-Sf3-Rid header
247
+ clear_trace_id()
248
+
249
+ # CRITICAL: Clear current request path to prevent stale data in thread pools
250
+ clear_current_request_path()
251
+
252
+ return response
253
+
254
+
255
+ # ──────────────────────────────────────────────────────────────
256
+ # OTEL-STYLE: Per-view endpoint metadata capture
257
+ # ──────────────────────────────────────────────────────────────
258
+ def _hop_wrapper(view_fn: Callable):
259
+ """
260
+ OTEL-STYLE: Pre-register endpoint and store endpoint_id in flask.g.
261
+ Emission happens in @after_request hook with captured body/headers.
262
+ """
263
+ from flask import g
264
+
265
+ real_fn = _unwrap_user_func(view_fn)
266
+
267
+ # Skip Strawberry handlers – handled by Strawberry extension
268
+ if real_fn.__module__.startswith("strawberry"):
269
+ return view_fn
270
+
271
+ code = getattr(real_fn, "__code__", None)
272
+ if not code or not _is_user_code(code.co_filename):
273
+ return view_fn
274
+
275
+ hop_key = (code.co_filename, code.co_firstlineno)
276
+ fn_name = real_fn.__name__
277
+ filename = code.co_filename
278
+ line_no = code.co_firstlineno
279
+
280
+ # We'll check route at runtime in the wrapper since we need request context
281
+ # Pre-register endpoint if user code
282
+ endpoint_id = _ENDPOINT_REGISTRY.get(hop_key)
283
+ if endpoint_id is None:
284
+ # Note: route will be None here, will be passed at runtime
285
+ endpoint_id = register_endpoint(
286
+ line=str(line_no), column="0", name=fn_name, entrypoint=filename, route=None
287
+ )
288
+ if endpoint_id >= 0:
289
+ _ENDPOINT_REGISTRY[hop_key] = endpoint_id
290
+ if SF_DEBUG and app_config._interceptors_initialized:
291
+ print(
292
+ f"[[Eve]] Registered endpoint: {fn_name} @ {filename}:{line_no} (id={endpoint_id})",
293
+ log=False,
294
+ )
295
+
296
+ @wraps(view_fn)
297
+ def _wrapped(*args, **kwargs):
298
+ from flask import request
299
+
300
+ # Check if route should be skipped
301
+ route_pattern = request.path
302
+ if route_pattern and should_skip_route(route_pattern, _ROUTES_TO_SKIP):
303
+ if SF_DEBUG and app_config._interceptors_initialized:
304
+ print(
305
+ f"[[Eve]] Skipping endpoint (route matches skip pattern): {route_pattern}",
306
+ log=False,
307
+ )
308
+ return view_fn(*args, **kwargs)
309
+
310
+ # OTEL-STYLE: Store endpoint_id for after_request to emit
311
+ if not hasattr(g, "_sf_endpoint_id"):
312
+ g._sf_endpoint_id = endpoint_id
313
+
314
+ if SF_DEBUG and app_config._interceptors_initialized:
315
+ print(
316
+ f"[[Eve]] Captured endpoint: {fn_name} ({filename}:{line_no}) endpoint_id={endpoint_id}",
317
+ log=False,
318
+ )
319
+
320
+ return view_fn(*args, **kwargs)
321
+
322
+ return _wrapped
323
+
324
+
325
+ def _patch_add_url_rule(cls):
326
+ """
327
+ Patch add_url_rule on *cls* (cls is Eve or Blueprint) so that the final
328
+ stored endpoint function is wrapped *after* Flask has done its own
329
+ bookkeeping. This catches:
330
+ • Eve resource endpoints created internally via register_resource()
331
+ • Manual @app.route() decorators
332
+ • Blueprints, CBVs, etc.
333
+ """
334
+ original_add = cls.add_url_rule
335
+
336
+ def patched_add(
337
+ self, rule, endpoint=None, view_func=None, **options
338
+ ): # noqa: ANN001
339
+ # let Eve/Flask register the route first
340
+ original_add(self, rule, endpoint=endpoint, view_func=view_func, **options)
341
+
342
+ ep = endpoint or (view_func and view_func.__name__)
343
+ if not ep: # defensive
344
+ return
345
+
346
+ target = self.view_functions.get(ep)
347
+ if callable(target):
348
+ self.view_functions[ep] = _hop_wrapper(target)
349
+
350
+ cls.add_url_rule = patched_add
351
+
352
+
353
+ # ──────────────────────────────────────────────────────────────
354
+ # Public entry-point
355
+ # ──────────────────────────────────────────────────────────────
356
+ def patch_eve(routes_to_skip: Optional[List[str]] = None):
357
+ """
358
+ • Adds ContextVar propagation middleware
359
+ • Wraps every Eve endpoint (and Blueprint endpoints) to emit one hop
360
+ """
361
+ global _ROUTES_TO_SKIP
362
+ _ROUTES_TO_SKIP = routes_to_skip or []
363
+
364
+ try:
365
+ import eve
366
+ from flask import Blueprint # Eve relies on Flask blueprints
367
+ except ImportError:
368
+ return
369
+
370
+ # Guard against double-patching
371
+ if getattr(eve.Eve, "__sf_tracing_patched__", False):
372
+ return
373
+
374
+ # 1. Patch Eve.add_url_rule *and* Blueprint.add_url_rule
375
+ _patch_add_url_rule(eve.Eve)
376
+ _patch_add_url_rule(Blueprint)
377
+
378
+ # 2. Patch Eve.__init__ to install request hooks
379
+ # Note: CORS patching is handled by patch_flask_cors() since Eve uses flask-cors
380
+ original_init = eve.Eve.__init__
381
+
382
+ def patched_init(self, import_name=None, settings=None, *args, **kwargs):
383
+ # Call original init
384
+ original_init(self, import_name, settings, *args, **kwargs)
385
+
386
+ # Install request hooks
387
+ _install_request_hooks(self)
388
+
389
+ eve.Eve.__init__ = patched_init
390
+ eve.Eve.__sf_tracing_patched__ = True
391
+
392
+ if SF_DEBUG and app_config._interceptors_initialized:
393
+ print(
394
+ "[[patch_eve]] OTEL-style request hooks + hop wrapper installed (CORS patching handled by Flask)", log=False
395
+ )