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.

Files changed (132) hide show
  1. sf_veritas/__init__.py +20 -0
  2. sf_veritas/_sffastlog.c +889 -0
  3. sf_veritas/_sffastlog.cpython-311-x86_64-linux-gnu.so +0 -0
  4. sf_veritas/_sffastnet.c +924 -0
  5. sf_veritas/_sffastnet.cpython-311-x86_64-linux-gnu.so +0 -0
  6. sf_veritas/_sffastnetworkrequest.c +730 -0
  7. sf_veritas/_sffastnetworkrequest.cpython-311-x86_64-linux-gnu.so +0 -0
  8. sf_veritas/_sffuncspan.c +2155 -0
  9. sf_veritas/_sffuncspan.cpython-311-x86_64-linux-gnu.so +0 -0
  10. sf_veritas/_sffuncspan_config.c +617 -0
  11. sf_veritas/_sffuncspan_config.cpython-311-x86_64-linux-gnu.so +0 -0
  12. sf_veritas/_sfheadercheck.c +341 -0
  13. sf_veritas/_sfheadercheck.cpython-311-x86_64-linux-gnu.so +0 -0
  14. sf_veritas/_sfnetworkhop.c +1451 -0
  15. sf_veritas/_sfnetworkhop.cpython-311-x86_64-linux-gnu.so +0 -0
  16. sf_veritas/_sfservice.c +1175 -0
  17. sf_veritas/_sfservice.cpython-311-x86_64-linux-gnu.so +0 -0
  18. sf_veritas/_sfteepreload.c +5167 -0
  19. sf_veritas/app_config.py +49 -0
  20. sf_veritas/cli.py +336 -0
  21. sf_veritas/constants.py +10 -0
  22. sf_veritas/custom_excepthook.py +304 -0
  23. sf_veritas/custom_log_handler.py +129 -0
  24. sf_veritas/custom_output_wrapper.py +144 -0
  25. sf_veritas/custom_print.py +146 -0
  26. sf_veritas/django_app.py +5 -0
  27. sf_veritas/env_vars.py +186 -0
  28. sf_veritas/exception_handling_middleware.py +18 -0
  29. sf_veritas/exception_metaclass.py +69 -0
  30. sf_veritas/fast_frame_info.py +116 -0
  31. sf_veritas/fast_network_hop.py +293 -0
  32. sf_veritas/frame_tools.py +112 -0
  33. sf_veritas/funcspan_config_loader.py +556 -0
  34. sf_veritas/function_span_profiler.py +1174 -0
  35. sf_veritas/import_hook.py +62 -0
  36. sf_veritas/infra_details/__init__.py +3 -0
  37. sf_veritas/infra_details/get_infra_details.py +24 -0
  38. sf_veritas/infra_details/kubernetes/__init__.py +3 -0
  39. sf_veritas/infra_details/kubernetes/get_cluster_name.py +147 -0
  40. sf_veritas/infra_details/kubernetes/get_details.py +7 -0
  41. sf_veritas/infra_details/running_on/__init__.py +17 -0
  42. sf_veritas/infra_details/running_on/kubernetes.py +11 -0
  43. sf_veritas/interceptors.py +497 -0
  44. sf_veritas/libsfnettee.so +0 -0
  45. sf_veritas/local_env_detect.py +118 -0
  46. sf_veritas/package_metadata.py +6 -0
  47. sf_veritas/patches/__init__.py +0 -0
  48. sf_veritas/patches/concurrent_futures.py +19 -0
  49. sf_veritas/patches/constants.py +1 -0
  50. sf_veritas/patches/exceptions.py +82 -0
  51. sf_veritas/patches/multiprocessing.py +32 -0
  52. sf_veritas/patches/network_libraries/__init__.py +76 -0
  53. sf_veritas/patches/network_libraries/aiohttp.py +281 -0
  54. sf_veritas/patches/network_libraries/curl_cffi.py +363 -0
  55. sf_veritas/patches/network_libraries/http_client.py +419 -0
  56. sf_veritas/patches/network_libraries/httpcore.py +515 -0
  57. sf_veritas/patches/network_libraries/httplib2.py +204 -0
  58. sf_veritas/patches/network_libraries/httpx.py +515 -0
  59. sf_veritas/patches/network_libraries/niquests.py +211 -0
  60. sf_veritas/patches/network_libraries/pycurl.py +385 -0
  61. sf_veritas/patches/network_libraries/requests.py +633 -0
  62. sf_veritas/patches/network_libraries/tornado.py +341 -0
  63. sf_veritas/patches/network_libraries/treq.py +270 -0
  64. sf_veritas/patches/network_libraries/urllib_request.py +468 -0
  65. sf_veritas/patches/network_libraries/utils.py +398 -0
  66. sf_veritas/patches/os.py +17 -0
  67. sf_veritas/patches/threading.py +218 -0
  68. sf_veritas/patches/web_frameworks/__init__.py +54 -0
  69. sf_veritas/patches/web_frameworks/aiohttp.py +793 -0
  70. sf_veritas/patches/web_frameworks/async_websocket_consumer.py +317 -0
  71. sf_veritas/patches/web_frameworks/blacksheep.py +527 -0
  72. sf_veritas/patches/web_frameworks/bottle.py +502 -0
  73. sf_veritas/patches/web_frameworks/cherrypy.py +678 -0
  74. sf_veritas/patches/web_frameworks/cors_utils.py +122 -0
  75. sf_veritas/patches/web_frameworks/django.py +944 -0
  76. sf_veritas/patches/web_frameworks/eve.py +395 -0
  77. sf_veritas/patches/web_frameworks/falcon.py +926 -0
  78. sf_veritas/patches/web_frameworks/fastapi.py +724 -0
  79. sf_veritas/patches/web_frameworks/flask.py +520 -0
  80. sf_veritas/patches/web_frameworks/klein.py +501 -0
  81. sf_veritas/patches/web_frameworks/litestar.py +551 -0
  82. sf_veritas/patches/web_frameworks/pyramid.py +428 -0
  83. sf_veritas/patches/web_frameworks/quart.py +824 -0
  84. sf_veritas/patches/web_frameworks/robyn.py +697 -0
  85. sf_veritas/patches/web_frameworks/sanic.py +857 -0
  86. sf_veritas/patches/web_frameworks/starlette.py +723 -0
  87. sf_veritas/patches/web_frameworks/strawberry.py +813 -0
  88. sf_veritas/patches/web_frameworks/tornado.py +481 -0
  89. sf_veritas/patches/web_frameworks/utils.py +91 -0
  90. sf_veritas/print_override.py +13 -0
  91. sf_veritas/regular_data_transmitter.py +409 -0
  92. sf_veritas/request_interceptor.py +401 -0
  93. sf_veritas/request_utils.py +550 -0
  94. sf_veritas/server_status.py +1 -0
  95. sf_veritas/shutdown_flag.py +11 -0
  96. sf_veritas/subprocess_startup.py +3 -0
  97. sf_veritas/test_cli.py +145 -0
  98. sf_veritas/thread_local.py +970 -0
  99. sf_veritas/timeutil.py +114 -0
  100. sf_veritas/transmit_exception_to_sailfish.py +28 -0
  101. sf_veritas/transmitter.py +132 -0
  102. sf_veritas/types.py +47 -0
  103. sf_veritas/unified_interceptor.py +1580 -0
  104. sf_veritas/utils.py +39 -0
  105. sf_veritas-0.10.3.dist-info/METADATA +97 -0
  106. sf_veritas-0.10.3.dist-info/RECORD +132 -0
  107. sf_veritas-0.10.3.dist-info/WHEEL +5 -0
  108. sf_veritas-0.10.3.dist-info/entry_points.txt +2 -0
  109. sf_veritas-0.10.3.dist-info/top_level.txt +1 -0
  110. sf_veritas.libs/libbrotlicommon-6ce2a53c.so.1.0.6 +0 -0
  111. sf_veritas.libs/libbrotlidec-811d1be3.so.1.0.6 +0 -0
  112. sf_veritas.libs/libcom_err-730ca923.so.2.1 +0 -0
  113. sf_veritas.libs/libcrypt-52aca757.so.1.1.0 +0 -0
  114. sf_veritas.libs/libcrypto-bdaed0ea.so.1.1.1k +0 -0
  115. sf_veritas.libs/libcurl-eaa3cf66.so.4.5.0 +0 -0
  116. sf_veritas.libs/libgssapi_krb5-323bbd21.so.2.2 +0 -0
  117. sf_veritas.libs/libidn2-2f4a5893.so.0.3.6 +0 -0
  118. sf_veritas.libs/libk5crypto-9a74ff38.so.3.1 +0 -0
  119. sf_veritas.libs/libkeyutils-2777d33d.so.1.6 +0 -0
  120. sf_veritas.libs/libkrb5-a55300e8.so.3.3 +0 -0
  121. sf_veritas.libs/libkrb5support-e6594cfc.so.0.1 +0 -0
  122. sf_veritas.libs/liblber-2-d20824ef.4.so.2.10.9 +0 -0
  123. sf_veritas.libs/libldap-2-cea2a960.4.so.2.10.9 +0 -0
  124. sf_veritas.libs/libnghttp2-39367a22.so.14.17.0 +0 -0
  125. sf_veritas.libs/libpcre2-8-516f4c9d.so.0.7.1 +0 -0
  126. sf_veritas.libs/libpsl-99becdd3.so.5.3.1 +0 -0
  127. sf_veritas.libs/libsasl2-7de4d792.so.3.0.0 +0 -0
  128. sf_veritas.libs/libselinux-d0805dcb.so.1 +0 -0
  129. sf_veritas.libs/libssh-c11d285b.so.4.8.7 +0 -0
  130. sf_veritas.libs/libssl-60250281.so.1.1.1k +0 -0
  131. sf_veritas.libs/libunistring-05abdd40.so.2.1.0 +0 -0
  132. sf_veritas.libs/libuuid-95b83d40.so.1.3.0 +0 -0
@@ -0,0 +1,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
+ )