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