sf-veritas 0.10.3__cp311-cp311-manylinux_2_28_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sf-veritas might be problematic. Click here for more details.
- sf_veritas/__init__.py +20 -0
- sf_veritas/_sffastlog.c +889 -0
- sf_veritas/_sffastlog.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnet.c +924 -0
- sf_veritas/_sffastnet.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnetworkrequest.c +730 -0
- sf_veritas/_sffastnetworkrequest.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan.c +2155 -0
- sf_veritas/_sffuncspan.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan_config.c +617 -0
- sf_veritas/_sffuncspan_config.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfheadercheck.c +341 -0
- sf_veritas/_sfheadercheck.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfnetworkhop.c +1451 -0
- sf_veritas/_sfnetworkhop.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfservice.c +1175 -0
- sf_veritas/_sfservice.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfteepreload.c +5167 -0
- sf_veritas/app_config.py +49 -0
- sf_veritas/cli.py +336 -0
- sf_veritas/constants.py +10 -0
- sf_veritas/custom_excepthook.py +304 -0
- sf_veritas/custom_log_handler.py +129 -0
- sf_veritas/custom_output_wrapper.py +144 -0
- sf_veritas/custom_print.py +146 -0
- sf_veritas/django_app.py +5 -0
- sf_veritas/env_vars.py +186 -0
- sf_veritas/exception_handling_middleware.py +18 -0
- sf_veritas/exception_metaclass.py +69 -0
- sf_veritas/fast_frame_info.py +116 -0
- sf_veritas/fast_network_hop.py +293 -0
- sf_veritas/frame_tools.py +112 -0
- sf_veritas/funcspan_config_loader.py +556 -0
- sf_veritas/function_span_profiler.py +1174 -0
- sf_veritas/import_hook.py +62 -0
- sf_veritas/infra_details/__init__.py +3 -0
- sf_veritas/infra_details/get_infra_details.py +24 -0
- sf_veritas/infra_details/kubernetes/__init__.py +3 -0
- sf_veritas/infra_details/kubernetes/get_cluster_name.py +147 -0
- sf_veritas/infra_details/kubernetes/get_details.py +7 -0
- sf_veritas/infra_details/running_on/__init__.py +17 -0
- sf_veritas/infra_details/running_on/kubernetes.py +11 -0
- sf_veritas/interceptors.py +497 -0
- sf_veritas/libsfnettee.so +0 -0
- sf_veritas/local_env_detect.py +118 -0
- sf_veritas/package_metadata.py +6 -0
- sf_veritas/patches/__init__.py +0 -0
- sf_veritas/patches/concurrent_futures.py +19 -0
- sf_veritas/patches/constants.py +1 -0
- sf_veritas/patches/exceptions.py +82 -0
- sf_veritas/patches/multiprocessing.py +32 -0
- sf_veritas/patches/network_libraries/__init__.py +76 -0
- sf_veritas/patches/network_libraries/aiohttp.py +281 -0
- sf_veritas/patches/network_libraries/curl_cffi.py +363 -0
- sf_veritas/patches/network_libraries/http_client.py +419 -0
- sf_veritas/patches/network_libraries/httpcore.py +515 -0
- sf_veritas/patches/network_libraries/httplib2.py +204 -0
- sf_veritas/patches/network_libraries/httpx.py +515 -0
- sf_veritas/patches/network_libraries/niquests.py +211 -0
- sf_veritas/patches/network_libraries/pycurl.py +385 -0
- sf_veritas/patches/network_libraries/requests.py +633 -0
- sf_veritas/patches/network_libraries/tornado.py +341 -0
- sf_veritas/patches/network_libraries/treq.py +270 -0
- sf_veritas/patches/network_libraries/urllib_request.py +468 -0
- sf_veritas/patches/network_libraries/utils.py +398 -0
- sf_veritas/patches/os.py +17 -0
- sf_veritas/patches/threading.py +218 -0
- sf_veritas/patches/web_frameworks/__init__.py +54 -0
- sf_veritas/patches/web_frameworks/aiohttp.py +793 -0
- sf_veritas/patches/web_frameworks/async_websocket_consumer.py +317 -0
- sf_veritas/patches/web_frameworks/blacksheep.py +527 -0
- sf_veritas/patches/web_frameworks/bottle.py +502 -0
- sf_veritas/patches/web_frameworks/cherrypy.py +678 -0
- sf_veritas/patches/web_frameworks/cors_utils.py +122 -0
- sf_veritas/patches/web_frameworks/django.py +944 -0
- sf_veritas/patches/web_frameworks/eve.py +395 -0
- sf_veritas/patches/web_frameworks/falcon.py +926 -0
- sf_veritas/patches/web_frameworks/fastapi.py +724 -0
- sf_veritas/patches/web_frameworks/flask.py +520 -0
- sf_veritas/patches/web_frameworks/klein.py +501 -0
- sf_veritas/patches/web_frameworks/litestar.py +551 -0
- sf_veritas/patches/web_frameworks/pyramid.py +428 -0
- sf_veritas/patches/web_frameworks/quart.py +824 -0
- sf_veritas/patches/web_frameworks/robyn.py +697 -0
- sf_veritas/patches/web_frameworks/sanic.py +857 -0
- sf_veritas/patches/web_frameworks/starlette.py +723 -0
- sf_veritas/patches/web_frameworks/strawberry.py +813 -0
- sf_veritas/patches/web_frameworks/tornado.py +481 -0
- sf_veritas/patches/web_frameworks/utils.py +91 -0
- sf_veritas/print_override.py +13 -0
- sf_veritas/regular_data_transmitter.py +409 -0
- sf_veritas/request_interceptor.py +401 -0
- sf_veritas/request_utils.py +550 -0
- sf_veritas/server_status.py +1 -0
- sf_veritas/shutdown_flag.py +11 -0
- sf_veritas/subprocess_startup.py +3 -0
- sf_veritas/test_cli.py +145 -0
- sf_veritas/thread_local.py +970 -0
- sf_veritas/timeutil.py +114 -0
- sf_veritas/transmit_exception_to_sailfish.py +28 -0
- sf_veritas/transmitter.py +132 -0
- sf_veritas/types.py +47 -0
- sf_veritas/unified_interceptor.py +1580 -0
- sf_veritas/utils.py +39 -0
- sf_veritas-0.10.3.dist-info/METADATA +97 -0
- sf_veritas-0.10.3.dist-info/RECORD +132 -0
- sf_veritas-0.10.3.dist-info/WHEEL +5 -0
- sf_veritas-0.10.3.dist-info/entry_points.txt +2 -0
- sf_veritas-0.10.3.dist-info/top_level.txt +1 -0
- sf_veritas.libs/libbrotlicommon-6ce2a53c.so.1.0.6 +0 -0
- sf_veritas.libs/libbrotlidec-811d1be3.so.1.0.6 +0 -0
- sf_veritas.libs/libcom_err-730ca923.so.2.1 +0 -0
- sf_veritas.libs/libcrypt-52aca757.so.1.1.0 +0 -0
- sf_veritas.libs/libcrypto-bdaed0ea.so.1.1.1k +0 -0
- sf_veritas.libs/libcurl-eaa3cf66.so.4.5.0 +0 -0
- sf_veritas.libs/libgssapi_krb5-323bbd21.so.2.2 +0 -0
- sf_veritas.libs/libidn2-2f4a5893.so.0.3.6 +0 -0
- sf_veritas.libs/libk5crypto-9a74ff38.so.3.1 +0 -0
- sf_veritas.libs/libkeyutils-2777d33d.so.1.6 +0 -0
- sf_veritas.libs/libkrb5-a55300e8.so.3.3 +0 -0
- sf_veritas.libs/libkrb5support-e6594cfc.so.0.1 +0 -0
- sf_veritas.libs/liblber-2-d20824ef.4.so.2.10.9 +0 -0
- sf_veritas.libs/libldap-2-cea2a960.4.so.2.10.9 +0 -0
- sf_veritas.libs/libnghttp2-39367a22.so.14.17.0 +0 -0
- sf_veritas.libs/libpcre2-8-516f4c9d.so.0.7.1 +0 -0
- sf_veritas.libs/libpsl-99becdd3.so.5.3.1 +0 -0
- sf_veritas.libs/libsasl2-7de4d792.so.3.0.0 +0 -0
- sf_veritas.libs/libselinux-d0805dcb.so.1 +0 -0
- sf_veritas.libs/libssh-c11d285b.so.4.8.7 +0 -0
- sf_veritas.libs/libssl-60250281.so.1.1.1k +0 -0
- sf_veritas.libs/libunistring-05abdd40.so.2.1.0 +0 -0
- 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)
|