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,798 @@
1
+ """
2
+ Context-propagation + user-code NetworkHop emission for every aiohttp
3
+ request, while skipping Strawberry GraphQL views.
4
+
5
+ OTEL-STYLE: Emits network hops AFTER handler completes (zero-overhead).
6
+ """
7
+
8
+ import os
9
+ import threading
10
+ from typing import List, Optional
11
+
12
+ from ... import _sffuncspan, _sffuncspan_config, app_config
13
+ from ...constants import (
14
+ FUNCSPAN_OVERRIDE_HEADER_BYTES,
15
+ SAILFISH_TRACING_HEADER,
16
+ SAILFISH_TRACING_HEADER_BYTES,
17
+ )
18
+ from ...custom_excepthook import custom_excepthook
19
+ from ...env_vars import (
20
+ SF_DEBUG,
21
+ SF_NETWORKHOP_CAPTURE_REQUEST_BODY,
22
+ SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS,
23
+ SF_NETWORKHOP_CAPTURE_RESPONSE_BODY,
24
+ SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS,
25
+ SF_NETWORKHOP_REQUEST_LIMIT_MB,
26
+ SF_NETWORKHOP_RESPONSE_LIMIT_MB,
27
+ )
28
+
29
+ # LAZY IMPORT: Import inside function to avoid circular import with _sfnetworkhop
30
+ # from ...fast_network_hop import fast_send_network_hop_fast, register_endpoint
31
+ from ...thread_local import (
32
+ clear_c_tls_parent_trace_id,
33
+ clear_current_request_path,
34
+ clear_funcspan_override,
35
+ clear_outbound_header_base,
36
+ clear_trace_id,
37
+ generate_new_trace_id,
38
+ get_or_set_sf_trace_id,
39
+ get_sf_trace_id,
40
+ set_current_request_path,
41
+ set_funcspan_override,
42
+ set_outbound_header_base,
43
+ )
44
+ from .cors_utils import inject_sailfish_headers, should_inject_headers
45
+
46
+ # Size limits in bytes
47
+ _REQUEST_LIMIT_BYTES = SF_NETWORKHOP_REQUEST_LIMIT_MB * 1024 * 1024
48
+ _RESPONSE_LIMIT_BYTES = SF_NETWORKHOP_RESPONSE_LIMIT_MB * 1024 * 1024
49
+
50
+ # Pre-registered endpoint IDs (maps (filename, lineno) tuple -> endpoint_id from C extension)
51
+ _ENDPOINT_REGISTRY: dict[tuple, int] = {}
52
+
53
+ # Track which aiohttp app instances have been registered (to support multiple apps)
54
+ _REGISTERED_APPS: set[int] = set()
55
+
56
+ # Routes to skip (set by patch_aiohttp)
57
+ _ROUTES_TO_SKIP = []
58
+
59
+ # ------------------------------------------------------------------ #
60
+ # shared helpers
61
+ # ------------------------------------------------------------------ #
62
+ from .utils import _is_user_code, _unwrap_user_func, should_skip_route, reinitialize_log_print_capture_for_worker # cached
63
+
64
+
65
+ def _should_trace_endpoint(handler) -> bool:
66
+ """Check if endpoint should be traced."""
67
+ real_fn = _unwrap_user_func(handler)
68
+ if not callable(real_fn):
69
+ return False
70
+
71
+ if getattr(real_fn, "__module__", "").startswith("strawberry"):
72
+ return False
73
+
74
+ code = getattr(real_fn, "__code__", None)
75
+ if not code:
76
+ return False
77
+
78
+ if not _is_user_code(code.co_filename):
79
+ return False
80
+
81
+ return True
82
+
83
+
84
+ def _pre_register_endpoints(app, routes_to_skip: Optional[List[str]] = None):
85
+ """Pre-register all endpoints at startup."""
86
+ # LAZY IMPORT: Import here to avoid circular import with _sfnetworkhop
87
+ from ...fast_network_hop import register_endpoint
88
+
89
+ routes_to_skip = routes_to_skip or []
90
+ count = 0
91
+ skipped = 0
92
+
93
+ app_id = id(app)
94
+
95
+ # Check if this app has already been registered
96
+ if app_id in _REGISTERED_APPS:
97
+ if SF_DEBUG and app_config._interceptors_initialized:
98
+ print(
99
+ f"[[_pre_register_endpoints]] App {app_id} already registered, skipping",
100
+ log=False,
101
+ )
102
+ return
103
+
104
+ if SF_DEBUG and app_config._interceptors_initialized:
105
+ print(
106
+ f"[[_pre_register_endpoints]] Starting registration for app {app_id}",
107
+ log=False,
108
+ )
109
+
110
+ # Iterate through all routes in the router
111
+ if not hasattr(app, "router") or not hasattr(app.router, "_resources"):
112
+ if SF_DEBUG and app_config._interceptors_initialized:
113
+ print(
114
+ f"[[_pre_register_endpoints]] App has no router or resources, skipping",
115
+ log=False,
116
+ )
117
+ return
118
+
119
+ for resource in app.router._resources:
120
+ if not hasattr(resource, "_routes"):
121
+ continue
122
+
123
+ for route in resource._routes:
124
+ if not hasattr(route, "_handler"):
125
+ skipped += 1
126
+ continue
127
+
128
+ handler = route._handler
129
+ if not _should_trace_endpoint(handler):
130
+ skipped += 1
131
+ continue
132
+
133
+ unwrapped = _unwrap_user_func(handler)
134
+ code = unwrapped.__code__
135
+ key = (code.co_filename, code.co_firstlineno)
136
+
137
+ # Check if this specific endpoint is already registered
138
+ if key in _ENDPOINT_REGISTRY:
139
+ continue
140
+
141
+ # Extract route pattern
142
+ route_pattern = None
143
+ if hasattr(resource, "_path"):
144
+ route_pattern = resource._path
145
+ elif hasattr(resource, "canonical"):
146
+ route_pattern = resource.canonical
147
+
148
+ # Check if route should be skipped based on wildcard patterns
149
+ if route_pattern and should_skip_route(route_pattern, routes_to_skip):
150
+ skipped += 1
151
+ if SF_DEBUG and app_config._interceptors_initialized:
152
+ print(
153
+ f"[[_pre_register_endpoints]] Skipping endpoint (route matches skip pattern): {route_pattern}",
154
+ log=False,
155
+ )
156
+ continue
157
+
158
+ endpoint_id = register_endpoint(
159
+ line=str(code.co_firstlineno),
160
+ column="0",
161
+ name=unwrapped.__name__,
162
+ entrypoint=code.co_filename,
163
+ route=route_pattern,
164
+ )
165
+
166
+ if endpoint_id < 0:
167
+ if SF_DEBUG and app_config._interceptors_initialized:
168
+ print(
169
+ f"[[_pre_register_endpoints]] Failed to register {unwrapped.__name__} (endpoint_id={endpoint_id})",
170
+ log=False,
171
+ )
172
+ continue
173
+
174
+ _ENDPOINT_REGISTRY[key] = endpoint_id
175
+ count += 1
176
+
177
+ if SF_DEBUG and app_config._interceptors_initialized:
178
+ print(
179
+ f"[[patch_aiohttp]] Registered: {unwrapped.__name__} @ {code.co_filename}:{code.co_firstlineno} route={route_pattern} (endpoint_id={endpoint_id})",
180
+ log=False,
181
+ )
182
+
183
+ if SF_DEBUG and app_config._interceptors_initialized:
184
+ print(f"[[patch_aiohttp]] Total endpoints registered: {count}", log=False)
185
+
186
+ # Only mark this app as registered if we actually registered user endpoints
187
+ if count > 0:
188
+ _REGISTERED_APPS.add(app_id)
189
+ if SF_DEBUG and app_config._interceptors_initialized:
190
+ print(f"[[patch_aiohttp]] App {app_id} marked as registered", log=False)
191
+ else:
192
+ if SF_DEBUG and app_config._interceptors_initialized:
193
+ print(
194
+ f"[[patch_aiohttp]] No user endpoints registered yet for app {app_id}",
195
+ log=False,
196
+ )
197
+
198
+
199
+ # ------------------------------------------------------------------ #
200
+ # monkey-patch
201
+ # ------------------------------------------------------------------ #
202
+ def patch_aiohttp(routes_to_skip: Optional[List[str]] = None):
203
+ """
204
+ OTEL-STYLE PURE ASYNC:
205
+ • Prepends middleware that propagates SAILFISH_TRACING_HEADER
206
+ • Captures endpoint metadata before handler
207
+ • Emits NetworkHop AFTER handler completes (zero-overhead)
208
+ • Patches Application.add_route(s) for RouteTableDef support
209
+ Safe no-op if aiohttp isn't installed.
210
+ """
211
+ global _ROUTES_TO_SKIP
212
+ _ROUTES_TO_SKIP = routes_to_skip or []
213
+
214
+ try:
215
+ from aiohttp import web
216
+ except ImportError: # aiohttp missing
217
+ return
218
+
219
+ # ===========================================================
220
+ # 1 | OTEL-STYLE Middleware
221
+ # ===========================================================
222
+ @web.middleware
223
+ async def _sf_tracing_middleware(request: web.Request, handler):
224
+ """
225
+ OTEL-STYLE aiohttp middleware that:
226
+ 1 - Seed ContextVar from the inbound SAILFISH_TRACING_HEADER header.
227
+ 2 - Capture request headers/body if enabled.
228
+ 3 - Captures endpoint metadata and register endpoint.
229
+ 4 - Call handler and capture exceptions.
230
+ 5 - Capture response headers/body if enabled.
231
+ 6 - Emits NetworkHop AFTER handler completes (OTEL-style zero-overhead).
232
+ """
233
+ # Set current request path for route-based suppression (SF_DISABLE_INBOUND_NETWORK_TRACING_ON_ROUTES)
234
+ set_current_request_path(request.path)
235
+
236
+ # LAZY IMPORT: Import here to avoid circular import with _sfnetworkhop
237
+ from ...fast_network_hop import fast_send_network_hop_fast, register_endpoint
238
+
239
+ # PERFORMANCE: Single-pass bytes-level header scan (no dict allocation until needed)
240
+ # Scan headers once on bytes, only decode what we need, use latin-1 (fast 1:1 byte map)
241
+ # aiohttp uses CIMultiDict for headers, need to iterate raw tuples
242
+ hdr_items = request.headers.items() if hasattr(request.headers, "items") else []
243
+ incoming_trace_raw = None # bytes or str
244
+ funcspan_raw = None # bytes or str
245
+ req_headers = None # dict[str,str] only if capture enabled
246
+
247
+ capture_req_headers = SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS # local cache
248
+
249
+ if capture_req_headers:
250
+ # build the dict while we're scanning
251
+ tmp = {}
252
+ for k, v in hdr_items:
253
+ # aiohttp headers are already strings, check as string first
254
+ if isinstance(k, str):
255
+ kl = k.lower()
256
+ if kl == SAILFISH_TRACING_HEADER.lower():
257
+ incoming_trace_raw = v
258
+ elif kl == "x-sf3-functionspancaptureoverride":
259
+ funcspan_raw = v
260
+ tmp[k] = v
261
+ else:
262
+ # fallback for bytes
263
+ kl = k.lower() if isinstance(k, bytes) else k.encode().lower()
264
+ if kl == SAILFISH_TRACING_HEADER_BYTES:
265
+ incoming_trace_raw = v
266
+ elif kl == FUNCSPAN_OVERRIDE_HEADER_BYTES:
267
+ funcspan_raw = v
268
+ tmp[k.decode("latin-1") if isinstance(k, bytes) else k] = (
269
+ v.decode("latin-1") if isinstance(v, bytes) else v
270
+ )
271
+ req_headers = tmp
272
+ else:
273
+ for k, v in hdr_items:
274
+ if isinstance(k, str):
275
+ kl = k.lower()
276
+ if kl == SAILFISH_TRACING_HEADER.lower():
277
+ incoming_trace_raw = v
278
+ elif kl == "x-sf3-functionspancaptureoverride":
279
+ funcspan_raw = v
280
+ else:
281
+ kl = k.lower() if isinstance(k, bytes) else k.encode().lower()
282
+ if kl == SAILFISH_TRACING_HEADER_BYTES:
283
+ incoming_trace_raw = v
284
+ elif kl == FUNCSPAN_OVERRIDE_HEADER_BYTES:
285
+ funcspan_raw = v
286
+
287
+ # 1. CRITICAL: Seed/ensure trace_id immediately (BEFORE any outbound work)
288
+ if incoming_trace_raw:
289
+ # Incoming X-Sf3-Rid header provided - use it
290
+ incoming_trace = (
291
+ incoming_trace_raw
292
+ if isinstance(incoming_trace_raw, str)
293
+ else incoming_trace_raw.decode("latin-1")
294
+ )
295
+ get_or_set_sf_trace_id(
296
+ incoming_trace, is_associated_with_inbound_request=True
297
+ )
298
+ else:
299
+ # No incoming X-Sf3-Rid header - generate fresh trace_id for this request
300
+ generate_new_trace_id()
301
+
302
+ # Optional funcspan override (decode only if present)
303
+ funcspan_override_header = None
304
+ if funcspan_raw:
305
+ funcspan_override_header = (
306
+ funcspan_raw
307
+ if isinstance(funcspan_raw, str)
308
+ else funcspan_raw.decode("latin-1")
309
+ )
310
+ try:
311
+ set_funcspan_override(funcspan_override_header)
312
+ if SF_DEBUG and app_config._interceptors_initialized:
313
+ print(
314
+ f"[[aiohttp.middleware]] Set function span override from header: {funcspan_override_header}",
315
+ log=False,
316
+ )
317
+ except Exception as e:
318
+ if SF_DEBUG and app_config._interceptors_initialized:
319
+ print(
320
+ f"[[aiohttp.middleware]] Failed to set function span override: {e}",
321
+ log=False,
322
+ )
323
+
324
+ # Initialize outbound base without list/allocs from split()
325
+ try:
326
+ trace_id = get_sf_trace_id()
327
+ if trace_id:
328
+ s = str(trace_id)
329
+ i = s.find("/") # session
330
+ j = s.find("/", i + 1) if i != -1 else -1 # page
331
+ if j != -1:
332
+ base_trace = s[:j] # "session/page"
333
+ set_outbound_header_base(
334
+ base_trace=base_trace,
335
+ parent_trace_id=s, # "session/page/uuid"
336
+ funcspan=funcspan_override_header,
337
+ )
338
+ if SF_DEBUG and app_config._interceptors_initialized:
339
+ print(
340
+ f"[[aiohttp.middleware]] Initialized outbound header base (base={base_trace[:16]}...)",
341
+ log=False,
342
+ )
343
+ except Exception as e:
344
+ if SF_DEBUG and app_config._interceptors_initialized:
345
+ print(
346
+ f"[[aiohttp.middleware]] Failed to initialize outbound header base: {e}",
347
+ log=False,
348
+ )
349
+
350
+ # 3. Capture request body if enabled
351
+ req_body = None
352
+ if SF_NETWORKHOP_CAPTURE_REQUEST_BODY:
353
+ try:
354
+ # aiohttp Request.read() returns the body
355
+ body = await request.read()
356
+ req_body = body[:_REQUEST_LIMIT_BYTES] if body else None
357
+ if SF_DEBUG and app_config._interceptors_initialized:
358
+ print(
359
+ f"[[aiohttp]] Request body capture: {len(body) if body else 0} bytes (method={request.method})",
360
+ log=False,
361
+ )
362
+ except Exception as e:
363
+ if SF_DEBUG and app_config._interceptors_initialized:
364
+ print(f"[[aiohttp]] Failed to capture request body: {e}", log=False)
365
+
366
+ # 4. OTEL-STYLE: Capture endpoint metadata and register endpoint
367
+ endpoint_id = None
368
+ real_fn = _unwrap_user_func(handler)
369
+ if callable(real_fn) and not real_fn.__module__.startswith("strawberry"):
370
+ code = getattr(real_fn, "__code__", None)
371
+ if code and _is_user_code(code.co_filename):
372
+ key = (code.co_filename, code.co_firstlineno)
373
+ sent = request.setdefault("sf_hops_sent", set())
374
+ if key not in sent:
375
+ fname = code.co_filename
376
+ lno = code.co_firstlineno
377
+ fname_str = real_fn.__name__
378
+
379
+ # Get route pattern if available
380
+ route_pattern = getattr(request.match_info, "route", None)
381
+ route_str = str(
382
+ route_pattern.resource.canonical if route_pattern else None
383
+ )
384
+
385
+ # Check if route should be skipped
386
+ if route_str and should_skip_route(route_str, _ROUTES_TO_SKIP):
387
+ if SF_DEBUG and app_config._interceptors_initialized:
388
+ print(
389
+ f"[[Aiohttp]] Skipping endpoint (route matches skip pattern): {route_str}",
390
+ log=False,
391
+ )
392
+ return
393
+
394
+ # Get or register endpoint
395
+ endpoint_id = _ENDPOINT_REGISTRY.get(key)
396
+ if endpoint_id is None:
397
+ endpoint_id = register_endpoint(
398
+ line=str(lno),
399
+ column="0",
400
+ name=fname_str,
401
+ entrypoint=fname,
402
+ route=route_str,
403
+ )
404
+ if endpoint_id >= 0:
405
+ _ENDPOINT_REGISTRY[key] = endpoint_id
406
+ if SF_DEBUG and app_config._interceptors_initialized:
407
+ print(
408
+ f"[[aiohttp]] Registered endpoint: {fname_str} @ {fname}:{lno} (id={endpoint_id})",
409
+ log=False,
410
+ )
411
+
412
+ sent.add(key)
413
+ request["sf_endpoint_id"] = endpoint_id
414
+
415
+ if SF_DEBUG and app_config._interceptors_initialized:
416
+ print(
417
+ f"[[aiohttp]] Captured endpoint: {fname_str} ({fname}:{lno}) endpoint_id={endpoint_id}",
418
+ log=False,
419
+ )
420
+
421
+ # 5. Call handler and capture exceptions (with cleanup in finally)
422
+ try:
423
+ try:
424
+ response = await handler(request)
425
+ except Exception as exc: # ← captures *all* errors
426
+ custom_excepthook(type(exc), exc, exc.__traceback__)
427
+ raise # re-raise for aiohttp
428
+
429
+ # 6. Capture response headers if enabled
430
+ resp_headers = None
431
+ if SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS and endpoint_id is not None:
432
+ try:
433
+ resp_headers = dict(response.headers)
434
+ if SF_DEBUG and app_config._interceptors_initialized:
435
+ print(
436
+ f"[[aiohttp]] Captured response headers: {len(resp_headers)} headers",
437
+ log=False,
438
+ )
439
+ except Exception as e:
440
+ if SF_DEBUG and app_config._interceptors_initialized:
441
+ print(
442
+ f"[[aiohttp]] Failed to capture response headers: {e}",
443
+ log=False,
444
+ )
445
+
446
+ # 7. Capture response body if enabled
447
+ resp_body = None
448
+ if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY and endpoint_id is not None:
449
+ try:
450
+ # aiohttp Response has body or text attribute
451
+ if hasattr(response, "body") and response.body:
452
+ if isinstance(response.body, bytes):
453
+ resp_body = response.body[:_RESPONSE_LIMIT_BYTES]
454
+ if SF_DEBUG and app_config._interceptors_initialized:
455
+ print(
456
+ f"[[aiohttp]] Captured from body (bytes): {len(resp_body)} bytes",
457
+ log=False,
458
+ )
459
+ elif isinstance(response.body, str):
460
+ resp_body = response.body.encode("utf-8")[
461
+ :_RESPONSE_LIMIT_BYTES
462
+ ]
463
+ if SF_DEBUG and app_config._interceptors_initialized:
464
+ print(
465
+ f"[[aiohttp]] Captured from body (str): {len(resp_body)} bytes",
466
+ log=False,
467
+ )
468
+ elif hasattr(response, "text") and response.text:
469
+ resp_body = response.text.encode("utf-8")[
470
+ :_RESPONSE_LIMIT_BYTES
471
+ ]
472
+ if SF_DEBUG and app_config._interceptors_initialized:
473
+ print(
474
+ f"[[aiohttp]] Captured from text: {len(resp_body)} bytes",
475
+ log=False,
476
+ )
477
+
478
+ if SF_DEBUG and not resp_body:
479
+ print(f"[[aiohttp]] No response body captured", log=False)
480
+ except Exception as e:
481
+ if SF_DEBUG and app_config._interceptors_initialized:
482
+ print(
483
+ f"[[aiohttp]] Failed to capture response body: {e}",
484
+ log=False,
485
+ )
486
+
487
+ # 8. OTEL-STYLE: Emit network hop AFTER handler completes
488
+ if endpoint_id is not None and endpoint_id >= 0:
489
+ try:
490
+ _, session_id = get_or_set_sf_trace_id()
491
+
492
+ # Extract raw path and query string for C to parse
493
+ raw_path = str(request.path) # e.g., "/log"
494
+ raw_query = (
495
+ request.query_string.encode("utf-8")
496
+ if request.query_string
497
+ else b""
498
+ ) # e.g., b"foo=5"
499
+
500
+ if SF_DEBUG and app_config._interceptors_initialized:
501
+ print(
502
+ f"[[aiohttp]] About to emit network hop: endpoint_id={endpoint_id}, "
503
+ f"req_headers={'present' if req_headers else 'None'}, "
504
+ f"req_body={len(req_body) if req_body else 0} bytes, "
505
+ f"resp_headers={'present' if resp_headers else 'None'}, "
506
+ f"resp_body={len(resp_body) if resp_body else 0} bytes",
507
+ log=False,
508
+ )
509
+
510
+ # Direct C call - queues to background worker, returns instantly
511
+ # C will parse route and query_params from raw data
512
+ fast_send_network_hop_fast(
513
+ session_id=session_id,
514
+ endpoint_id=endpoint_id,
515
+ raw_path=raw_path,
516
+ raw_query_string=raw_query,
517
+ request_headers=req_headers,
518
+ request_body=req_body,
519
+ response_headers=resp_headers,
520
+ response_body=resp_body,
521
+ )
522
+
523
+ if SF_DEBUG and app_config._interceptors_initialized:
524
+ print(
525
+ f"[[aiohttp]] Emitted network hop: endpoint_id={endpoint_id} "
526
+ f"session={session_id}",
527
+ log=False,
528
+ )
529
+ except Exception as e: # noqa: BLE001 S110
530
+ if SF_DEBUG and app_config._interceptors_initialized:
531
+ print(f"[[aiohttp]] Failed to emit network hop: {e}", log=False)
532
+
533
+ return response
534
+ finally:
535
+ # CRITICAL: Clear C TLS to prevent stale data in thread pools
536
+ # This runs even if handler raises exception!
537
+ clear_c_tls_parent_trace_id()
538
+
539
+ # CRITICAL: Clear outbound header base to prevent stale cached headers
540
+ # ContextVar does NOT automatically clean up in thread pools - must clear explicitly
541
+ clear_outbound_header_base()
542
+
543
+ # CRITICAL: Clear trace_id to ensure fresh generation for next request
544
+ # Without this, get_or_set_sf_trace_id() reuses trace_id from previous request
545
+ # causing X-Sf4-Prid to stay constant when no incoming X-Sf3-Rid header
546
+ clear_trace_id()
547
+
548
+ # CRITICAL: Clear current request path to prevent stale data in thread pools
549
+ clear_current_request_path()
550
+
551
+ # Clear function span override for this request (ContextVar cleanup - also syncs C thread-local)
552
+ try:
553
+ clear_funcspan_override()
554
+ except Exception:
555
+ pass
556
+
557
+ # ===========================================================
558
+ # 2 | Patch Application.__init__ to insert middleware
559
+ # ===========================================================
560
+ original_init = web.Application.__init__
561
+
562
+ def patched_init(self, *args, middlewares=None, **kwargs):
563
+ mlist = list(middlewares or [])
564
+ mlist.insert(0, _sf_tracing_middleware) # prepend → runs first
565
+ original_init(self, *args, middlewares=mlist, **kwargs)
566
+ _patch_router(self.router) # apply once per app
567
+
568
+ # Try to register endpoints immediately if routes are already defined
569
+ if (
570
+ hasattr(self, "router")
571
+ and hasattr(self.router, "_resources")
572
+ and self.router._resources
573
+ ):
574
+ try:
575
+ if SF_DEBUG and app_config._interceptors_initialized:
576
+ print(
577
+ f"[[patch_aiohttp]] Routes already defined, registering immediately",
578
+ log=False,
579
+ )
580
+ _pre_register_endpoints(self, routes_to_skip)
581
+ except Exception as e:
582
+ if SF_DEBUG and app_config._interceptors_initialized:
583
+ print(
584
+ f"[[patch_aiohttp]] Immediate registration failed: {e}",
585
+ log=False,
586
+ )
587
+
588
+ # Register on_startup hook as a fallback (for routes added after __init__)
589
+ async def _sf_startup(app):
590
+ # Note: Profiler is already installed by unified_interceptor.py
591
+
592
+ if SF_DEBUG and app_config._interceptors_initialized:
593
+ print(
594
+ "[[patch_aiohttp]] Startup event fired, registering endpoints",
595
+ log=False,
596
+ )
597
+ _pre_register_endpoints(app, routes_to_skip)
598
+
599
+ self.on_startup.append(_sf_startup)
600
+
601
+ web.Application.__init__ = patched_init
602
+
603
+ # ===========================================================
604
+ # 3 | Patch router.add_route / add_routes for future calls
605
+ # ===========================================================
606
+ def _patch_router(router):
607
+ if getattr(router, "_sf_tracing_patched", False):
608
+ return # already done
609
+
610
+ orig_add_route = router.add_route
611
+ orig_add_routes = router.add_routes
612
+
613
+ def _wrap_and_add(method, path, handler, *a, **kw): # noqa: ANN001
614
+ return orig_add_route(method, path, _wrap_handler(handler), *a, **kw)
615
+
616
+ def _wrap_handler(h):
617
+ # strawberry skip & user-code check happen in middleware,
618
+ # but wrapping here avoids duplicate stack frames
619
+ return _unwrap_user_func(h) or h
620
+
621
+ def _new_add_routes(routes):
622
+ wrapped = [
623
+ (
624
+ (m, p, _wrap_handler(h), *rest) # route is (method,path,handler,…)
625
+ if len(r) >= 3
626
+ else r
627
+ )
628
+ for r in routes
629
+ for (m, p, h, *rest) in (r,) # unpack safely
630
+ ]
631
+ return orig_add_routes(wrapped)
632
+
633
+ router.add_route = _wrap_and_add
634
+ router.add_routes = _new_add_routes
635
+ router._sf_tracing_patched = True
636
+ if SF_DEBUG and app_config._interceptors_initialized:
637
+ print("[[patch_aiohttp]] router hooks installed", log=False)
638
+
639
+ if SF_DEBUG and app_config._interceptors_initialized:
640
+ print("[[patch_aiohttp]] OTEL-style middleware + init patch applied", log=False)
641
+
642
+ # ===========================================================
643
+ # 4 | Patch aiohttp-cors if installed
644
+ # ===========================================================
645
+ def patch_aiohttp_cors():
646
+ """
647
+ Patch aiohttp-cors to automatically inject Sailfish headers.
648
+
649
+ SAFE: Only modifies CORS if aiohttp-cors is installed and used.
650
+ """
651
+ try:
652
+ import aiohttp_cors
653
+ except ImportError:
654
+ # aiohttp-cors not installed, skip patching
655
+ if SF_DEBUG and app_config._interceptors_initialized:
656
+ print(
657
+ "[[patch_aiohttp_cors]] aiohttp-cors not installed, skipping",
658
+ log=False,
659
+ )
660
+ return
661
+
662
+ # Check if already patched
663
+ if hasattr(aiohttp_cors.CorsConfig, "_sf_cors_patched"):
664
+ if SF_DEBUG and app_config._interceptors_initialized:
665
+ print("[[patch_aiohttp_cors]] Already patched, skipping", log=False)
666
+ return
667
+
668
+ # Patch CorsConfig.__init__ to intercept defaults parameter
669
+ original_init = aiohttp_cors.CorsConfig.__init__
670
+
671
+ def patched_init(self, app, *, defaults=None, router_adapter=None):
672
+ # Intercept and modify defaults parameter
673
+ if defaults:
674
+ modified_defaults = {}
675
+ for origin, resource_options in defaults.items():
676
+ # Handle both ResourceOptions objects and dicts
677
+ if isinstance(resource_options, aiohttp_cors.ResourceOptions):
678
+ # ResourceOptions object - access allow_headers attribute
679
+ if hasattr(resource_options, "allow_headers"):
680
+ original_headers = resource_options.allow_headers
681
+ if should_inject_headers(original_headers):
682
+ # Create new ResourceOptions with modified headers
683
+ # Convert frozenset to list for allow_methods and expose_headers
684
+ allow_methods = resource_options.allow_methods
685
+ if isinstance(allow_methods, frozenset):
686
+ allow_methods = list(allow_methods)
687
+
688
+ expose_headers = resource_options.expose_headers
689
+ if isinstance(expose_headers, frozenset):
690
+ expose_headers = list(expose_headers)
691
+
692
+ modified_defaults[origin] = aiohttp_cors.ResourceOptions(
693
+ allow_credentials=resource_options.allow_credentials,
694
+ expose_headers=expose_headers,
695
+ allow_headers=inject_sailfish_headers(original_headers),
696
+ allow_methods=allow_methods,
697
+ max_age=resource_options.max_age,
698
+ )
699
+ if SF_DEBUG and app_config._interceptors_initialized:
700
+ print(
701
+ f"[[patch_aiohttp_cors]] Injected Sailfish headers into defaults for origin {origin}",
702
+ log=False,
703
+ )
704
+ else:
705
+ modified_defaults[origin] = resource_options
706
+ else:
707
+ modified_defaults[origin] = resource_options
708
+ elif isinstance(resource_options, dict) and "allow_headers" in resource_options:
709
+ # Dict config - modify directly
710
+ original_headers = resource_options["allow_headers"]
711
+ if should_inject_headers(original_headers):
712
+ modified_config = resource_options.copy()
713
+ modified_config["allow_headers"] = inject_sailfish_headers(original_headers)
714
+ modified_defaults[origin] = modified_config
715
+ if SF_DEBUG and app_config._interceptors_initialized:
716
+ print(
717
+ f"[[patch_aiohttp_cors]] Injected Sailfish headers into defaults dict for origin {origin}",
718
+ log=False,
719
+ )
720
+ else:
721
+ modified_defaults[origin] = resource_options
722
+ else:
723
+ modified_defaults[origin] = resource_options
724
+
725
+ defaults = modified_defaults
726
+
727
+ # Call original init with modified defaults
728
+ original_init(self, app, defaults=defaults, router_adapter=router_adapter)
729
+
730
+ aiohttp_cors.CorsConfig.__init__ = patched_init
731
+
732
+ # Patch CorsConfig.add method (for route-specific overrides)
733
+ original_add = aiohttp_cors.CorsConfig.add
734
+
735
+ def patched_add(self, route, config=None):
736
+ # Intercept the config and modify allow_headers
737
+ if config:
738
+ modified_config = {}
739
+ for origin, resource_config in config.items():
740
+ if isinstance(resource_config, aiohttp_cors.ResourceOptions):
741
+ # ResourceOptions object
742
+ if hasattr(resource_config, "allow_headers"):
743
+ original_headers = resource_config.allow_headers
744
+ if should_inject_headers(original_headers):
745
+ # Convert frozenset to list for allow_methods and expose_headers
746
+ allow_methods = resource_config.allow_methods
747
+ if isinstance(allow_methods, frozenset):
748
+ allow_methods = list(allow_methods)
749
+
750
+ expose_headers = resource_config.expose_headers
751
+ if isinstance(expose_headers, frozenset):
752
+ expose_headers = list(expose_headers)
753
+
754
+ modified_config[origin] = aiohttp_cors.ResourceOptions(
755
+ allow_credentials=resource_config.allow_credentials,
756
+ expose_headers=expose_headers,
757
+ allow_headers=inject_sailfish_headers(original_headers),
758
+ allow_methods=allow_methods,
759
+ max_age=resource_config.max_age,
760
+ )
761
+ if SF_DEBUG and app_config._interceptors_initialized:
762
+ print(
763
+ "[[patch_aiohttp_cors]] Injected Sailfish headers into CorsConfig.add()",
764
+ log=False,
765
+ )
766
+ else:
767
+ modified_config[origin] = resource_config
768
+ else:
769
+ modified_config[origin] = resource_config
770
+ elif isinstance(resource_config, dict) and "allow_headers" in resource_config:
771
+ original_headers = resource_config["allow_headers"]
772
+ if should_inject_headers(original_headers):
773
+ modified = resource_config.copy()
774
+ modified["allow_headers"] = inject_sailfish_headers(original_headers)
775
+ modified_config[origin] = modified
776
+ if SF_DEBUG and app_config._interceptors_initialized:
777
+ print(
778
+ "[[patch_aiohttp_cors]] Injected Sailfish headers into CorsConfig.add()",
779
+ log=False,
780
+ )
781
+ else:
782
+ modified_config[origin] = resource_config
783
+ else:
784
+ modified_config[origin] = resource_config
785
+
786
+ config = modified_config
787
+
788
+ # Call original add
789
+ return original_add(self, route, config)
790
+
791
+ aiohttp_cors.CorsConfig.add = patched_add
792
+ aiohttp_cors.CorsConfig._sf_cors_patched = True
793
+
794
+ if SF_DEBUG and app_config._interceptors_initialized:
795
+ print("[[patch_aiohttp_cors]] Successfully patched aiohttp-cors", log=False)
796
+
797
+ # Call CORS patching
798
+ patch_aiohttp_cors()