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.
- sf_veritas/__init__.py +46 -0
- sf_veritas/_auto_preload.py +73 -0
- sf_veritas/_sfconfig.c +162 -0
- sf_veritas/_sfconfig.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfcrashhandler.c +267 -0
- sf_veritas/_sfcrashhandler.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastlog.c +953 -0
- sf_veritas/_sffastlog.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnet.c +994 -0
- sf_veritas/_sffastnet.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnetworkrequest.c +727 -0
- sf_veritas/_sffastnetworkrequest.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan.c +2791 -0
- sf_veritas/_sffuncspan.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan_config.c +730 -0
- sf_veritas/_sffuncspan_config.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfheadercheck.c +341 -0
- sf_veritas/_sfheadercheck.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfnetworkhop.c +1454 -0
- sf_veritas/_sfnetworkhop.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfservice.c +1223 -0
- sf_veritas/_sfservice.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfteepreload.c +6227 -0
- sf_veritas/app_config.py +57 -0
- sf_veritas/cli.py +336 -0
- sf_veritas/constants.py +10 -0
- sf_veritas/custom_excepthook.py +304 -0
- sf_veritas/custom_log_handler.py +146 -0
- sf_veritas/custom_output_wrapper.py +153 -0
- sf_veritas/custom_print.py +153 -0
- sf_veritas/django_app.py +5 -0
- sf_veritas/env_vars.py +186 -0
- sf_veritas/exception_handling_middleware.py +18 -0
- sf_veritas/exception_metaclass.py +69 -0
- sf_veritas/fast_frame_info.py +116 -0
- sf_veritas/fast_network_hop.py +293 -0
- sf_veritas/frame_tools.py +112 -0
- sf_veritas/funcspan_config_loader.py +693 -0
- sf_veritas/function_span_profiler.py +1313 -0
- sf_veritas/get_preload_path.py +34 -0
- sf_veritas/import_hook.py +62 -0
- sf_veritas/infra_details/__init__.py +3 -0
- sf_veritas/infra_details/get_infra_details.py +24 -0
- sf_veritas/infra_details/kubernetes/__init__.py +3 -0
- sf_veritas/infra_details/kubernetes/get_cluster_name.py +147 -0
- sf_veritas/infra_details/kubernetes/get_details.py +7 -0
- sf_veritas/infra_details/running_on/__init__.py +17 -0
- sf_veritas/infra_details/running_on/kubernetes.py +11 -0
- sf_veritas/interceptors.py +543 -0
- sf_veritas/libsfnettee.so +0 -0
- sf_veritas/local_env_detect.py +118 -0
- sf_veritas/package_metadata.py +6 -0
- sf_veritas/patches/__init__.py +0 -0
- sf_veritas/patches/_patch_tracker.py +74 -0
- sf_veritas/patches/concurrent_futures.py +19 -0
- sf_veritas/patches/constants.py +1 -0
- sf_veritas/patches/exceptions.py +82 -0
- sf_veritas/patches/multiprocessing.py +32 -0
- sf_veritas/patches/network_libraries/__init__.py +99 -0
- sf_veritas/patches/network_libraries/aiohttp.py +294 -0
- sf_veritas/patches/network_libraries/curl_cffi.py +363 -0
- sf_veritas/patches/network_libraries/http_client.py +670 -0
- sf_veritas/patches/network_libraries/httpcore.py +580 -0
- sf_veritas/patches/network_libraries/httplib2.py +315 -0
- sf_veritas/patches/network_libraries/httpx.py +557 -0
- sf_veritas/patches/network_libraries/niquests.py +218 -0
- sf_veritas/patches/network_libraries/pycurl.py +399 -0
- sf_veritas/patches/network_libraries/requests.py +595 -0
- sf_veritas/patches/network_libraries/ssl_socket.py +822 -0
- sf_veritas/patches/network_libraries/tornado.py +360 -0
- sf_veritas/patches/network_libraries/treq.py +270 -0
- sf_veritas/patches/network_libraries/urllib_request.py +483 -0
- sf_veritas/patches/network_libraries/utils.py +598 -0
- sf_veritas/patches/os.py +17 -0
- sf_veritas/patches/threading.py +231 -0
- sf_veritas/patches/web_frameworks/__init__.py +54 -0
- sf_veritas/patches/web_frameworks/aiohttp.py +798 -0
- sf_veritas/patches/web_frameworks/async_websocket_consumer.py +337 -0
- sf_veritas/patches/web_frameworks/blacksheep.py +532 -0
- sf_veritas/patches/web_frameworks/bottle.py +513 -0
- sf_veritas/patches/web_frameworks/cherrypy.py +683 -0
- sf_veritas/patches/web_frameworks/cors_utils.py +122 -0
- sf_veritas/patches/web_frameworks/django.py +963 -0
- sf_veritas/patches/web_frameworks/eve.py +401 -0
- sf_veritas/patches/web_frameworks/falcon.py +931 -0
- sf_veritas/patches/web_frameworks/fastapi.py +738 -0
- sf_veritas/patches/web_frameworks/flask.py +526 -0
- sf_veritas/patches/web_frameworks/klein.py +501 -0
- sf_veritas/patches/web_frameworks/litestar.py +616 -0
- sf_veritas/patches/web_frameworks/pyramid.py +440 -0
- sf_veritas/patches/web_frameworks/quart.py +841 -0
- sf_veritas/patches/web_frameworks/robyn.py +708 -0
- sf_veritas/patches/web_frameworks/sanic.py +874 -0
- sf_veritas/patches/web_frameworks/starlette.py +742 -0
- sf_veritas/patches/web_frameworks/strawberry.py +1446 -0
- sf_veritas/patches/web_frameworks/tornado.py +485 -0
- sf_veritas/patches/web_frameworks/utils.py +170 -0
- sf_veritas/print_override.py +13 -0
- sf_veritas/regular_data_transmitter.py +444 -0
- sf_veritas/request_interceptor.py +401 -0
- sf_veritas/request_utils.py +550 -0
- sf_veritas/segfault_handler.py +116 -0
- sf_veritas/server_status.py +1 -0
- sf_veritas/shutdown_flag.py +11 -0
- sf_veritas/subprocess_startup.py +3 -0
- sf_veritas/test_cli.py +145 -0
- sf_veritas/thread_local.py +1319 -0
- sf_veritas/timeutil.py +114 -0
- sf_veritas/transmit_exception_to_sailfish.py +28 -0
- sf_veritas/transmitter.py +132 -0
- sf_veritas/types.py +47 -0
- sf_veritas/unified_interceptor.py +1678 -0
- sf_veritas/utils.py +39 -0
- sf_veritas-0.11.10.dist-info/METADATA +97 -0
- sf_veritas-0.11.10.dist-info/RECORD +141 -0
- sf_veritas-0.11.10.dist-info/WHEEL +5 -0
- sf_veritas-0.11.10.dist-info/entry_points.txt +2 -0
- sf_veritas-0.11.10.dist-info/top_level.txt +1 -0
- sf_veritas.libs/libbrotlicommon-6ce2a53c.so.1.0.6 +0 -0
- sf_veritas.libs/libbrotlidec-811d1be3.so.1.0.6 +0 -0
- sf_veritas.libs/libcom_err-730ca923.so.2.1 +0 -0
- sf_veritas.libs/libcrypt-52aca757.so.1.1.0 +0 -0
- sf_veritas.libs/libcrypto-bdaed0ea.so.1.1.1k +0 -0
- sf_veritas.libs/libcurl-eaa3cf66.so.4.5.0 +0 -0
- sf_veritas.libs/libgssapi_krb5-323bbd21.so.2.2 +0 -0
- sf_veritas.libs/libidn2-2f4a5893.so.0.3.6 +0 -0
- sf_veritas.libs/libk5crypto-9a74ff38.so.3.1 +0 -0
- sf_veritas.libs/libkeyutils-2777d33d.so.1.6 +0 -0
- sf_veritas.libs/libkrb5-a55300e8.so.3.3 +0 -0
- sf_veritas.libs/libkrb5support-e6594cfc.so.0.1 +0 -0
- sf_veritas.libs/liblber-2-d20824ef.4.so.2.10.9 +0 -0
- sf_veritas.libs/libldap-2-cea2a960.4.so.2.10.9 +0 -0
- sf_veritas.libs/libnghttp2-39367a22.so.14.17.0 +0 -0
- sf_veritas.libs/libpcre2-8-516f4c9d.so.0.7.1 +0 -0
- sf_veritas.libs/libpsl-99becdd3.so.5.3.1 +0 -0
- sf_veritas.libs/libsasl2-7de4d792.so.3.0.0 +0 -0
- sf_veritas.libs/libselinux-d0805dcb.so.1 +0 -0
- sf_veritas.libs/libssh-c11d285b.so.4.8.7 +0 -0
- sf_veritas.libs/libssl-60250281.so.1.1.1k +0 -0
- sf_veritas.libs/libunistring-05abdd40.so.2.1.0 +0 -0
- sf_veritas.libs/libuuid-95b83d40.so.1.3.0 +0 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Patch httpx to inject tracing headers and capture network requests using event hooks.
|
|
3
|
+
|
|
4
|
+
• For every outbound request, propagate the SAILFISH_TRACING_HEADER + FUNCSPAN_OVERRIDE_HEADER
|
|
5
|
+
unless the destination host is in `domains_to_not_propagate_headers_to`.
|
|
6
|
+
• Fire NetworkRequestTransmitter via utils.record_network_request
|
|
7
|
+
so we always capture (url, status, timings, success, error).
|
|
8
|
+
• When LD_PRELOAD is active, ONLY inject headers (skip capture - socket layer handles it).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import os
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
from typing import Dict, List, Optional, Tuple
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import wrapt
|
|
21
|
+
|
|
22
|
+
HAS_WRAPT = True
|
|
23
|
+
except ImportError:
|
|
24
|
+
HAS_WRAPT = False
|
|
25
|
+
|
|
26
|
+
from ...constants import FUNCSPAN_OVERRIDE_HEADER, SAILFISH_TRACING_HEADER
|
|
27
|
+
from ...thread_local import (
|
|
28
|
+
activate_reentrancy_guards_exception,
|
|
29
|
+
activate_reentrancy_guards_logging,
|
|
30
|
+
activate_reentrancy_guards_print,
|
|
31
|
+
get_funcspan_override,
|
|
32
|
+
trace_id_ctx,
|
|
33
|
+
)
|
|
34
|
+
from .utils import (
|
|
35
|
+
get_trace_and_should_propagate,
|
|
36
|
+
get_trace_and_should_propagate_fast,
|
|
37
|
+
init_fast_header_check,
|
|
38
|
+
inject_headers_ultrafast,
|
|
39
|
+
is_ssl_socket_active,
|
|
40
|
+
record_network_request,
|
|
41
|
+
)
|
|
42
|
+
from .._patch_tracker import is_already_patched, mark_as_patched
|
|
43
|
+
|
|
44
|
+
# JSON serialization - try fast orjson first, fallback to stdlib json
|
|
45
|
+
try:
|
|
46
|
+
import orjson
|
|
47
|
+
|
|
48
|
+
HAS_ORJSON = True
|
|
49
|
+
except ImportError:
|
|
50
|
+
import json
|
|
51
|
+
|
|
52
|
+
HAS_ORJSON = False
|
|
53
|
+
|
|
54
|
+
###############################################################################
|
|
55
|
+
# Internal helpers
|
|
56
|
+
###############################################################################
|
|
57
|
+
|
|
58
|
+
# header names used for re-entrancy guards
|
|
59
|
+
REENTRANCY_GUARD_LOGGING_PREACTIVE = "reentrancy_guard_logging_preactive"
|
|
60
|
+
REENTRANCY_GUARD_PRINT_PREACTIVE = "reentrancy_guard_print_preactive"
|
|
61
|
+
REENTRANCY_GUARD_EXCEPTIONS_PREACTIVE = "reentrancy_guard_exception_preactive"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _tee_preload_active() -> bool:
|
|
65
|
+
"""Detect if LD_PRELOAD tee is active (same logic as requests.py)."""
|
|
66
|
+
if os.getenv("SF_TEE_PRELOAD_ONLY", "0") == "1":
|
|
67
|
+
return True
|
|
68
|
+
ld = os.getenv("LD_PRELOAD", "")
|
|
69
|
+
return "libsfnettee.so" in ld or "_sfteepreload" in ld
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _activate_rg(headers: Dict[str, str]) -> None:
|
|
73
|
+
"""Turn the three 'preactive' guard flags ON for downstream hops."""
|
|
74
|
+
headers[REENTRANCY_GUARD_LOGGING_PREACTIVE] = "true"
|
|
75
|
+
headers[REENTRANCY_GUARD_PRINT_PREACTIVE] = "true"
|
|
76
|
+
headers[REENTRANCY_GUARD_EXCEPTIONS_PREACTIVE] = "true"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _check_rg(headers: Dict[str, str]) -> None:
|
|
80
|
+
"""If any pre-active guard present, switch the corresponding guard on."""
|
|
81
|
+
if headers.get(REENTRANCY_GUARD_LOGGING_PREACTIVE, "false").lower() == "true":
|
|
82
|
+
activate_reentrancy_guards_logging()
|
|
83
|
+
if headers.get(REENTRANCY_GUARD_PRINT_PREACTIVE, "false").lower() == "true":
|
|
84
|
+
activate_reentrancy_guards_print()
|
|
85
|
+
if headers.get(REENTRANCY_GUARD_EXCEPTIONS_PREACTIVE, "false").lower() == "true":
|
|
86
|
+
activate_reentrancy_guards_exception()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _prepare(
|
|
90
|
+
url: str,
|
|
91
|
+
domains_to_skip: List[str],
|
|
92
|
+
headers: Dict[str, str],
|
|
93
|
+
) -> Tuple[str, Dict[str, str], int]:
|
|
94
|
+
"""
|
|
95
|
+
Inject trace header + funcspan override header (unless excluded) and return:
|
|
96
|
+
trace_id, merged_headers, timestamp_ms
|
|
97
|
+
|
|
98
|
+
ULTRA-FAST: <20ns overhead for header injection.
|
|
99
|
+
"""
|
|
100
|
+
trace_id, propagate = get_trace_and_should_propagate(url, domains_to_skip)
|
|
101
|
+
hdrs: Dict[str, str] = dict(headers or {})
|
|
102
|
+
_check_rg(hdrs)
|
|
103
|
+
if propagate:
|
|
104
|
+
hdrs[SAILFISH_TRACING_HEADER] = trace_id
|
|
105
|
+
|
|
106
|
+
# Inject funcspan override header if present (ContextVar lookup ~8ns)
|
|
107
|
+
try:
|
|
108
|
+
funcspan_override = get_funcspan_override()
|
|
109
|
+
if funcspan_override is not None:
|
|
110
|
+
hdrs[FUNCSPAN_OVERRIDE_HEADER] = funcspan_override
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
_activate_rg(hdrs)
|
|
115
|
+
return trace_id, hdrs, int(time.time() * 1_000)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _capture_request_data(request) -> bytes:
|
|
119
|
+
"""Capture request body data as bytes."""
|
|
120
|
+
req_data = b""
|
|
121
|
+
try:
|
|
122
|
+
# Check if content is available
|
|
123
|
+
if hasattr(request, "content"):
|
|
124
|
+
content = request.content
|
|
125
|
+
if isinstance(content, bytes):
|
|
126
|
+
req_data = content
|
|
127
|
+
elif isinstance(content, str):
|
|
128
|
+
req_data = content.encode("utf-8")
|
|
129
|
+
except Exception: # noqa: BLE001
|
|
130
|
+
pass
|
|
131
|
+
return req_data
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _capture_and_record(
|
|
135
|
+
trace_id: str,
|
|
136
|
+
url: str,
|
|
137
|
+
method: str,
|
|
138
|
+
status: int,
|
|
139
|
+
success: bool,
|
|
140
|
+
err: str | None,
|
|
141
|
+
t0: int,
|
|
142
|
+
t1: int,
|
|
143
|
+
req_data: bytes,
|
|
144
|
+
req_headers: bytes,
|
|
145
|
+
resp_data: bytes,
|
|
146
|
+
resp_headers: bytes,
|
|
147
|
+
) -> None:
|
|
148
|
+
"""Schedule capture and record in background thread AFTER response is returned to user."""
|
|
149
|
+
|
|
150
|
+
def _do_record():
|
|
151
|
+
record_network_request(
|
|
152
|
+
trace_id,
|
|
153
|
+
url,
|
|
154
|
+
method,
|
|
155
|
+
status,
|
|
156
|
+
success,
|
|
157
|
+
err,
|
|
158
|
+
timestamp_start=t0,
|
|
159
|
+
timestamp_end=t1,
|
|
160
|
+
request_data=req_data,
|
|
161
|
+
response_data=resp_data,
|
|
162
|
+
request_headers=req_headers,
|
|
163
|
+
response_headers=resp_headers,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
threading.Thread(target=_do_record, daemon=True).start()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
###############################################################################
|
|
170
|
+
# Event hook factories
|
|
171
|
+
###############################################################################
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _make_request_hook(domains_to_skip: List[str], preload_active: bool):
|
|
175
|
+
"""Create a sync request hook that injects headers before request is sent."""
|
|
176
|
+
|
|
177
|
+
if preload_active:
|
|
178
|
+
# ========== ULTRA-FAST PATH: When LD_PRELOAD is active ==========
|
|
179
|
+
def request_hook(request):
|
|
180
|
+
"""Inject tracing headers into outbound request (ultra-fast C extension)."""
|
|
181
|
+
try:
|
|
182
|
+
url = str(request.url)
|
|
183
|
+
# CRITICAL: Skip if already injected (prevents double injection)
|
|
184
|
+
if SAILFISH_TRACING_HEADER not in request.headers:
|
|
185
|
+
# ULTRA-FAST: Thread-local cache + direct ContextVar.get() (<100ns!)
|
|
186
|
+
inject_headers_ultrafast(request.headers, url, domains_to_skip)
|
|
187
|
+
except Exception: # noqa: BLE001
|
|
188
|
+
pass # Fail silently to not break requests
|
|
189
|
+
|
|
190
|
+
else:
|
|
191
|
+
# ========== FULL CAPTURE PATH: When LD_PRELOAD is NOT active ==========
|
|
192
|
+
def request_hook(request):
|
|
193
|
+
"""Inject tracing headers into outbound request (optimized - no debug logging)."""
|
|
194
|
+
try:
|
|
195
|
+
url = str(request.url)
|
|
196
|
+
trace_id, hdrs, t0 = _prepare(
|
|
197
|
+
url, domains_to_skip, dict(request.headers)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Update request headers
|
|
201
|
+
request.headers.update(hdrs)
|
|
202
|
+
|
|
203
|
+
# Store metadata on request for response hook to use
|
|
204
|
+
# httpx Request objects always have an extensions dict
|
|
205
|
+
request.extensions["sf_trace_id"] = trace_id
|
|
206
|
+
request.extensions["sf_timestamp_start"] = t0
|
|
207
|
+
|
|
208
|
+
# Capture request data
|
|
209
|
+
request.extensions["sf_request_data"] = _capture_request_data(request)
|
|
210
|
+
|
|
211
|
+
# Capture request headers
|
|
212
|
+
if HAS_ORJSON:
|
|
213
|
+
request.extensions["sf_request_headers"] = orjson.dumps(
|
|
214
|
+
dict(request.headers)
|
|
215
|
+
)
|
|
216
|
+
else:
|
|
217
|
+
request.extensions["sf_request_headers"] = json.dumps(
|
|
218
|
+
dict(request.headers)
|
|
219
|
+
).encode("utf-8")
|
|
220
|
+
except Exception: # noqa: BLE001
|
|
221
|
+
pass # Fail silently to not break requests
|
|
222
|
+
|
|
223
|
+
return request_hook
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _make_async_request_hook(domains_to_skip: List[str], preload_active: bool):
|
|
227
|
+
"""Create an async request hook that injects headers before request is sent."""
|
|
228
|
+
|
|
229
|
+
if preload_active:
|
|
230
|
+
# ========== ULTRA-FAST PATH: When LD_PRELOAD is active ==========
|
|
231
|
+
async def async_request_hook(request):
|
|
232
|
+
"""Inject tracing headers into outbound request (ultra-fast C extension)."""
|
|
233
|
+
# Get trace ID and check if we should propagate
|
|
234
|
+
url = str(request.url)
|
|
235
|
+
# CRITICAL: Skip if already injected (prevents double injection)
|
|
236
|
+
if SAILFISH_TRACING_HEADER not in request.headers:
|
|
237
|
+
# ULTRA-FAST: Thread-local cache + direct ContextVar.get() (<100ns!)
|
|
238
|
+
inject_headers_ultrafast(request.headers, url, domains_to_skip)
|
|
239
|
+
|
|
240
|
+
else:
|
|
241
|
+
# ========== FULL CAPTURE PATH: When LD_PRELOAD is NOT active ==========
|
|
242
|
+
async def async_request_hook(request):
|
|
243
|
+
"""Inject tracing headers into outbound request (optimized - no debug logging)."""
|
|
244
|
+
# Get trace ID and timing
|
|
245
|
+
url = str(request.url)
|
|
246
|
+
trace_id = trace_id_ctx.get(None) or ""
|
|
247
|
+
t0 = int(time.time() * 1_000)
|
|
248
|
+
|
|
249
|
+
# Check and activate re-entrancy guards from incoming headers (avoid dict copy)
|
|
250
|
+
req_headers = request.headers
|
|
251
|
+
if (
|
|
252
|
+
req_headers.get(REENTRANCY_GUARD_LOGGING_PREACTIVE, "false").lower()
|
|
253
|
+
== "true"
|
|
254
|
+
):
|
|
255
|
+
activate_reentrancy_guards_logging()
|
|
256
|
+
if (
|
|
257
|
+
req_headers.get(REENTRANCY_GUARD_PRINT_PREACTIVE, "false").lower()
|
|
258
|
+
== "true"
|
|
259
|
+
):
|
|
260
|
+
activate_reentrancy_guards_print()
|
|
261
|
+
if (
|
|
262
|
+
req_headers.get(REENTRANCY_GUARD_EXCEPTIONS_PREACTIVE, "false").lower()
|
|
263
|
+
== "true"
|
|
264
|
+
):
|
|
265
|
+
activate_reentrancy_guards_exception()
|
|
266
|
+
|
|
267
|
+
# CRITICAL: Skip if already injected (prevents double injection)
|
|
268
|
+
if SAILFISH_TRACING_HEADER not in req_headers:
|
|
269
|
+
# ULTRA-FAST: Thread-local cache + direct ContextVar.get() (<100ns!)
|
|
270
|
+
inject_headers_ultrafast(req_headers, url, domains_to_skip)
|
|
271
|
+
|
|
272
|
+
# Activate re-entrancy guards for downstream (inject into request)
|
|
273
|
+
req_headers[REENTRANCY_GUARD_LOGGING_PREACTIVE] = "true"
|
|
274
|
+
req_headers[REENTRANCY_GUARD_PRINT_PREACTIVE] = "true"
|
|
275
|
+
req_headers[REENTRANCY_GUARD_EXCEPTIONS_PREACTIVE] = "true"
|
|
276
|
+
|
|
277
|
+
# Store metadata on request for response hook to use
|
|
278
|
+
request.extensions["sf_trace_id"] = trace_id
|
|
279
|
+
request.extensions["sf_timestamp_start"] = t0
|
|
280
|
+
|
|
281
|
+
# Capture request data
|
|
282
|
+
request.extensions["sf_request_data"] = _capture_request_data(request)
|
|
283
|
+
|
|
284
|
+
# Capture request headers (AFTER injection)
|
|
285
|
+
if HAS_ORJSON:
|
|
286
|
+
request.extensions["sf_request_headers"] = orjson.dumps(
|
|
287
|
+
dict(req_headers)
|
|
288
|
+
)
|
|
289
|
+
else:
|
|
290
|
+
request.extensions["sf_request_headers"] = json.dumps(
|
|
291
|
+
dict(req_headers)
|
|
292
|
+
).encode("utf-8")
|
|
293
|
+
|
|
294
|
+
return async_request_hook
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _make_response_hook(preload_active: bool):
|
|
298
|
+
"""Create a response hook that captures and records response data."""
|
|
299
|
+
|
|
300
|
+
def response_hook(response):
|
|
301
|
+
"""Capture response data and record the network request."""
|
|
302
|
+
# Skip recording if LD_PRELOAD is active (socket layer already captured it)
|
|
303
|
+
if preload_active:
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
# Skip capture for HTTPS when ssl_socket.py is active (avoids double-capture)
|
|
307
|
+
url = str(response.request.url)
|
|
308
|
+
is_https = url.startswith("https://")
|
|
309
|
+
if is_https and is_ssl_socket_active():
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
# Extract metadata from request
|
|
313
|
+
request = response.request
|
|
314
|
+
trace_id = request.extensions.get("sf_trace_id", "")
|
|
315
|
+
t0 = request.extensions.get("sf_timestamp_start", 0)
|
|
316
|
+
req_data = request.extensions.get("sf_request_data", b"")
|
|
317
|
+
req_headers = request.extensions.get("sf_request_headers", b"")
|
|
318
|
+
|
|
319
|
+
# Capture response data
|
|
320
|
+
url = str(request.url)
|
|
321
|
+
method = str(request.method).upper()
|
|
322
|
+
status = response.status_code
|
|
323
|
+
success = status < 400
|
|
324
|
+
t1 = int(time.time() * 1_000)
|
|
325
|
+
|
|
326
|
+
resp_data = b""
|
|
327
|
+
resp_headers = b""
|
|
328
|
+
|
|
329
|
+
try:
|
|
330
|
+
# Capture response body - check if already consumed/materialized
|
|
331
|
+
# Don't force materialization of streaming responses
|
|
332
|
+
if hasattr(response, '_content') and response._content is not None:
|
|
333
|
+
# Response body already materialized (non-streaming or already read)
|
|
334
|
+
resp_data = response._content
|
|
335
|
+
elif hasattr(response, 'is_stream_consumed') and not response.is_stream_consumed:
|
|
336
|
+
# Streaming response not yet consumed - don't materialize
|
|
337
|
+
# We'll capture what we can without breaking streaming behavior
|
|
338
|
+
resp_data = b""
|
|
339
|
+
else:
|
|
340
|
+
# Safe to access .content (either not streaming or already consumed)
|
|
341
|
+
resp_data = response.content
|
|
342
|
+
|
|
343
|
+
# Capture response headers
|
|
344
|
+
if HAS_ORJSON:
|
|
345
|
+
resp_headers = orjson.dumps({str(k): str(v) for k, v in response.headers.items()})
|
|
346
|
+
else:
|
|
347
|
+
resp_headers = json.dumps({str(k): str(v) for k, v in response.headers.items()}).encode("utf-8")
|
|
348
|
+
except Exception: # noqa: BLE001
|
|
349
|
+
pass
|
|
350
|
+
|
|
351
|
+
# Record in background thread
|
|
352
|
+
_capture_and_record(
|
|
353
|
+
trace_id,
|
|
354
|
+
url,
|
|
355
|
+
method,
|
|
356
|
+
status,
|
|
357
|
+
success,
|
|
358
|
+
None,
|
|
359
|
+
t0,
|
|
360
|
+
t1,
|
|
361
|
+
req_data,
|
|
362
|
+
req_headers,
|
|
363
|
+
resp_data,
|
|
364
|
+
resp_headers,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
return response_hook
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _make_async_response_hook(preload_active: bool):
|
|
371
|
+
"""Create an async response hook that captures and records response data."""
|
|
372
|
+
|
|
373
|
+
async def async_response_hook(response):
|
|
374
|
+
"""Capture response data and record the network request (optimized - no debug logging)."""
|
|
375
|
+
# Skip recording if LD_PRELOAD is active (socket layer already captured it)
|
|
376
|
+
if preload_active:
|
|
377
|
+
return
|
|
378
|
+
|
|
379
|
+
# Skip capture for HTTPS when ssl_socket.py is active (avoids double-capture)
|
|
380
|
+
url = str(response.request.url)
|
|
381
|
+
is_https = url.startswith("https://")
|
|
382
|
+
if is_https and is_ssl_socket_active():
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
try:
|
|
386
|
+
# Extract metadata from request
|
|
387
|
+
request = response.request
|
|
388
|
+
trace_id = request.extensions.get("sf_trace_id", "")
|
|
389
|
+
t0 = request.extensions.get("sf_timestamp_start", 0)
|
|
390
|
+
req_data = request.extensions.get("sf_request_data", b"")
|
|
391
|
+
req_headers = request.extensions.get("sf_request_headers", b"")
|
|
392
|
+
|
|
393
|
+
# Capture response data
|
|
394
|
+
url = str(request.url)
|
|
395
|
+
method = str(request.method).upper()
|
|
396
|
+
status = response.status_code
|
|
397
|
+
success = status < 400
|
|
398
|
+
t1 = int(time.time() * 1_000)
|
|
399
|
+
|
|
400
|
+
resp_data = b""
|
|
401
|
+
resp_headers = b""
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
# Capture response body - check if already consumed/materialized
|
|
405
|
+
# Don't force materialization of streaming responses
|
|
406
|
+
if hasattr(response, '_content') and response._content is not None:
|
|
407
|
+
# Response body already materialized (non-streaming or already read)
|
|
408
|
+
resp_data = response._content
|
|
409
|
+
elif hasattr(response, 'is_stream_consumed') and not response.is_stream_consumed:
|
|
410
|
+
# Streaming response not yet consumed - don't materialize
|
|
411
|
+
# We'll capture what we can without breaking streaming behavior
|
|
412
|
+
resp_data = b""
|
|
413
|
+
else:
|
|
414
|
+
# Safe to read (either not streaming or already consumed)
|
|
415
|
+
# For non-streaming async responses, read the body
|
|
416
|
+
try:
|
|
417
|
+
await response.aread()
|
|
418
|
+
resp_data = response.content
|
|
419
|
+
except asyncio.CancelledError:
|
|
420
|
+
raise # CRITICAL: Must re-raise CancelledError immediately
|
|
421
|
+
except Exception:
|
|
422
|
+
resp_data = b""
|
|
423
|
+
|
|
424
|
+
# Capture response headers
|
|
425
|
+
if HAS_ORJSON:
|
|
426
|
+
resp_headers = orjson.dumps({str(k): str(v) for k, v in response.headers.items()})
|
|
427
|
+
else:
|
|
428
|
+
resp_headers = json.dumps({str(k): str(v) for k, v in response.headers.items()}).encode("utf-8")
|
|
429
|
+
except Exception: # noqa: BLE001
|
|
430
|
+
pass
|
|
431
|
+
|
|
432
|
+
# Record in background thread
|
|
433
|
+
_capture_and_record(
|
|
434
|
+
trace_id,
|
|
435
|
+
url,
|
|
436
|
+
method,
|
|
437
|
+
status,
|
|
438
|
+
success,
|
|
439
|
+
None,
|
|
440
|
+
t0,
|
|
441
|
+
t1,
|
|
442
|
+
req_data,
|
|
443
|
+
req_headers,
|
|
444
|
+
resp_data,
|
|
445
|
+
resp_headers,
|
|
446
|
+
)
|
|
447
|
+
except Exception: # noqa: BLE001
|
|
448
|
+
pass # Silently fail to not break requests
|
|
449
|
+
|
|
450
|
+
return async_response_hook
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
###############################################################################
|
|
454
|
+
# Top-level patch function
|
|
455
|
+
###############################################################################
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def patch_httpx(domains_to_not_propagate_headers_to: Optional[List[str]] = None):
|
|
459
|
+
"""
|
|
460
|
+
Patch httpx to inject SAILFISH_TRACING_HEADER into all outbound requests
|
|
461
|
+
using event hooks. Safe to call even if httpx is not installed.
|
|
462
|
+
|
|
463
|
+
When LD_PRELOAD is active:
|
|
464
|
+
- ALWAYS inject headers (trace_id + funcspan_override)
|
|
465
|
+
- SKIP capture/emission (LD_PRELOAD handles at socket layer)
|
|
466
|
+
- Uses ultra-fast C extension for <10ns overhead
|
|
467
|
+
"""
|
|
468
|
+
# Idempotency guard: prevent double-patching (handles forks, reloading)
|
|
469
|
+
if is_already_patched("httpx"):
|
|
470
|
+
return
|
|
471
|
+
mark_as_patched("httpx")
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
import httpx
|
|
475
|
+
except ImportError:
|
|
476
|
+
return # No httpx installed—nothing to patch
|
|
477
|
+
|
|
478
|
+
domains = domains_to_not_propagate_headers_to or []
|
|
479
|
+
preload_active = _tee_preload_active()
|
|
480
|
+
|
|
481
|
+
# Initialize C extension for ultra-fast header checking (if available)
|
|
482
|
+
if preload_active:
|
|
483
|
+
init_fast_header_check(domains)
|
|
484
|
+
|
|
485
|
+
# Create hooks
|
|
486
|
+
sync_request_hook = _make_request_hook(domains, preload_active)
|
|
487
|
+
async_request_hook = _make_async_request_hook(domains, preload_active)
|
|
488
|
+
sync_response_hook = _make_response_hook(preload_active)
|
|
489
|
+
async_response_hook = _make_async_response_hook(preload_active)
|
|
490
|
+
|
|
491
|
+
# Patch Client.__init__ to attach sync hooks
|
|
492
|
+
if HAS_WRAPT:
|
|
493
|
+
|
|
494
|
+
def instrumented_client_init(wrapped, instance, args, kwargs):
|
|
495
|
+
"""Ultra-fast hook injection using wrapt."""
|
|
496
|
+
# Get existing event_hooks or create empty dict
|
|
497
|
+
event_hooks = kwargs.get("event_hooks") or {}
|
|
498
|
+
|
|
499
|
+
# Add our sync hooks to the request and response lists
|
|
500
|
+
event_hooks.setdefault("request", []).append(sync_request_hook)
|
|
501
|
+
event_hooks.setdefault("response", []).append(sync_response_hook)
|
|
502
|
+
|
|
503
|
+
kwargs["event_hooks"] = event_hooks
|
|
504
|
+
return wrapped(*args, **kwargs)
|
|
505
|
+
|
|
506
|
+
wrapt.wrap_function_wrapper(
|
|
507
|
+
"httpx", "Client.__init__", instrumented_client_init
|
|
508
|
+
)
|
|
509
|
+
else:
|
|
510
|
+
original_client_init = httpx.Client.__init__
|
|
511
|
+
|
|
512
|
+
def patched_client_init(self, *args, **kwargs):
|
|
513
|
+
# Get existing event_hooks or create empty dict
|
|
514
|
+
event_hooks = kwargs.get("event_hooks") or {}
|
|
515
|
+
|
|
516
|
+
# Add our sync hooks to the request and response lists
|
|
517
|
+
event_hooks.setdefault("request", []).append(sync_request_hook)
|
|
518
|
+
event_hooks.setdefault("response", []).append(sync_response_hook)
|
|
519
|
+
|
|
520
|
+
kwargs["event_hooks"] = event_hooks
|
|
521
|
+
original_client_init(self, *args, **kwargs)
|
|
522
|
+
|
|
523
|
+
httpx.Client.__init__ = patched_client_init
|
|
524
|
+
|
|
525
|
+
# Patch AsyncClient.__init__ to attach async hooks
|
|
526
|
+
if HAS_WRAPT:
|
|
527
|
+
|
|
528
|
+
def instrumented_async_client_init(wrapped, instance, args, kwargs):
|
|
529
|
+
"""Ultra-fast hook injection using wrapt."""
|
|
530
|
+
# Get existing event_hooks or create empty dict
|
|
531
|
+
event_hooks = kwargs.get("event_hooks") or {}
|
|
532
|
+
|
|
533
|
+
# Add our ASYNC hooks to the request and response lists
|
|
534
|
+
event_hooks.setdefault("request", []).append(async_request_hook)
|
|
535
|
+
event_hooks.setdefault("response", []).append(async_response_hook)
|
|
536
|
+
|
|
537
|
+
kwargs["event_hooks"] = event_hooks
|
|
538
|
+
return wrapped(*args, **kwargs)
|
|
539
|
+
|
|
540
|
+
wrapt.wrap_function_wrapper(
|
|
541
|
+
"httpx", "AsyncClient.__init__", instrumented_async_client_init
|
|
542
|
+
)
|
|
543
|
+
else:
|
|
544
|
+
original_async_client_init = httpx.AsyncClient.__init__
|
|
545
|
+
|
|
546
|
+
def patched_async_client_init(self, *args, **kwargs):
|
|
547
|
+
# Get existing event_hooks or create empty dict
|
|
548
|
+
event_hooks = kwargs.get("event_hooks") or {}
|
|
549
|
+
|
|
550
|
+
# Add our ASYNC hooks to the request and response lists
|
|
551
|
+
event_hooks.setdefault("request", []).append(async_request_hook)
|
|
552
|
+
event_hooks.setdefault("response", []).append(async_response_hook)
|
|
553
|
+
|
|
554
|
+
kwargs["event_hooks"] = event_hooks
|
|
555
|
+
original_async_client_init(self, *args, **kwargs)
|
|
556
|
+
|
|
557
|
+
httpx.AsyncClient.__init__ = patched_async_client_init
|