sf-veritas 0.10.3__cp313-cp313-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-313-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnet.c +924 -0
- sf_veritas/_sffastnet.cpython-313-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnetworkrequest.c +730 -0
- sf_veritas/_sffastnetworkrequest.cpython-313-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan.c +2155 -0
- sf_veritas/_sffuncspan.cpython-313-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan_config.c +617 -0
- sf_veritas/_sffuncspan_config.cpython-313-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfheadercheck.c +341 -0
- sf_veritas/_sfheadercheck.cpython-313-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfnetworkhop.c +1451 -0
- sf_veritas/_sfnetworkhop.cpython-313-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfservice.c +1175 -0
- sf_veritas/_sfservice.cpython-313-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,697 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Robyn web framework patch for OTEL-style network hop capture.
|
|
3
|
+
Captures request/response headers and bodies when enabled via env vars.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import functools
|
|
7
|
+
import inspect
|
|
8
|
+
import sys
|
|
9
|
+
import sysconfig
|
|
10
|
+
from functools import lru_cache
|
|
11
|
+
from threading import local
|
|
12
|
+
from typing import Any, Callable, Optional, Set
|
|
13
|
+
|
|
14
|
+
from ... import app_config
|
|
15
|
+
from ...constants import (
|
|
16
|
+
FUNCSPAN_OVERRIDE_HEADER_BYTES,
|
|
17
|
+
SAILFISH_TRACING_HEADER,
|
|
18
|
+
SAILFISH_TRACING_HEADER_BYTES,
|
|
19
|
+
)
|
|
20
|
+
from ...custom_excepthook import custom_excepthook
|
|
21
|
+
from ...env_vars import (
|
|
22
|
+
SF_DEBUG,
|
|
23
|
+
SF_NETWORKHOP_CAPTURE_ENABLED,
|
|
24
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_BODY,
|
|
25
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS,
|
|
26
|
+
SF_NETWORKHOP_CAPTURE_RESPONSE_BODY,
|
|
27
|
+
SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS,
|
|
28
|
+
SF_NETWORKHOP_REQUEST_LIMIT_MB,
|
|
29
|
+
SF_NETWORKHOP_RESPONSE_LIMIT_MB,
|
|
30
|
+
)
|
|
31
|
+
from ...fast_network_hop import fast_send_network_hop_fast, register_endpoint
|
|
32
|
+
from ...thread_local import (
|
|
33
|
+
clear_c_tls_parent_trace_id,
|
|
34
|
+
clear_outbound_header_base,
|
|
35
|
+
clear_trace_id,
|
|
36
|
+
generate_new_trace_id,
|
|
37
|
+
get_or_set_sf_trace_id,
|
|
38
|
+
get_sf_trace_id,
|
|
39
|
+
set_funcspan_override,
|
|
40
|
+
set_outbound_header_base,
|
|
41
|
+
)
|
|
42
|
+
from ..constants import supported_network_verbs as HTTP_METHODS
|
|
43
|
+
from .cors_utils import inject_sailfish_headers, should_inject_headers
|
|
44
|
+
from .utils import _is_user_code, _unwrap_user_func, should_skip_route
|
|
45
|
+
|
|
46
|
+
# JSON serialization - try fast orjson first, fallback to stdlib json
|
|
47
|
+
try:
|
|
48
|
+
import orjson
|
|
49
|
+
|
|
50
|
+
HAS_ORJSON = True
|
|
51
|
+
except ImportError:
|
|
52
|
+
import json
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
_stdlib = sysconfig.get_paths()["stdlib"]
|
|
56
|
+
|
|
57
|
+
_SKIP_TRACING_ATTR = "_sf_skip_tracing"
|
|
58
|
+
|
|
59
|
+
# Size limits in bytes
|
|
60
|
+
_REQUEST_LIMIT_BYTES = SF_NETWORKHOP_REQUEST_LIMIT_MB * 1024 * 1024
|
|
61
|
+
_RESPONSE_LIMIT_BYTES = SF_NETWORKHOP_RESPONSE_LIMIT_MB * 1024 * 1024
|
|
62
|
+
|
|
63
|
+
# Pre-registered endpoint IDs
|
|
64
|
+
_ENDPOINT_REGISTRY: dict[tuple, int] = {}
|
|
65
|
+
|
|
66
|
+
# Thread-local storage for request data (since we can't set attributes on Request object)
|
|
67
|
+
_request_data = local()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@lru_cache(maxsize=512)
|
|
71
|
+
def _is_user_code(path: Optional[str]) -> bool:
|
|
72
|
+
"""
|
|
73
|
+
True only for “application” files (not stdlib or site-packages).
|
|
74
|
+
"""
|
|
75
|
+
if not path or path.startswith("<"):
|
|
76
|
+
return False
|
|
77
|
+
if path.startswith(_stdlib):
|
|
78
|
+
return False
|
|
79
|
+
if "site-packages" in path or "dist-packages" in path:
|
|
80
|
+
return False
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _should_trace_endpoint(endpoint_fn: Callable) -> bool:
|
|
85
|
+
"""Check if endpoint should be traced."""
|
|
86
|
+
if getattr(endpoint_fn, _SKIP_TRACING_ATTR, False):
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
code = getattr(endpoint_fn, "__code__", None)
|
|
90
|
+
if not code:
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
filename = code.co_filename
|
|
94
|
+
if not _is_user_code(filename):
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
if getattr(endpoint_fn, "__module__", "").startswith("strawberry"):
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def patch_robyn(routes_to_skip: Optional[list] = None):
|
|
104
|
+
"""
|
|
105
|
+
OTEL-STYLE Robyn patch using lightweight wrappers + hooks.
|
|
106
|
+
- Wrappers: Pre-register endpoints and store endpoint_id (minimal overhead)
|
|
107
|
+
- Hooks: Capture request/response data and emit network hops
|
|
108
|
+
Safe no-op if Robyn isn't installed.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
routes_to_skip: Optional list of route patterns to skip (supports wildcards)
|
|
112
|
+
"""
|
|
113
|
+
routes_to_skip = routes_to_skip or []
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
import robyn
|
|
117
|
+
except ImportError:
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
# Patch route decorators to wrap handlers
|
|
121
|
+
for method_name in HTTP_METHODS:
|
|
122
|
+
if not hasattr(robyn.Robyn, method_name):
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
original_method = getattr(robyn.Robyn, method_name)
|
|
126
|
+
|
|
127
|
+
def make_patched(orig):
|
|
128
|
+
@functools.wraps(orig)
|
|
129
|
+
def patched(self, path: str, *args, **kwargs):
|
|
130
|
+
# Get original decorator
|
|
131
|
+
decorator = orig(self, path, *args, **kwargs)
|
|
132
|
+
|
|
133
|
+
def wrapper(fn):
|
|
134
|
+
# Check for @skip_network_tracing on the wrapped function BEFORE unwrapping
|
|
135
|
+
if getattr(fn, _SKIP_TRACING_ATTR, False):
|
|
136
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
137
|
+
print(
|
|
138
|
+
f"[[Robyn]] Skipping endpoint (marked with @skip_network_tracing): {fn.__name__ if hasattr(fn, '__name__') else fn}",
|
|
139
|
+
log=False,
|
|
140
|
+
)
|
|
141
|
+
return decorator(fn)
|
|
142
|
+
|
|
143
|
+
real_fn = _unwrap_user_func(fn)
|
|
144
|
+
|
|
145
|
+
# Check if endpoint should be traced
|
|
146
|
+
if not _should_trace_endpoint(real_fn):
|
|
147
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
148
|
+
print(
|
|
149
|
+
f"[[Robyn]] Skipping endpoint (not user code or Strawberry): {real_fn.__name__ if hasattr(real_fn, '__name__') else real_fn}",
|
|
150
|
+
log=False,
|
|
151
|
+
)
|
|
152
|
+
return decorator(fn)
|
|
153
|
+
|
|
154
|
+
# Check if route should be skipped based on wildcard patterns
|
|
155
|
+
if should_skip_route(path, routes_to_skip):
|
|
156
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
157
|
+
print(
|
|
158
|
+
f"[[Robyn]] Skipping endpoint (route matches skip pattern): {path}",
|
|
159
|
+
log=False,
|
|
160
|
+
)
|
|
161
|
+
return decorator(fn)
|
|
162
|
+
|
|
163
|
+
# Pre-register endpoint if user code
|
|
164
|
+
endpoint_id = None
|
|
165
|
+
filename = real_fn.__code__.co_filename
|
|
166
|
+
if _is_user_code(filename):
|
|
167
|
+
line_no = real_fn.__code__.co_firstlineno
|
|
168
|
+
hop_key = (filename, line_no)
|
|
169
|
+
|
|
170
|
+
endpoint_id = _ENDPOINT_REGISTRY.get(hop_key)
|
|
171
|
+
if endpoint_id is None:
|
|
172
|
+
# Extract route pattern (Robyn uses the path parameter directly)
|
|
173
|
+
route_pattern = path
|
|
174
|
+
|
|
175
|
+
endpoint_id = register_endpoint(
|
|
176
|
+
line=str(line_no),
|
|
177
|
+
column="0",
|
|
178
|
+
name=real_fn.__name__,
|
|
179
|
+
entrypoint=filename,
|
|
180
|
+
route=route_pattern,
|
|
181
|
+
)
|
|
182
|
+
if endpoint_id >= 0:
|
|
183
|
+
_ENDPOINT_REGISTRY[hop_key] = endpoint_id
|
|
184
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
185
|
+
print(
|
|
186
|
+
f"[[Robyn]] Registered endpoint: {real_fn.__name__} @ {filename}:{line_no} route={route_pattern} (id={endpoint_id})",
|
|
187
|
+
log=False,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Minimal wrapper: just store endpoint_id
|
|
191
|
+
@functools.wraps(fn)
|
|
192
|
+
async def minimal_wrapper(*a, **kw):
|
|
193
|
+
# Store endpoint_id in thread-local for after_request hook
|
|
194
|
+
if endpoint_id is not None:
|
|
195
|
+
_request_data.endpoint_id = endpoint_id
|
|
196
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
197
|
+
print(
|
|
198
|
+
f"[[Robyn]] Wrapper storing endpoint_id={endpoint_id} for {real_fn.__name__}",
|
|
199
|
+
log=False,
|
|
200
|
+
)
|
|
201
|
+
# Call original handler
|
|
202
|
+
return await fn(*a, **kw)
|
|
203
|
+
|
|
204
|
+
return decorator(minimal_wrapper)
|
|
205
|
+
|
|
206
|
+
return wrapper
|
|
207
|
+
|
|
208
|
+
return patched
|
|
209
|
+
|
|
210
|
+
setattr(robyn.Robyn, method_name, make_patched(original_method))
|
|
211
|
+
|
|
212
|
+
original_init = robyn.Robyn.__init__
|
|
213
|
+
|
|
214
|
+
def patched_init(self, *args, **kwargs):
|
|
215
|
+
# Let Robyn initialize normally
|
|
216
|
+
original_init(self, *args, **kwargs)
|
|
217
|
+
|
|
218
|
+
# Install before_request hook for header propagation and request capture
|
|
219
|
+
@self.before_request()
|
|
220
|
+
async def _sf_before_request(request):
|
|
221
|
+
"""Capture trace header and request data before handler runs."""
|
|
222
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
223
|
+
print(f"[[Robyn]] Request object: {request}", log=False)
|
|
224
|
+
print(f"[[Robyn]] Request type: {type(request)}", log=False)
|
|
225
|
+
print("[[Robyn]] Request attributes and values:", log=False)
|
|
226
|
+
for attr in dir(request):
|
|
227
|
+
if not attr.startswith("_"):
|
|
228
|
+
try:
|
|
229
|
+
value = getattr(request, attr)
|
|
230
|
+
print(f" {attr} = {value}", log=False)
|
|
231
|
+
except Exception as e:
|
|
232
|
+
print(f" {attr} = <error: {e}>", log=False)
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
# 0. Capture path and query string for later use
|
|
236
|
+
try:
|
|
237
|
+
if hasattr(request, "url"):
|
|
238
|
+
url = request.url
|
|
239
|
+
_request_data.path = getattr(url, "path", None)
|
|
240
|
+
query = getattr(url, "queries", None)
|
|
241
|
+
# queries might be a dict, convert to query string
|
|
242
|
+
if query:
|
|
243
|
+
_request_data.query = "&".join(
|
|
244
|
+
f"{k}={v}" for k, v in query.items()
|
|
245
|
+
).encode("utf-8")
|
|
246
|
+
else:
|
|
247
|
+
_request_data.query = b""
|
|
248
|
+
else:
|
|
249
|
+
_request_data.path = None
|
|
250
|
+
_request_data.query = b""
|
|
251
|
+
except Exception as e:
|
|
252
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
253
|
+
print(f"[[Robyn]] Failed to capture path/query: {e}", log=False)
|
|
254
|
+
|
|
255
|
+
headers = getattr(request, "headers", {})
|
|
256
|
+
|
|
257
|
+
# PERFORMANCE: Single-pass bytes-level header scan (similar to FastAPI)
|
|
258
|
+
# Scan headers once on bytes, only decode what we need
|
|
259
|
+
incoming_trace_raw = None # bytes
|
|
260
|
+
funcspan_raw = None # bytes
|
|
261
|
+
req_headers = None # dict[str,str] only if capture enabled
|
|
262
|
+
|
|
263
|
+
capture_req_headers = (
|
|
264
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS # local cache
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# 1. Header propagation and capture
|
|
268
|
+
if hasattr(headers, "get_headers"):
|
|
269
|
+
try:
|
|
270
|
+
raw_headers = headers.get_headers()
|
|
271
|
+
|
|
272
|
+
if capture_req_headers:
|
|
273
|
+
# Build dict while scanning for special headers
|
|
274
|
+
tmp = {}
|
|
275
|
+
for k, v in raw_headers.items():
|
|
276
|
+
k_lower = k.lower() if isinstance(k, str) else k
|
|
277
|
+
v_val = (
|
|
278
|
+
v[0] if isinstance(v, list) and len(v) > 0 else v
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
if k_lower == SAILFISH_TRACING_HEADER.lower():
|
|
282
|
+
incoming_trace_raw = (
|
|
283
|
+
v_val
|
|
284
|
+
if isinstance(v_val, bytes)
|
|
285
|
+
else v_val.encode("latin-1")
|
|
286
|
+
)
|
|
287
|
+
elif k_lower == "x-sf3-functionspancaptureoverride":
|
|
288
|
+
funcspan_raw = (
|
|
289
|
+
v_val
|
|
290
|
+
if isinstance(v_val, bytes)
|
|
291
|
+
else v_val.encode("latin-1")
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
tmp[k] = v_val
|
|
295
|
+
req_headers = tmp
|
|
296
|
+
else:
|
|
297
|
+
# Just scan for special headers
|
|
298
|
+
for k, v in raw_headers.items():
|
|
299
|
+
k_lower = k.lower() if isinstance(k, str) else k
|
|
300
|
+
v_val = (
|
|
301
|
+
v[0] if isinstance(v, list) and len(v) > 0 else v
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
if k_lower == SAILFISH_TRACING_HEADER.lower():
|
|
305
|
+
incoming_trace_raw = (
|
|
306
|
+
v_val
|
|
307
|
+
if isinstance(v_val, bytes)
|
|
308
|
+
else v_val.encode("latin-1")
|
|
309
|
+
)
|
|
310
|
+
elif k_lower == "x-sf3-functionspancaptureoverride":
|
|
311
|
+
funcspan_raw = (
|
|
312
|
+
v_val
|
|
313
|
+
if isinstance(v_val, bytes)
|
|
314
|
+
else v_val.encode("latin-1")
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
# Store headers for later
|
|
318
|
+
if req_headers:
|
|
319
|
+
_request_data.headers = req_headers
|
|
320
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
321
|
+
print(
|
|
322
|
+
f"[[Robyn]] Captured request headers: {len(req_headers)} headers",
|
|
323
|
+
log=False,
|
|
324
|
+
)
|
|
325
|
+
except Exception as e:
|
|
326
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
327
|
+
print(
|
|
328
|
+
f"[[Robyn]] Failed to capture request headers: {e}",
|
|
329
|
+
log=False,
|
|
330
|
+
)
|
|
331
|
+
elif hasattr(headers, "get"):
|
|
332
|
+
# Fallback to dict-like interface
|
|
333
|
+
try:
|
|
334
|
+
hdr = headers.get(SAILFISH_TRACING_HEADER)
|
|
335
|
+
if hdr:
|
|
336
|
+
incoming_trace_raw = (
|
|
337
|
+
hdr if isinstance(hdr, bytes) else hdr.encode("latin-1")
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
funcspan_hdr = headers.get("X-Sf3-FunctionSpanCaptureOverride")
|
|
341
|
+
if funcspan_hdr:
|
|
342
|
+
funcspan_raw = (
|
|
343
|
+
funcspan_hdr
|
|
344
|
+
if isinstance(funcspan_hdr, bytes)
|
|
345
|
+
else funcspan_hdr.encode("latin-1")
|
|
346
|
+
)
|
|
347
|
+
except (KeyError, TypeError):
|
|
348
|
+
pass
|
|
349
|
+
|
|
350
|
+
# CRITICAL: Seed/ensure trace_id immediately (BEFORE any outbound work)
|
|
351
|
+
if incoming_trace_raw:
|
|
352
|
+
# Incoming X-Sf3-Rid header provided - use it
|
|
353
|
+
incoming_trace = (
|
|
354
|
+
incoming_trace_raw.decode("latin-1")
|
|
355
|
+
if isinstance(incoming_trace_raw, bytes)
|
|
356
|
+
else incoming_trace_raw
|
|
357
|
+
)
|
|
358
|
+
get_or_set_sf_trace_id(
|
|
359
|
+
incoming_trace, is_associated_with_inbound_request=True
|
|
360
|
+
)
|
|
361
|
+
else:
|
|
362
|
+
# No incoming X-Sf3-Rid header - generate fresh trace_id for this request
|
|
363
|
+
generate_new_trace_id()
|
|
364
|
+
|
|
365
|
+
# Optional funcspan override (decode only if present)
|
|
366
|
+
funcspan_override_header = (
|
|
367
|
+
(
|
|
368
|
+
funcspan_raw.decode("latin-1")
|
|
369
|
+
if isinstance(funcspan_raw, bytes)
|
|
370
|
+
else funcspan_raw
|
|
371
|
+
)
|
|
372
|
+
if funcspan_raw
|
|
373
|
+
else None
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
if funcspan_override_header:
|
|
377
|
+
try:
|
|
378
|
+
set_funcspan_override(funcspan_override_header)
|
|
379
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
380
|
+
print(
|
|
381
|
+
f"[[Robyn.before_request]] Set function span override from header: {funcspan_override_header}",
|
|
382
|
+
log=False,
|
|
383
|
+
)
|
|
384
|
+
except Exception as e:
|
|
385
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
386
|
+
print(
|
|
387
|
+
f"[[Robyn.before_request]] Failed to set function span override: {e}",
|
|
388
|
+
log=False,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# Initialize outbound base without list/allocs from split()
|
|
392
|
+
try:
|
|
393
|
+
trace_id = get_sf_trace_id()
|
|
394
|
+
if trace_id:
|
|
395
|
+
s = str(trace_id)
|
|
396
|
+
i = s.find("/") # session
|
|
397
|
+
j = s.find("/", i + 1) if i != -1 else -1 # page
|
|
398
|
+
if j != -1:
|
|
399
|
+
base_trace = s[:j] # "session/page"
|
|
400
|
+
set_outbound_header_base(
|
|
401
|
+
base_trace=base_trace,
|
|
402
|
+
parent_trace_id=s, # "session/page/uuid"
|
|
403
|
+
funcspan=funcspan_override_header,
|
|
404
|
+
)
|
|
405
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
406
|
+
print(
|
|
407
|
+
f"[[Robyn.before_request]] Initialized outbound header base (base={base_trace[:16]}...)",
|
|
408
|
+
log=False,
|
|
409
|
+
)
|
|
410
|
+
except Exception as e:
|
|
411
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
412
|
+
print(
|
|
413
|
+
f"[[Robyn.before_request]] Failed to initialize outbound header base: {e}",
|
|
414
|
+
log=False,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
# 2. Capture request body if enabled (only if capturing network hops)
|
|
418
|
+
if SF_NETWORKHOP_CAPTURE_ENABLED and SF_NETWORKHOP_CAPTURE_REQUEST_BODY:
|
|
419
|
+
try:
|
|
420
|
+
body = getattr(request, "body", None)
|
|
421
|
+
if body:
|
|
422
|
+
if isinstance(body, bytes):
|
|
423
|
+
req_body = body[:_REQUEST_LIMIT_BYTES]
|
|
424
|
+
elif isinstance(body, str):
|
|
425
|
+
req_body = body.encode("utf-8")[:_REQUEST_LIMIT_BYTES]
|
|
426
|
+
else:
|
|
427
|
+
req_body = None
|
|
428
|
+
_request_data.body = req_body
|
|
429
|
+
if SF_DEBUG and req_body:
|
|
430
|
+
print(
|
|
431
|
+
f"[[Robyn]] Request body capture: {len(req_body)} bytes",
|
|
432
|
+
log=False,
|
|
433
|
+
)
|
|
434
|
+
except Exception as e:
|
|
435
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
436
|
+
print(
|
|
437
|
+
f"[[Robyn]] Failed to capture request body: {e}",
|
|
438
|
+
log=False,
|
|
439
|
+
)
|
|
440
|
+
except Exception as e:
|
|
441
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
442
|
+
print(f"[[Robyn]] before_request error: {e}", log=False)
|
|
443
|
+
|
|
444
|
+
return request
|
|
445
|
+
|
|
446
|
+
# Install after_request hook for OTEL-style emission
|
|
447
|
+
@self.after_request()
|
|
448
|
+
async def _sf_after_request(response):
|
|
449
|
+
"""OTEL-STYLE: Emit network hop AFTER response is built."""
|
|
450
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
451
|
+
print(f"[[Robyn]] Response object: {response}", log=False)
|
|
452
|
+
print(f"[[Robyn]] Response type: {type(response)}", log=False)
|
|
453
|
+
print("[[Robyn]] Response attributes and values:", log=False)
|
|
454
|
+
for attr in dir(response):
|
|
455
|
+
if not attr.startswith("_"):
|
|
456
|
+
try:
|
|
457
|
+
value = getattr(response, attr)
|
|
458
|
+
print(f" {attr} = {value}", log=False)
|
|
459
|
+
except Exception as e:
|
|
460
|
+
print(f" {attr} = <error: {e}>", log=False)
|
|
461
|
+
|
|
462
|
+
try:
|
|
463
|
+
# OPTIMIZATION: Skip ALL capture infrastructure if not capturing network hops
|
|
464
|
+
# We still needed to set up trace_id and outbound header base in before_request
|
|
465
|
+
# (for outbound call tracing), but we can skip all request/response capture overhead
|
|
466
|
+
if SF_NETWORKHOP_CAPTURE_ENABLED:
|
|
467
|
+
# Get endpoint_id from thread-local storage (set by wrapper)
|
|
468
|
+
endpoint_id = getattr(_request_data, "endpoint_id", None)
|
|
469
|
+
|
|
470
|
+
if endpoint_id is not None and endpoint_id >= 0:
|
|
471
|
+
# OPTIMIZATION: Use get_sf_trace_id() directly instead of get_or_set_sf_trace_id()
|
|
472
|
+
# Trace ID is GUARANTEED to be set at request start
|
|
473
|
+
# This saves time by avoiding tuple unpacking and conditional logic
|
|
474
|
+
session_id = get_sf_trace_id()
|
|
475
|
+
|
|
476
|
+
# Get captured request data from thread-local storage
|
|
477
|
+
req_headers = getattr(_request_data, "headers", None)
|
|
478
|
+
req_body = getattr(_request_data, "body", None)
|
|
479
|
+
|
|
480
|
+
# Capture response headers if enabled (from Response object)
|
|
481
|
+
resp_headers = None
|
|
482
|
+
if SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS:
|
|
483
|
+
try:
|
|
484
|
+
if hasattr(response, "headers"):
|
|
485
|
+
resp_hdrs = response.headers
|
|
486
|
+
if hasattr(resp_hdrs, "get_headers"):
|
|
487
|
+
raw_resp_headers = resp_hdrs.get_headers()
|
|
488
|
+
resp_headers = (
|
|
489
|
+
{
|
|
490
|
+
k: (
|
|
491
|
+
v[0]
|
|
492
|
+
if isinstance(v, list)
|
|
493
|
+
and len(v) > 0
|
|
494
|
+
else v
|
|
495
|
+
)
|
|
496
|
+
for k, v in raw_resp_headers.items()
|
|
497
|
+
}
|
|
498
|
+
if raw_resp_headers
|
|
499
|
+
else None
|
|
500
|
+
)
|
|
501
|
+
elif isinstance(resp_hdrs, dict):
|
|
502
|
+
resp_headers = dict(resp_hdrs)
|
|
503
|
+
if SF_DEBUG and resp_headers:
|
|
504
|
+
print(
|
|
505
|
+
f"[[Robyn]] Captured response headers: {len(resp_headers)} headers",
|
|
506
|
+
log=False,
|
|
507
|
+
)
|
|
508
|
+
except Exception as e:
|
|
509
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
510
|
+
print(
|
|
511
|
+
f"[[Robyn]] Failed to capture response headers: {e}",
|
|
512
|
+
log=False,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# Capture response body if enabled
|
|
516
|
+
resp_body = None
|
|
517
|
+
if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY:
|
|
518
|
+
try:
|
|
519
|
+
# Response object should have body or description
|
|
520
|
+
if hasattr(response, "body"):
|
|
521
|
+
body = response.body
|
|
522
|
+
elif hasattr(response, "description"):
|
|
523
|
+
body = response.description
|
|
524
|
+
else:
|
|
525
|
+
body = None
|
|
526
|
+
|
|
527
|
+
if body:
|
|
528
|
+
if isinstance(body, bytes):
|
|
529
|
+
resp_body = body[:_RESPONSE_LIMIT_BYTES]
|
|
530
|
+
elif isinstance(body, str):
|
|
531
|
+
resp_body = body.encode("utf-8")[
|
|
532
|
+
:_RESPONSE_LIMIT_BYTES
|
|
533
|
+
]
|
|
534
|
+
elif isinstance(body, dict):
|
|
535
|
+
if HAS_ORJSON:
|
|
536
|
+
resp_body = orjson.dumps(body).encode(
|
|
537
|
+
"utf-8"
|
|
538
|
+
)[:_RESPONSE_LIMIT_BYTES]
|
|
539
|
+
else:
|
|
540
|
+
resp_body = json.dumps(body).encode(
|
|
541
|
+
"utf-8"
|
|
542
|
+
)[:_RESPONSE_LIMIT_BYTES]
|
|
543
|
+
if SF_DEBUG and resp_body:
|
|
544
|
+
print(
|
|
545
|
+
f"[[Robyn]] Captured response body: {len(resp_body)} bytes",
|
|
546
|
+
log=False,
|
|
547
|
+
)
|
|
548
|
+
except Exception as e:
|
|
549
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
550
|
+
print(
|
|
551
|
+
f"[[Robyn]] Failed to capture response body: {e}",
|
|
552
|
+
log=False,
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
# Extract raw path and query string for C to parse (from thread-local request data)
|
|
556
|
+
raw_path = getattr(_request_data, "path", None)
|
|
557
|
+
raw_query = getattr(_request_data, "query", b"")
|
|
558
|
+
|
|
559
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
560
|
+
print(
|
|
561
|
+
f"[[Robyn]] About to emit network hop: endpoint_id={endpoint_id}, "
|
|
562
|
+
f"req_headers={'present' if req_headers else 'None'}, "
|
|
563
|
+
f"req_body={len(req_body) if req_body else 0} bytes, "
|
|
564
|
+
f"resp_headers={'present' if resp_headers else 'None'}, "
|
|
565
|
+
f"resp_body={len(resp_body) if resp_body else 0} bytes",
|
|
566
|
+
log=False,
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
# Direct C call - queues to background worker, returns instantly
|
|
570
|
+
# C will parse route and query_params from raw data
|
|
571
|
+
fast_send_network_hop_fast(
|
|
572
|
+
session_id=session_id,
|
|
573
|
+
endpoint_id=endpoint_id,
|
|
574
|
+
raw_path=raw_path,
|
|
575
|
+
raw_query_string=raw_query,
|
|
576
|
+
request_headers=req_headers,
|
|
577
|
+
request_body=req_body,
|
|
578
|
+
response_headers=resp_headers,
|
|
579
|
+
response_body=resp_body,
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
583
|
+
print(
|
|
584
|
+
f"[[Robyn]] Emitted network hop: endpoint_id={endpoint_id} "
|
|
585
|
+
f"session={session_id}",
|
|
586
|
+
log=False,
|
|
587
|
+
)
|
|
588
|
+
except Exception as e:
|
|
589
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
590
|
+
print(f"[[Robyn]] after_request error: {e}", log=False)
|
|
591
|
+
finally:
|
|
592
|
+
# CRITICAL: Clear C TLS to prevent stale data in thread pools
|
|
593
|
+
clear_c_tls_parent_trace_id()
|
|
594
|
+
|
|
595
|
+
# CRITICAL: Clear outbound header base to prevent stale cached headers
|
|
596
|
+
# ContextVar does NOT automatically clean up in thread pools - must clear explicitly
|
|
597
|
+
clear_outbound_header_base()
|
|
598
|
+
|
|
599
|
+
# CRITICAL: Clear trace_id to ensure fresh generation for next request
|
|
600
|
+
# Without this, get_or_set_sf_trace_id() reuses trace_id from previous request
|
|
601
|
+
# causing X-Sf4-Prid to stay constant when no incoming X-Sf3-Rid header
|
|
602
|
+
clear_trace_id()
|
|
603
|
+
|
|
604
|
+
return response
|
|
605
|
+
|
|
606
|
+
# Install exception handler
|
|
607
|
+
@self.exception
|
|
608
|
+
async def _sf_exception_handler(error):
|
|
609
|
+
"""Capture all exceptions and forward to custom_excepthook."""
|
|
610
|
+
try:
|
|
611
|
+
custom_excepthook(type(error), error, error.__traceback__)
|
|
612
|
+
except Exception:
|
|
613
|
+
pass
|
|
614
|
+
# Re-raise so Robyn's default error handler processes it
|
|
615
|
+
raise error
|
|
616
|
+
|
|
617
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
618
|
+
print(
|
|
619
|
+
"[[patch_robyn]] OTEL-style hooks installed (no handler wrapping)",
|
|
620
|
+
log=False,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
robyn.Robyn.__init__ = patched_init
|
|
624
|
+
|
|
625
|
+
# Apply CORS patching
|
|
626
|
+
patch_robyn_cors()
|
|
627
|
+
|
|
628
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
629
|
+
print("[[patch_robyn]] OTEL-style patch applied", log=False)
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def patch_robyn_cors():
|
|
633
|
+
"""
|
|
634
|
+
Patch Robyn's ALLOW_CORS function to automatically inject Sailfish headers.
|
|
635
|
+
|
|
636
|
+
SAFE: Only modifies CORS if ALLOW_CORS is used by the application.
|
|
637
|
+
This ensures Sailfish tracing headers are included in CORS allow-headers.
|
|
638
|
+
"""
|
|
639
|
+
try:
|
|
640
|
+
import robyn
|
|
641
|
+
except ImportError:
|
|
642
|
+
# Robyn or cors_utils not available, skip patching
|
|
643
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
644
|
+
print(
|
|
645
|
+
"[[patch_robyn_cors]] Robyn or cors_utils not found, skipping",
|
|
646
|
+
log=False,
|
|
647
|
+
)
|
|
648
|
+
return
|
|
649
|
+
|
|
650
|
+
# Check if ALLOW_CORS exists
|
|
651
|
+
if not hasattr(robyn, "ALLOW_CORS"):
|
|
652
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
653
|
+
print(
|
|
654
|
+
"[[patch_robyn_cors]] ALLOW_CORS not found in Robyn, skipping",
|
|
655
|
+
log=False,
|
|
656
|
+
)
|
|
657
|
+
return
|
|
658
|
+
|
|
659
|
+
# Check if already patched
|
|
660
|
+
if hasattr(robyn.ALLOW_CORS, "_sf_cors_patched"):
|
|
661
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
662
|
+
print("[[patch_robyn_cors]] Already patched, skipping", log=False)
|
|
663
|
+
return
|
|
664
|
+
|
|
665
|
+
original_allow_cors = robyn.ALLOW_CORS
|
|
666
|
+
|
|
667
|
+
def patched_allow_cors(app, origins=None, **kwargs):
|
|
668
|
+
"""
|
|
669
|
+
Patched ALLOW_CORS that injects Sailfish headers into allowed headers.
|
|
670
|
+
|
|
671
|
+
Robyn's ALLOW_CORS signature varies by version, but typically:
|
|
672
|
+
- ALLOW_CORS(app, origins) or
|
|
673
|
+
- ALLOW_CORS(app, origins=..., allow_headers=..., ...)
|
|
674
|
+
"""
|
|
675
|
+
# Try to intercept allow_headers parameter if present
|
|
676
|
+
allow_headers = kwargs.get("allow_headers", None)
|
|
677
|
+
|
|
678
|
+
if should_inject_headers(allow_headers):
|
|
679
|
+
kwargs["allow_headers"] = inject_sailfish_headers(allow_headers)
|
|
680
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
681
|
+
print(
|
|
682
|
+
"[[patch_robyn_cors]] Injected Sailfish headers into Robyn CORS",
|
|
683
|
+
log=False,
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
# Call original ALLOW_CORS with potentially modified headers
|
|
687
|
+
return original_allow_cors(app, origins, **kwargs)
|
|
688
|
+
|
|
689
|
+
# Replace ALLOW_CORS with patched version
|
|
690
|
+
robyn.ALLOW_CORS = patched_allow_cors
|
|
691
|
+
robyn.ALLOW_CORS._sf_cors_patched = True
|
|
692
|
+
|
|
693
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
694
|
+
print(
|
|
695
|
+
"[[patch_robyn_cors]] Successfully patched Robyn ALLOW_CORS",
|
|
696
|
+
log=False,
|
|
697
|
+
)
|