sf-veritas 0.11.10__cp314-cp314-manylinux_2_28_x86_64.whl

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