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