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,931 @@
1
+ """
2
+ • SFTracingFalconMiddleware – propagates SAILFISH_TRACING_HEADER → ContextVar.
3
+ • per-responder wrapper – emits ONE NetworkHop per request for
4
+ user-land Falcon responders (sync & async), skipping Strawberry.
5
+ • patch_falcon() – monkey-patches both falcon.App (WSGI) and
6
+ falcon.asgi.App (ASGI) so the above logic is automatic.
7
+
8
+ This patch adds <1 µs overhead per request on CPython 3.11.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import functools
14
+ import inspect
15
+ import os
16
+ import threading
17
+ from types import MethodType
18
+ from typing import Any, Callable, List, Optional, Set, Tuple
19
+
20
+ from ... import _sffuncspan, _sffuncspan_config, app_config
21
+ from ...constants import (
22
+ FUNCSPAN_OVERRIDE_HEADER_BYTES,
23
+ SAILFISH_TRACING_HEADER,
24
+ SAILFISH_TRACING_HEADER_BYTES,
25
+ )
26
+ from ...custom_excepthook import custom_excepthook
27
+ from ...env_vars import (
28
+ SF_DEBUG,
29
+ SF_NETWORKHOP_CAPTURE_REQUEST_BODY,
30
+ SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS,
31
+ SF_NETWORKHOP_CAPTURE_RESPONSE_BODY,
32
+ SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS,
33
+ SF_NETWORKHOP_REQUEST_LIMIT_MB,
34
+ SF_NETWORKHOP_RESPONSE_LIMIT_MB,
35
+ )
36
+ from ...fast_network_hop import fast_send_network_hop_fast, register_endpoint
37
+ from ...thread_local import (
38
+ clear_c_tls_parent_trace_id,
39
+ clear_current_request_path,
40
+ clear_funcspan_override,
41
+ clear_outbound_header_base,
42
+ clear_trace_id,
43
+ generate_new_trace_id,
44
+ get_or_set_sf_trace_id,
45
+ get_sf_trace_id,
46
+ set_current_request_path,
47
+ set_funcspan_override,
48
+ set_outbound_header_base,
49
+ )
50
+ from .utils import _is_user_code, _unwrap_user_func, should_skip_route, reinitialize_log_print_capture_for_worker # shared helpers
51
+
52
+ # Size limits in bytes
53
+ _REQUEST_LIMIT_BYTES = SF_NETWORKHOP_REQUEST_LIMIT_MB * 1024 * 1024
54
+ _RESPONSE_LIMIT_BYTES = SF_NETWORKHOP_RESPONSE_LIMIT_MB * 1024 * 1024
55
+
56
+ # Pre-registered endpoint IDs
57
+ _ENDPOINT_REGISTRY: dict[tuple, int] = {}
58
+
59
+ # Module-level variable for routes to skip (set by patch_falcon)
60
+ _ROUTES_TO_SKIP = []
61
+
62
+ # Map resource instances to their route patterns
63
+ _RESOURCE_ROUTES: dict[int, str] = {}
64
+
65
+ # JSON serialization - try fast orjson first, fallback to stdlib json
66
+ try:
67
+ import orjson
68
+
69
+ HAS_ORJSON = True
70
+ except ImportError:
71
+ import json
72
+
73
+ HAS_ORJSON = False
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # 1 | Context-propagation middleware
77
+ # ---------------------------------------------------------------------------
78
+
79
+
80
+ class SFTracingFalconMiddleware:
81
+ """Works for BOTH WSGI and ASGI flavours of Falcon."""
82
+
83
+ # synchronous apps
84
+ def process_request(self, req, resp): # noqa: D401
85
+ # Set current request path for route-based suppression (SF_DISABLE_INBOUND_NETWORK_TRACING_ON_ROUTES)
86
+ set_current_request_path(req.path)
87
+
88
+ # PERFORMANCE: Single-pass bytes-level header scan (no dict allocation until needed)
89
+ # Scan headers once on bytes, only decode what we need, use latin-1 (fast 1:1 byte map)
90
+ incoming_trace_raw = None # bytes
91
+ funcspan_raw = None # bytes
92
+ req_headers = None # dict[str,str] only if capture enabled
93
+
94
+ capture_req_headers = SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS # local cache
95
+
96
+ # Falcon headers are accessible as req.headers (dict-like object)
97
+ # We need to iterate through the raw headers if available
98
+ try:
99
+ # Try to access raw headers if available (_headers is internal dict in Falcon)
100
+ if hasattr(req, "_headers") and req._headers:
101
+ # Falcon WSGI stores headers internally
102
+ hdr_items = req._headers.items()
103
+ if capture_req_headers:
104
+ tmp = {}
105
+ for k, v in hdr_items:
106
+ kl = k.lower() if isinstance(k, str) else k
107
+ kb = kl.encode("latin-1") if isinstance(kl, str) else kl
108
+ vb = v.encode("latin-1") if isinstance(v, str) else v
109
+ if kb == SAILFISH_TRACING_HEADER_BYTES:
110
+ incoming_trace_raw = vb
111
+ elif kb == FUNCSPAN_OVERRIDE_HEADER_BYTES:
112
+ funcspan_raw = vb
113
+ tmp[k] = v
114
+ req_headers = tmp
115
+ else:
116
+ for k, v in hdr_items:
117
+ kl = k.lower() if isinstance(k, str) else k
118
+ kb = kl.encode("latin-1") if isinstance(kl, str) else kl
119
+ vb = v.encode("latin-1") if isinstance(v, str) else v
120
+ if kb == SAILFISH_TRACING_HEADER_BYTES:
121
+ incoming_trace_raw = vb
122
+ elif kb == FUNCSPAN_OVERRIDE_HEADER_BYTES:
123
+ funcspan_raw = vb
124
+ else:
125
+ # Fallback: use req.get_header (slower but safer)
126
+ incoming_trace_raw = req.get_header(SAILFISH_TRACING_HEADER)
127
+ if incoming_trace_raw and isinstance(incoming_trace_raw, str):
128
+ incoming_trace_raw = incoming_trace_raw.encode("latin-1")
129
+ funcspan_raw = req.get_header("X-Sf3-FunctionSpanCaptureOverride")
130
+ if funcspan_raw and isinstance(funcspan_raw, str):
131
+ funcspan_raw = funcspan_raw.encode("latin-1")
132
+ if capture_req_headers:
133
+ req_headers = dict(req.headers)
134
+ except Exception as e:
135
+ if SF_DEBUG and app_config._interceptors_initialized:
136
+ print(f"[[Falcon.process_request]] Header scan failed: {e}", log=False)
137
+ # Fallback to simple approach
138
+ incoming_trace_raw = req.get_header(SAILFISH_TRACING_HEADER)
139
+ if incoming_trace_raw and isinstance(incoming_trace_raw, str):
140
+ incoming_trace_raw = incoming_trace_raw.encode("latin-1")
141
+ funcspan_raw = req.get_header("X-Sf3-FunctionSpanCaptureOverride")
142
+ if funcspan_raw and isinstance(funcspan_raw, str):
143
+ funcspan_raw = funcspan_raw.encode("latin-1")
144
+ if capture_req_headers:
145
+ req_headers = dict(req.headers)
146
+
147
+ # Store captured headers for later emission
148
+ req.context._sf_request_headers = req_headers
149
+
150
+ # CRITICAL: Seed/ensure trace_id immediately (BEFORE any outbound work)
151
+ if incoming_trace_raw:
152
+ # Incoming X-Sf3-Rid header provided - use it
153
+ incoming_trace = (
154
+ incoming_trace_raw.decode("latin-1")
155
+ if isinstance(incoming_trace_raw, bytes)
156
+ else incoming_trace_raw
157
+ )
158
+ get_or_set_sf_trace_id(
159
+ incoming_trace, is_associated_with_inbound_request=True
160
+ )
161
+ else:
162
+ # No incoming X-Sf3-Rid header - generate fresh trace_id for this request
163
+ generate_new_trace_id()
164
+
165
+ # Optional funcspan override (decode only if present)
166
+ funcspan_override_header = (
167
+ funcspan_raw.decode("latin-1")
168
+ if funcspan_raw and isinstance(funcspan_raw, bytes)
169
+ else funcspan_raw
170
+ )
171
+ if funcspan_override_header:
172
+ try:
173
+ set_funcspan_override(funcspan_override_header)
174
+ if SF_DEBUG and app_config._interceptors_initialized:
175
+ print(
176
+ f"[[Falcon.process_request]] Set function span override from header: {funcspan_override_header}",
177
+ log=False,
178
+ )
179
+ except Exception as e:
180
+ if SF_DEBUG and app_config._interceptors_initialized:
181
+ print(
182
+ f"[[Falcon.process_request]] Failed to set function span override: {e}",
183
+ log=False,
184
+ )
185
+
186
+ # Initialize outbound base without list/allocs from split()
187
+ try:
188
+ trace_id = get_sf_trace_id()
189
+ if trace_id:
190
+ s = str(trace_id)
191
+ i = s.find("/") # session
192
+ j = s.find("/", i + 1) if i != -1 else -1 # page
193
+ if j != -1:
194
+ base_trace = s[:j] # "session/page"
195
+ set_outbound_header_base(
196
+ base_trace=base_trace,
197
+ parent_trace_id=s, # "session/page/uuid"
198
+ funcspan=funcspan_override_header,
199
+ )
200
+ if SF_DEBUG and app_config._interceptors_initialized:
201
+ print(
202
+ f"[[Falcon.process_request]] Initialized outbound header base (base={base_trace[:16]}...)",
203
+ log=False,
204
+ )
205
+ except Exception as e:
206
+ if SF_DEBUG and app_config._interceptors_initialized:
207
+ print(
208
+ f"[[Falcon.process_request]] Failed to initialize outbound header base: {e}",
209
+ log=False,
210
+ )
211
+
212
+ # Capture request body if enabled
213
+ if SF_NETWORKHOP_CAPTURE_REQUEST_BODY:
214
+ try:
215
+ # Falcon provides bounded_stream that we can read
216
+ # For GET requests, this will typically be empty
217
+ body = req.bounded_stream.read(_REQUEST_LIMIT_BYTES)
218
+ req.context._sf_request_body = body if body else None
219
+ if SF_DEBUG and app_config._interceptors_initialized:
220
+ print(
221
+ f"[[Falcon]] Request body capture: {len(body) if body else 0} bytes (method={req.method})",
222
+ log=False,
223
+ )
224
+ except Exception as e:
225
+ if SF_DEBUG and app_config._interceptors_initialized:
226
+ print(f"[[Falcon]] Failed to capture request body: {e}", log=False)
227
+ req.context._sf_request_body = None
228
+ else:
229
+ req.context._sf_request_body = None
230
+
231
+ # asynchronous apps
232
+ async def process_request_async(self, req, resp): # noqa: D401
233
+ # Set current request path for route-based suppression (SF_DISABLE_INBOUND_NETWORK_TRACING_ON_ROUTES)
234
+ set_current_request_path(req.path)
235
+
236
+ # PERFORMANCE: Single-pass bytes-level header scan (no dict allocation until needed)
237
+ # Scan headers once on bytes, only decode what we need, use latin-1 (fast 1:1 byte map)
238
+ incoming_trace_raw = None # bytes
239
+ funcspan_raw = None # bytes
240
+ req_headers = None # dict[str,str] only if capture enabled
241
+
242
+ capture_req_headers = SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS # local cache
243
+
244
+ # Falcon headers are accessible as req.headers (dict-like object)
245
+ # We need to iterate through the raw headers if available
246
+ try:
247
+ # Try to access raw headers if available (_headers is internal dict in Falcon)
248
+ if hasattr(req, "_headers") and req._headers:
249
+ # Falcon ASGI stores headers internally
250
+ hdr_items = req._headers.items()
251
+ if capture_req_headers:
252
+ tmp = {}
253
+ for k, v in hdr_items:
254
+ kl = k.lower() if isinstance(k, str) else k
255
+ kb = kl.encode("latin-1") if isinstance(kl, str) else kl
256
+ vb = v.encode("latin-1") if isinstance(v, str) else v
257
+ if kb == SAILFISH_TRACING_HEADER_BYTES:
258
+ incoming_trace_raw = vb
259
+ elif kb == FUNCSPAN_OVERRIDE_HEADER_BYTES:
260
+ funcspan_raw = vb
261
+ tmp[k] = v
262
+ req_headers = tmp
263
+ else:
264
+ for k, v in hdr_items:
265
+ kl = k.lower() if isinstance(k, str) else k
266
+ kb = kl.encode("latin-1") if isinstance(kl, str) else kl
267
+ vb = v.encode("latin-1") if isinstance(v, str) else v
268
+ if kb == SAILFISH_TRACING_HEADER_BYTES:
269
+ incoming_trace_raw = vb
270
+ elif kb == FUNCSPAN_OVERRIDE_HEADER_BYTES:
271
+ funcspan_raw = vb
272
+ else:
273
+ # Fallback: use req.get_header (slower but safer)
274
+ incoming_trace_raw = req.get_header(SAILFISH_TRACING_HEADER)
275
+ if incoming_trace_raw and isinstance(incoming_trace_raw, str):
276
+ incoming_trace_raw = incoming_trace_raw.encode("latin-1")
277
+ funcspan_raw = req.get_header("X-Sf3-FunctionSpanCaptureOverride")
278
+ if funcspan_raw and isinstance(funcspan_raw, str):
279
+ funcspan_raw = funcspan_raw.encode("latin-1")
280
+ if capture_req_headers:
281
+ req_headers = dict(req.headers)
282
+ except Exception as e:
283
+ if SF_DEBUG and app_config._interceptors_initialized:
284
+ print(
285
+ f"[[Falcon.process_request_async]] Header scan failed: {e}",
286
+ log=False,
287
+ )
288
+ # Fallback to simple approach
289
+ incoming_trace_raw = req.get_header(SAILFISH_TRACING_HEADER)
290
+ if incoming_trace_raw and isinstance(incoming_trace_raw, str):
291
+ incoming_trace_raw = incoming_trace_raw.encode("latin-1")
292
+ funcspan_raw = req.get_header("X-Sf3-FunctionSpanCaptureOverride")
293
+ if funcspan_raw and isinstance(funcspan_raw, str):
294
+ funcspan_raw = funcspan_raw.encode("latin-1")
295
+ if capture_req_headers:
296
+ req_headers = dict(req.headers)
297
+
298
+ # Store captured headers for later emission
299
+ req.context._sf_request_headers = req_headers
300
+
301
+ # CRITICAL: Seed/ensure trace_id immediately (BEFORE any outbound work)
302
+ if incoming_trace_raw:
303
+ # Incoming X-Sf3-Rid header provided - use it
304
+ incoming_trace = (
305
+ incoming_trace_raw.decode("latin-1")
306
+ if isinstance(incoming_trace_raw, bytes)
307
+ else incoming_trace_raw
308
+ )
309
+ get_or_set_sf_trace_id(
310
+ incoming_trace, is_associated_with_inbound_request=True
311
+ )
312
+ else:
313
+ # No incoming X-Sf3-Rid header - generate fresh trace_id for this request
314
+ generate_new_trace_id()
315
+
316
+ # Optional funcspan override (decode only if present)
317
+ funcspan_override_header = (
318
+ funcspan_raw.decode("latin-1")
319
+ if funcspan_raw and isinstance(funcspan_raw, bytes)
320
+ else funcspan_raw
321
+ )
322
+ if funcspan_override_header:
323
+ try:
324
+ set_funcspan_override(funcspan_override_header)
325
+ if SF_DEBUG and app_config._interceptors_initialized:
326
+ print(
327
+ f"[[Falcon.process_request_async]] Set function span override from header: {funcspan_override_header}",
328
+ log=False,
329
+ )
330
+ except Exception as e:
331
+ if SF_DEBUG and app_config._interceptors_initialized:
332
+ print(
333
+ f"[[Falcon.process_request_async]] Failed to set function span override: {e}",
334
+ log=False,
335
+ )
336
+
337
+ # Initialize outbound base without list/allocs from split()
338
+ try:
339
+ trace_id = get_sf_trace_id()
340
+ if trace_id:
341
+ s = str(trace_id)
342
+ i = s.find("/") # session
343
+ j = s.find("/", i + 1) if i != -1 else -1 # page
344
+ if j != -1:
345
+ base_trace = s[:j] # "session/page"
346
+ set_outbound_header_base(
347
+ base_trace=base_trace,
348
+ parent_trace_id=s, # "session/page/uuid"
349
+ funcspan=funcspan_override_header,
350
+ )
351
+ if SF_DEBUG and app_config._interceptors_initialized:
352
+ print(
353
+ f"[[Falcon.process_request_async]] Initialized outbound header base (base={base_trace[:16]}...)",
354
+ log=False,
355
+ )
356
+ except Exception as e:
357
+ if SF_DEBUG and app_config._interceptors_initialized:
358
+ print(
359
+ f"[[Falcon.process_request_async]] Failed to initialize outbound header base: {e}",
360
+ log=False,
361
+ )
362
+
363
+ # Capture request body if enabled
364
+ if SF_NETWORKHOP_CAPTURE_REQUEST_BODY:
365
+ try:
366
+ # Falcon ASGI provides bounded_stream that we can read
367
+ # For GET requests, this will typically be empty
368
+ body = await req.bounded_stream.read(_REQUEST_LIMIT_BYTES)
369
+ req.context._sf_request_body = body if body else None
370
+ if SF_DEBUG and app_config._interceptors_initialized:
371
+ print(
372
+ f"[[Falcon]] Request body capture: {len(body) if body else 0} bytes (method={req.method})",
373
+ log=False,
374
+ )
375
+ except Exception as e:
376
+ if SF_DEBUG and app_config._interceptors_initialized:
377
+ print(f"[[Falcon]] Failed to capture request body: {e}", log=False)
378
+ req.context._sf_request_body = None
379
+ else:
380
+ req.context._sf_request_body = None
381
+
382
+ # OTEL-STYLE: Emit network hop AFTER response (sync version)
383
+ def process_response(self, req, resp, resource, req_succeeded):
384
+ """Emit network hop after response built (sync version). Captures headers/bodies if enabled."""
385
+ try:
386
+ endpoint_id = getattr(req.context, "_sf_endpoint_id", None)
387
+ if endpoint_id is not None and endpoint_id >= 0:
388
+ try:
389
+ _, session_id = get_or_set_sf_trace_id()
390
+
391
+ # Get captured request data
392
+ req_headers = getattr(req.context, "_sf_request_headers", None)
393
+ req_body = getattr(req.context, "_sf_request_body", None)
394
+
395
+ # Capture response headers if enabled
396
+ resp_headers = None
397
+ if SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS:
398
+ try:
399
+ # Debug: check what's available
400
+ if SF_DEBUG and app_config._interceptors_initialized:
401
+ attrs = [a for a in dir(resp) if not a.startswith("__")]
402
+ print(
403
+ f"[[Falcon]] Response object has: {attrs[:10]}...",
404
+ log=False,
405
+ )
406
+ if hasattr(resp, "_headers"):
407
+ print(
408
+ f"[[Falcon]] resp._headers = {resp._headers}",
409
+ log=False,
410
+ )
411
+ if hasattr(resp, "headers"):
412
+ print(
413
+ f"[[Falcon]] resp.headers type = {type(resp.headers)}",
414
+ log=False,
415
+ )
416
+
417
+ # Falcon response headers - try multiple approaches
418
+ if hasattr(resp, "_headers") and resp._headers:
419
+ resp_headers = dict(resp._headers)
420
+ if SF_DEBUG and app_config._interceptors_initialized:
421
+ print(
422
+ f"[[Falcon]] Captured from _headers: {resp_headers}",
423
+ log=False,
424
+ )
425
+ elif hasattr(resp, "headers"):
426
+ # resp.headers is a Headers object, iterate it
427
+ try:
428
+ resp_headers = {
429
+ k: v for k, v in resp.headers.items()
430
+ }
431
+ if (
432
+ SF_DEBUG
433
+ and app_config._interceptors_initialized
434
+ ):
435
+ print(
436
+ f"[[Falcon]] Captured from headers.items(): {resp_headers}",
437
+ log=False,
438
+ )
439
+ except Exception:
440
+ # Try converting to dict directly
441
+ resp_headers = dict(resp.headers)
442
+ if (
443
+ SF_DEBUG
444
+ and app_config._interceptors_initialized
445
+ ):
446
+ print(
447
+ f"[[Falcon]] Captured from dict(headers): {resp_headers}",
448
+ log=False,
449
+ )
450
+ except Exception as e:
451
+ if SF_DEBUG and app_config._interceptors_initialized:
452
+ print(
453
+ f"[[Falcon]] Failed to capture response headers: {e}",
454
+ log=False,
455
+ )
456
+
457
+ # Capture response body if enabled
458
+ resp_body = None
459
+ if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY:
460
+ try:
461
+ # Falcon serializes resp.media to JSON, or uses resp.text/resp.data
462
+ if hasattr(resp, "text") and resp.text:
463
+ resp_body = resp.text.encode("utf-8")[
464
+ :_RESPONSE_LIMIT_BYTES
465
+ ]
466
+ elif hasattr(resp, "data") and resp.data:
467
+ resp_body = resp.data[:_RESPONSE_LIMIT_BYTES]
468
+ elif hasattr(resp, "media") and resp.media is not None:
469
+ # Serialize media to JSON for capture
470
+ try:
471
+ if HAS_ORJSON:
472
+ media_json = orjson.dumps(
473
+ resp.media, separators=(",", ":")
474
+ )
475
+ else:
476
+ media_json = json.dumps(
477
+ resp.media, separators=(",", ":")
478
+ )
479
+ resp_body = media_json.encode("utf-8")[
480
+ :_RESPONSE_LIMIT_BYTES
481
+ ]
482
+ except (TypeError, ValueError):
483
+ pass
484
+ except Exception:
485
+ pass
486
+
487
+ # Extract raw path and query string for C to parse
488
+ raw_path = req.path # e.g., "/log"
489
+ raw_query = (
490
+ req.query_string.encode("utf-8") if req.query_string else b""
491
+ ) # e.g., b"foo=5"
492
+
493
+ if SF_DEBUG and app_config._interceptors_initialized:
494
+ print(
495
+ f"[[Falcon]] About to emit network hop: endpoint_id={endpoint_id}, "
496
+ f"req_headers={'present' if req_headers else 'None'}, "
497
+ f"req_body={len(req_body) if req_body else 0} bytes, "
498
+ f"resp_headers={'present' if resp_headers else 'None'}, "
499
+ f"resp_body={len(resp_body) if resp_body else 0} bytes",
500
+ log=False,
501
+ )
502
+
503
+ # Direct C call - queues to background worker, returns instantly
504
+ # C will parse route and query_params from raw data
505
+ fast_send_network_hop_fast(
506
+ session_id=session_id,
507
+ endpoint_id=endpoint_id,
508
+ raw_path=raw_path,
509
+ raw_query_string=raw_query,
510
+ request_headers=req_headers,
511
+ request_body=req_body,
512
+ response_headers=resp_headers,
513
+ response_body=resp_body,
514
+ )
515
+
516
+ if SF_DEBUG and app_config._interceptors_initialized:
517
+ print(
518
+ f"[[Falcon]] Emitted network hop: endpoint_id={endpoint_id} "
519
+ f"session={session_id}",
520
+ log=False,
521
+ )
522
+ except Exception as e: # noqa: BLE001 S110
523
+ if SF_DEBUG and app_config._interceptors_initialized:
524
+ print(f"[[Falcon]] Failed to emit network hop: {e}", log=False)
525
+ finally:
526
+ # CRITICAL: Clear C TLS to prevent stale data in thread pools
527
+ clear_c_tls_parent_trace_id()
528
+
529
+ # CRITICAL: Clear outbound header base to prevent stale cached headers
530
+ # ContextVar does NOT automatically clean up in thread pools - must clear explicitly
531
+ clear_outbound_header_base()
532
+
533
+ # CRITICAL: Clear trace_id to ensure fresh generation for next request
534
+ # Without this, get_or_set_sf_trace_id() reuses trace_id from previous request
535
+ # causing X-Sf4-Prid to stay constant when no incoming X-Sf3-Rid header
536
+ clear_trace_id()
537
+
538
+ # CRITICAL: Clear current request path to prevent stale data in thread pools
539
+ clear_current_request_path()
540
+
541
+ # Clear function span override for this request (ContextVar cleanup - also syncs C thread-local)
542
+ try:
543
+ clear_funcspan_override()
544
+ except Exception:
545
+ pass
546
+
547
+ async def process_response_async(self, req, resp, resource, req_succeeded):
548
+ """Emit network hop after response built (async version). Captures headers/bodies if enabled."""
549
+ try:
550
+ endpoint_id = getattr(req.context, "_sf_endpoint_id", None)
551
+ if endpoint_id is not None and endpoint_id >= 0:
552
+ try:
553
+ _, session_id = get_or_set_sf_trace_id()
554
+
555
+ # Get captured request data
556
+ req_headers = getattr(req.context, "_sf_request_headers", None)
557
+ req_body = getattr(req.context, "_sf_request_body", None)
558
+
559
+ # Capture response headers if enabled
560
+ resp_headers = None
561
+ if SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS:
562
+ try:
563
+ # Debug: check what's available
564
+ if SF_DEBUG and app_config._interceptors_initialized:
565
+ attrs = [a for a in dir(resp) if not a.startswith("__")]
566
+ print(
567
+ f"[[Falcon]] Response object has: {attrs[:10]}...",
568
+ log=False,
569
+ )
570
+ if hasattr(resp, "_headers"):
571
+ print(
572
+ f"[[Falcon]] resp._headers = {resp._headers}",
573
+ log=False,
574
+ )
575
+ if hasattr(resp, "headers"):
576
+ print(
577
+ f"[[Falcon]] resp.headers type = {type(resp.headers)}",
578
+ log=False,
579
+ )
580
+
581
+ # Falcon response headers - try multiple approaches
582
+ if hasattr(resp, "_headers") and resp._headers:
583
+ resp_headers = dict(resp._headers)
584
+ if SF_DEBUG and app_config._interceptors_initialized:
585
+ print(
586
+ f"[[Falcon]] Captured from _headers: {resp_headers}",
587
+ log=False,
588
+ )
589
+ elif hasattr(resp, "headers"):
590
+ # resp.headers is a Headers object, iterate it
591
+ try:
592
+ resp_headers = {
593
+ k: v for k, v in resp.headers.items()
594
+ }
595
+ if (
596
+ SF_DEBUG
597
+ and app_config._interceptors_initialized
598
+ ):
599
+ print(
600
+ f"[[Falcon]] Captured from headers.items(): {resp_headers}",
601
+ log=False,
602
+ )
603
+ except Exception:
604
+ # Try converting to dict directly
605
+ resp_headers = dict(resp.headers)
606
+ if (
607
+ SF_DEBUG
608
+ and app_config._interceptors_initialized
609
+ ):
610
+ print(
611
+ f"[[Falcon]] Captured from dict(headers): {resp_headers}",
612
+ log=False,
613
+ )
614
+ except Exception as e:
615
+ if SF_DEBUG and app_config._interceptors_initialized:
616
+ print(
617
+ f"[[Falcon]] Failed to capture response headers: {e}",
618
+ log=False,
619
+ )
620
+
621
+ # Capture response body if enabled
622
+ resp_body = None
623
+ if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY:
624
+ try:
625
+ # Falcon serializes resp.media to JSON, or uses resp.text/resp.data
626
+ if hasattr(resp, "text") and resp.text:
627
+ resp_body = resp.text.encode("utf-8")[
628
+ :_RESPONSE_LIMIT_BYTES
629
+ ]
630
+ elif hasattr(resp, "data") and resp.data:
631
+ resp_body = resp.data[:_RESPONSE_LIMIT_BYTES]
632
+ elif hasattr(resp, "media") and resp.media is not None:
633
+ # Serialize media to JSON for capture
634
+ try:
635
+ if HAS_ORJSON:
636
+ media_json = orjson.dumps(
637
+ resp.media, separators=(",", ":")
638
+ )
639
+ else:
640
+ media_json = json.dumps(
641
+ resp.media, separators=(",", ":")
642
+ )
643
+ resp_body = media_json.encode("utf-8")[
644
+ :_RESPONSE_LIMIT_BYTES
645
+ ]
646
+ except (TypeError, ValueError):
647
+ pass
648
+ except Exception:
649
+ pass
650
+
651
+ # Extract raw path and query string for C to parse
652
+ raw_path = req.path # e.g., "/log"
653
+ raw_query = (
654
+ req.query_string.encode("utf-8") if req.query_string else b""
655
+ ) # e.g., b"foo=5"
656
+
657
+ if SF_DEBUG and app_config._interceptors_initialized:
658
+ print(
659
+ f"[[Falcon]] About to emit network hop: endpoint_id={endpoint_id}, "
660
+ f"req_headers={'present' if req_headers else 'None'}, "
661
+ f"req_body={len(req_body) if req_body else 0} bytes, "
662
+ f"resp_headers={'present' if resp_headers else 'None'}, "
663
+ f"resp_body={len(resp_body) if resp_body else 0} bytes",
664
+ log=False,
665
+ )
666
+
667
+ # Direct C call - queues to background worker, returns instantly
668
+ # C will parse route and query_params from raw data
669
+ fast_send_network_hop_fast(
670
+ session_id=session_id,
671
+ endpoint_id=endpoint_id,
672
+ raw_path=raw_path,
673
+ raw_query_string=raw_query,
674
+ request_headers=req_headers,
675
+ request_body=req_body,
676
+ response_headers=resp_headers,
677
+ response_body=resp_body,
678
+ )
679
+
680
+ if SF_DEBUG and app_config._interceptors_initialized:
681
+ print(
682
+ f"[[Falcon]] Emitted network hop: endpoint_id={endpoint_id} "
683
+ f"session={session_id}",
684
+ log=False,
685
+ )
686
+ except Exception as e: # noqa: BLE001 S110
687
+ if SF_DEBUG and app_config._interceptors_initialized:
688
+ print(f"[[Falcon]] Failed to emit network hop: {e}", log=False)
689
+ finally:
690
+ # CRITICAL: Clear C TLS to prevent stale data in thread pools
691
+ clear_c_tls_parent_trace_id()
692
+
693
+ # CRITICAL: Clear outbound header base to prevent stale cached headers
694
+ # ContextVar does NOT automatically clean up in thread pools - must clear explicitly
695
+ clear_outbound_header_base()
696
+
697
+ # CRITICAL: Clear trace_id to ensure fresh generation for next request
698
+ # Without this, get_or_set_sf_trace_id() reuses trace_id from previous request
699
+ # causing X-Sf4-Prid to stay constant when no incoming X-Sf3-Rid header
700
+ clear_trace_id()
701
+
702
+ # CRITICAL: Clear current request path to prevent stale data in thread pools
703
+ clear_current_request_path()
704
+
705
+ # Clear function span override for this request (ContextVar cleanup - also syncs C thread-local)
706
+ try:
707
+ clear_funcspan_override()
708
+ except Exception:
709
+ pass
710
+
711
+
712
+ # ---------------------------------------------------------------------------
713
+ # 2 | Hop-emission helper
714
+ # ---------------------------------------------------------------------------
715
+
716
+
717
+ def _capture_endpoint_info(
718
+ req,
719
+ hop_key: Tuple[str, int],
720
+ fname: str,
721
+ lno: int,
722
+ responder_name: str,
723
+ route: str = None,
724
+ ) -> None:
725
+ """OTEL-STYLE: Capture endpoint metadata and register endpoint for later emission."""
726
+ # Check if route should be skipped
727
+ if should_skip_route(route, _ROUTES_TO_SKIP):
728
+ if SF_DEBUG and app_config._interceptors_initialized:
729
+ print(
730
+ f"[[Falcon]] Skipping endpoint (route matches skip pattern): {route}",
731
+ log=False,
732
+ )
733
+ req.context._sf_endpoint_id = -1 # Mark as skipped
734
+ return
735
+
736
+ # Get or register endpoint_id
737
+ endpoint_id = _ENDPOINT_REGISTRY.get(hop_key)
738
+
739
+ if endpoint_id is None:
740
+ endpoint_id = register_endpoint(
741
+ line=str(lno),
742
+ column="0",
743
+ name=responder_name,
744
+ entrypoint=fname,
745
+ route=route,
746
+ )
747
+ if endpoint_id >= 0:
748
+ _ENDPOINT_REGISTRY[hop_key] = endpoint_id
749
+ if SF_DEBUG and app_config._interceptors_initialized:
750
+ print(
751
+ f"[[Falcon]] Registered endpoint: {responder_name} @ {fname}:{lno} (id={endpoint_id})",
752
+ log=False,
753
+ )
754
+
755
+ # Store endpoint_id for process_response to emit
756
+ req.context._sf_endpoint_id = endpoint_id
757
+
758
+ if SF_DEBUG and app_config._interceptors_initialized:
759
+ print(
760
+ f"[[Falcon]] Captured endpoint: {responder_name} ({fname}:{lno}) endpoint_id={endpoint_id}",
761
+ log=False,
762
+ )
763
+
764
+
765
+ def _make_wrapper(base_fn: Callable, resource: Any) -> Callable:
766
+ """Return a hop-emitting, exception-capturing wrapper around *base_fn*."""
767
+
768
+ real_fn = _unwrap_user_func(base_fn)
769
+
770
+ # Ignore non-user and Strawberry handlers
771
+ if real_fn.__module__.startswith("strawberry") or not _is_user_code(
772
+ real_fn.__code__.co_filename
773
+ ):
774
+ return base_fn
775
+
776
+ fname = real_fn.__code__.co_filename
777
+ lno = real_fn.__code__.co_firstlineno
778
+ hop_key = (fname, lno)
779
+ responder_name = real_fn.__name__
780
+
781
+ # ---------------- asynchronous responders ------------------------- #
782
+ if inspect.iscoroutinefunction(base_fn):
783
+
784
+ async def _async_wrapped(self, req, resp, *args, **kwargs): # noqa: D401
785
+ # Get route pattern from resource mapping
786
+ route = _RESOURCE_ROUTES.get(id(self))
787
+ _capture_endpoint_info(
788
+ req, hop_key, fname, lno, responder_name, route=route
789
+ )
790
+ try:
791
+ return await base_fn(self, req, resp, *args, **kwargs)
792
+ except Exception as exc: # catches falcon.HTTPError too
793
+ custom_excepthook(type(exc), exc, exc.__traceback__)
794
+ raise
795
+
796
+ return _async_wrapped
797
+
798
+ # ---------------- synchronous responders -------------------------- #
799
+ def _sync_wrapped(self, req, resp, *args, **kwargs): # noqa: D401
800
+ # Get route pattern from resource mapping
801
+ route = _RESOURCE_ROUTES.get(id(self))
802
+ _capture_endpoint_info(req, hop_key, fname, lno, responder_name, route=route)
803
+ try:
804
+ return base_fn(self, req, resp, *args, **kwargs)
805
+ except Exception as exc: # catches falcon.HTTPError too
806
+ custom_excepthook(type(exc), exc, exc.__traceback__)
807
+ raise
808
+
809
+ return _sync_wrapped
810
+
811
+
812
+ # ---------------------------------------------------------------------------
813
+ # 3 | Attach wrapper to every on_<METHOD> responder in a resource
814
+ # ---------------------------------------------------------------------------
815
+
816
+
817
+ def _wrap_resource(resource: Any) -> None:
818
+ for attr in dir(resource):
819
+ if not attr.startswith("on_"):
820
+ continue
821
+
822
+ handler = getattr(resource, attr)
823
+ if not callable(handler) or getattr(handler, "__sf_hop_wrapped__", False):
824
+ continue
825
+
826
+ base_fn = handler.__func__ if isinstance(handler, MethodType) else handler
827
+ wrapped_fn = _make_wrapper(base_fn, resource)
828
+ setattr(wrapped_fn, "__sf_hop_wrapped__", True)
829
+
830
+ # Bind to the *instance* so Falcon passes (req, resp, …) correctly
831
+ bound = MethodType(wrapped_fn, resource)
832
+ setattr(resource, attr, bound)
833
+
834
+
835
+ # ---------------------------------------------------------------------------
836
+ # 4 | Middleware merge utility (unchanged from earlier patch)
837
+ # ---------------------------------------------------------------------------
838
+
839
+
840
+ def _middleware_pos(cls) -> int:
841
+ sig = inspect.signature(cls.__init__)
842
+ params = [p for p in sig.parameters.values() if p.name != "self"]
843
+ try:
844
+ return [p.name for p in params].index("middleware")
845
+ except ValueError:
846
+ return -1
847
+
848
+
849
+ def _merge_middleware(args, kwargs, mw_pos):
850
+ pos = list(args)
851
+ kw = dict(kwargs)
852
+ existing, used = None, None
853
+
854
+ if "middleware" in kw:
855
+ existing = kw.pop("middleware")
856
+ if existing is None and mw_pos >= 0 and mw_pos < len(pos):
857
+ cand = pos[mw_pos]
858
+ # Not the Response class?
859
+ if not inspect.isclass(cand):
860
+ existing, used = cand, mw_pos
861
+ if existing is None and len(pos) == 1:
862
+ existing, used = pos[0], 0
863
+
864
+ merged: List[Any] = []
865
+ if existing is not None:
866
+ merged = list(existing) if isinstance(existing, (list, tuple)) else [existing]
867
+ merged.insert(0, SFTracingFalconMiddleware())
868
+
869
+ if used is not None:
870
+ pos[used] = merged
871
+ else:
872
+ kw["middleware"] = merged
873
+
874
+ return tuple(pos), kw
875
+
876
+
877
+ # ---------------------------------------------------------------------------
878
+ # 5 | Patch helpers
879
+ # ---------------------------------------------------------------------------
880
+
881
+
882
+ def _patch_app_class(app_cls) -> None:
883
+ mw_pos = _middleware_pos(app_cls)
884
+ orig_init = app_cls.__init__
885
+ orig_add = app_cls.add_route
886
+
887
+ @functools.wraps(orig_init)
888
+ def patched_init(self, *args, **kwargs):
889
+ new_args, new_kwargs = _merge_middleware(args, kwargs, mw_pos)
890
+ orig_init(self, *new_args, **new_kwargs)
891
+
892
+ # Note: Profiler is already installed by unified_interceptor.py
893
+
894
+ def patched_add_route(self, uri_template, resource, **kwargs):
895
+ # Store route pattern for this resource instance
896
+ _RESOURCE_ROUTES[id(resource)] = uri_template
897
+ _wrap_resource(resource)
898
+ return orig_add(self, uri_template, resource, **kwargs)
899
+
900
+ app_cls.__init__ = patched_init
901
+ app_cls.add_route = patched_add_route
902
+
903
+
904
+ # ---------------------------------------------------------------------------
905
+ # 6 | Public entry point
906
+ # ---------------------------------------------------------------------------
907
+
908
+
909
+ def patch_falcon(routes_to_skip: Optional[List[str]] = None) -> None:
910
+ """Activate tracing for both WSGI and ASGI Falcon apps."""
911
+ global _ROUTES_TO_SKIP
912
+ _ROUTES_TO_SKIP = routes_to_skip or []
913
+
914
+ try:
915
+ import falcon
916
+ except ImportError: # pragma: no cover
917
+ return
918
+
919
+ # Patch synchronous WSGI app
920
+ _patch_app_class(falcon.App)
921
+
922
+ # Patch asynchronous ASGI app, if available
923
+ try:
924
+ from falcon.asgi import App as ASGIApp # type: ignore
925
+
926
+ _patch_app_class(ASGIApp)
927
+ except ImportError:
928
+ pass
929
+
930
+ if SF_DEBUG and app_config._interceptors_initialized:
931
+ print("[[patch_falcon]] Falcon tracing middleware installed", log=False)