sf-veritas 0.10.3__cp313-cp313-manylinux_2_28_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sf-veritas might be problematic. Click here for more details.
- sf_veritas/__init__.py +20 -0
- sf_veritas/_sffastlog.c +889 -0
- sf_veritas/_sffastlog.cpython-313-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnet.c +924 -0
- sf_veritas/_sffastnet.cpython-313-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnetworkrequest.c +730 -0
- sf_veritas/_sffastnetworkrequest.cpython-313-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan.c +2155 -0
- sf_veritas/_sffuncspan.cpython-313-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan_config.c +617 -0
- sf_veritas/_sffuncspan_config.cpython-313-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfheadercheck.c +341 -0
- sf_veritas/_sfheadercheck.cpython-313-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfnetworkhop.c +1451 -0
- sf_veritas/_sfnetworkhop.cpython-313-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfservice.c +1175 -0
- sf_veritas/_sfservice.cpython-313-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfteepreload.c +5167 -0
- sf_veritas/app_config.py +49 -0
- sf_veritas/cli.py +336 -0
- sf_veritas/constants.py +10 -0
- sf_veritas/custom_excepthook.py +304 -0
- sf_veritas/custom_log_handler.py +129 -0
- sf_veritas/custom_output_wrapper.py +144 -0
- sf_veritas/custom_print.py +146 -0
- sf_veritas/django_app.py +5 -0
- sf_veritas/env_vars.py +186 -0
- sf_veritas/exception_handling_middleware.py +18 -0
- sf_veritas/exception_metaclass.py +69 -0
- sf_veritas/fast_frame_info.py +116 -0
- sf_veritas/fast_network_hop.py +293 -0
- sf_veritas/frame_tools.py +112 -0
- sf_veritas/funcspan_config_loader.py +556 -0
- sf_veritas/function_span_profiler.py +1174 -0
- sf_veritas/import_hook.py +62 -0
- sf_veritas/infra_details/__init__.py +3 -0
- sf_veritas/infra_details/get_infra_details.py +24 -0
- sf_veritas/infra_details/kubernetes/__init__.py +3 -0
- sf_veritas/infra_details/kubernetes/get_cluster_name.py +147 -0
- sf_veritas/infra_details/kubernetes/get_details.py +7 -0
- sf_veritas/infra_details/running_on/__init__.py +17 -0
- sf_veritas/infra_details/running_on/kubernetes.py +11 -0
- sf_veritas/interceptors.py +497 -0
- sf_veritas/libsfnettee.so +0 -0
- sf_veritas/local_env_detect.py +118 -0
- sf_veritas/package_metadata.py +6 -0
- sf_veritas/patches/__init__.py +0 -0
- sf_veritas/patches/concurrent_futures.py +19 -0
- sf_veritas/patches/constants.py +1 -0
- sf_veritas/patches/exceptions.py +82 -0
- sf_veritas/patches/multiprocessing.py +32 -0
- sf_veritas/patches/network_libraries/__init__.py +76 -0
- sf_veritas/patches/network_libraries/aiohttp.py +281 -0
- sf_veritas/patches/network_libraries/curl_cffi.py +363 -0
- sf_veritas/patches/network_libraries/http_client.py +419 -0
- sf_veritas/patches/network_libraries/httpcore.py +515 -0
- sf_veritas/patches/network_libraries/httplib2.py +204 -0
- sf_veritas/patches/network_libraries/httpx.py +515 -0
- sf_veritas/patches/network_libraries/niquests.py +211 -0
- sf_veritas/patches/network_libraries/pycurl.py +385 -0
- sf_veritas/patches/network_libraries/requests.py +633 -0
- sf_veritas/patches/network_libraries/tornado.py +341 -0
- sf_veritas/patches/network_libraries/treq.py +270 -0
- sf_veritas/patches/network_libraries/urllib_request.py +468 -0
- sf_veritas/patches/network_libraries/utils.py +398 -0
- sf_veritas/patches/os.py +17 -0
- sf_veritas/patches/threading.py +218 -0
- sf_veritas/patches/web_frameworks/__init__.py +54 -0
- sf_veritas/patches/web_frameworks/aiohttp.py +793 -0
- sf_veritas/patches/web_frameworks/async_websocket_consumer.py +317 -0
- sf_veritas/patches/web_frameworks/blacksheep.py +527 -0
- sf_veritas/patches/web_frameworks/bottle.py +502 -0
- sf_veritas/patches/web_frameworks/cherrypy.py +678 -0
- sf_veritas/patches/web_frameworks/cors_utils.py +122 -0
- sf_veritas/patches/web_frameworks/django.py +944 -0
- sf_veritas/patches/web_frameworks/eve.py +395 -0
- sf_veritas/patches/web_frameworks/falcon.py +926 -0
- sf_veritas/patches/web_frameworks/fastapi.py +724 -0
- sf_veritas/patches/web_frameworks/flask.py +520 -0
- sf_veritas/patches/web_frameworks/klein.py +501 -0
- sf_veritas/patches/web_frameworks/litestar.py +551 -0
- sf_veritas/patches/web_frameworks/pyramid.py +428 -0
- sf_veritas/patches/web_frameworks/quart.py +824 -0
- sf_veritas/patches/web_frameworks/robyn.py +697 -0
- sf_veritas/patches/web_frameworks/sanic.py +857 -0
- sf_veritas/patches/web_frameworks/starlette.py +723 -0
- sf_veritas/patches/web_frameworks/strawberry.py +813 -0
- sf_veritas/patches/web_frameworks/tornado.py +481 -0
- sf_veritas/patches/web_frameworks/utils.py +91 -0
- sf_veritas/print_override.py +13 -0
- sf_veritas/regular_data_transmitter.py +409 -0
- sf_veritas/request_interceptor.py +401 -0
- sf_veritas/request_utils.py +550 -0
- sf_veritas/server_status.py +1 -0
- sf_veritas/shutdown_flag.py +11 -0
- sf_veritas/subprocess_startup.py +3 -0
- sf_veritas/test_cli.py +145 -0
- sf_veritas/thread_local.py +970 -0
- sf_veritas/timeutil.py +114 -0
- sf_veritas/transmit_exception_to_sailfish.py +28 -0
- sf_veritas/transmitter.py +132 -0
- sf_veritas/types.py +47 -0
- sf_veritas/unified_interceptor.py +1580 -0
- sf_veritas/utils.py +39 -0
- sf_veritas-0.10.3.dist-info/METADATA +97 -0
- sf_veritas-0.10.3.dist-info/RECORD +132 -0
- sf_veritas-0.10.3.dist-info/WHEEL +5 -0
- sf_veritas-0.10.3.dist-info/entry_points.txt +2 -0
- sf_veritas-0.10.3.dist-info/top_level.txt +1 -0
- sf_veritas.libs/libbrotlicommon-6ce2a53c.so.1.0.6 +0 -0
- sf_veritas.libs/libbrotlidec-811d1be3.so.1.0.6 +0 -0
- sf_veritas.libs/libcom_err-730ca923.so.2.1 +0 -0
- sf_veritas.libs/libcrypt-52aca757.so.1.1.0 +0 -0
- sf_veritas.libs/libcrypto-bdaed0ea.so.1.1.1k +0 -0
- sf_veritas.libs/libcurl-eaa3cf66.so.4.5.0 +0 -0
- sf_veritas.libs/libgssapi_krb5-323bbd21.so.2.2 +0 -0
- sf_veritas.libs/libidn2-2f4a5893.so.0.3.6 +0 -0
- sf_veritas.libs/libk5crypto-9a74ff38.so.3.1 +0 -0
- sf_veritas.libs/libkeyutils-2777d33d.so.1.6 +0 -0
- sf_veritas.libs/libkrb5-a55300e8.so.3.3 +0 -0
- sf_veritas.libs/libkrb5support-e6594cfc.so.0.1 +0 -0
- sf_veritas.libs/liblber-2-d20824ef.4.so.2.10.9 +0 -0
- sf_veritas.libs/libldap-2-cea2a960.4.so.2.10.9 +0 -0
- sf_veritas.libs/libnghttp2-39367a22.so.14.17.0 +0 -0
- sf_veritas.libs/libpcre2-8-516f4c9d.so.0.7.1 +0 -0
- sf_veritas.libs/libpsl-99becdd3.so.5.3.1 +0 -0
- sf_veritas.libs/libsasl2-7de4d792.so.3.0.0 +0 -0
- sf_veritas.libs/libselinux-d0805dcb.so.1 +0 -0
- sf_veritas.libs/libssh-c11d285b.so.4.8.7 +0 -0
- sf_veritas.libs/libssl-60250281.so.1.1.1k +0 -0
- sf_veritas.libs/libunistring-05abdd40.so.2.1.0 +0 -0
- sf_veritas.libs/libuuid-95b83d40.so.1.3.0 +0 -0
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import inspect
|
|
3
|
+
import sys
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
from ... import _sffuncspan_config
|
|
7
|
+
from .cors_utils import inject_sailfish_headers, should_inject_headers
|
|
8
|
+
from .utils import _is_user_code, _unwrap_user_func, should_skip_route
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from django.utils.deprecation import MiddlewareMixin
|
|
12
|
+
except ImportError:
|
|
13
|
+
MiddlewareMixin = object # fallback for non-Django environments
|
|
14
|
+
|
|
15
|
+
import traceback
|
|
16
|
+
|
|
17
|
+
from ... import app_config
|
|
18
|
+
from ...constants import (
|
|
19
|
+
FUNCSPAN_OVERRIDE_HEADER_BYTES,
|
|
20
|
+
SAILFISH_TRACING_HEADER,
|
|
21
|
+
SAILFISH_TRACING_HEADER_BYTES,
|
|
22
|
+
)
|
|
23
|
+
from ...custom_excepthook import custom_excepthook
|
|
24
|
+
from ...env_vars import (
|
|
25
|
+
PRINT_CONFIGURATION_STATUSES,
|
|
26
|
+
SF_DEBUG,
|
|
27
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_BODY,
|
|
28
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS,
|
|
29
|
+
SF_NETWORKHOP_CAPTURE_RESPONSE_BODY,
|
|
30
|
+
SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS,
|
|
31
|
+
SF_NETWORKHOP_REQUEST_LIMIT_MB,
|
|
32
|
+
SF_NETWORKHOP_RESPONSE_LIMIT_MB,
|
|
33
|
+
)
|
|
34
|
+
from ...fast_network_hop import fast_send_network_hop_fast, register_endpoint
|
|
35
|
+
from ...thread_local import (
|
|
36
|
+
clear_c_tls_parent_trace_id,
|
|
37
|
+
clear_current_request_path,
|
|
38
|
+
clear_outbound_header_base,
|
|
39
|
+
clear_trace_id,
|
|
40
|
+
generate_new_trace_id,
|
|
41
|
+
get_or_set_sf_trace_id,
|
|
42
|
+
get_sf_trace_id,
|
|
43
|
+
set_current_request_path,
|
|
44
|
+
set_outbound_header_base,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Registry mapping view function id → endpoint_id (for fast C path)
|
|
48
|
+
_ENDPOINT_REGISTRY = {}
|
|
49
|
+
|
|
50
|
+
# Module-level variable for routes to skip (set by patch_django_middleware)
|
|
51
|
+
_ROUTES_TO_SKIP = []
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def find_and_modify_output_wrapper():
|
|
55
|
+
"""
|
|
56
|
+
Monkey-patch Django's OutputWrapper to always use the current sys.stdout/stderr
|
|
57
|
+
instead of storing a reference at init time. This ensures Django management
|
|
58
|
+
commands (like migrate) output is captured by our UnifiedInterceptor even if
|
|
59
|
+
OutputWrapper was instantiated before setup_interceptors() ran.
|
|
60
|
+
"""
|
|
61
|
+
if PRINT_CONFIGURATION_STATUSES:
|
|
62
|
+
print("find_and_modify_output_wrapper", log=False)
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
from django.core.management.base import OutputWrapper
|
|
66
|
+
except ImportError:
|
|
67
|
+
if PRINT_CONFIGURATION_STATUSES:
|
|
68
|
+
print("Django not found; skipping OutputWrapper patch", log=False)
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
# Check if already patched (idempotent)
|
|
72
|
+
if hasattr(OutputWrapper, "_sf_patched"):
|
|
73
|
+
if PRINT_CONFIGURATION_STATUSES:
|
|
74
|
+
print("OutputWrapper already patched; skipping", log=False)
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
# Save original methods
|
|
78
|
+
_original_init = OutputWrapper.__init__
|
|
79
|
+
_original_write = OutputWrapper.write
|
|
80
|
+
|
|
81
|
+
def patched_init(self, out, ending="\n"):
|
|
82
|
+
"""Patched __init__ that tracks if this wrapper is wrapping stdout/stderr."""
|
|
83
|
+
# Call original init
|
|
84
|
+
_original_init(self, out, ending)
|
|
85
|
+
# Track if this wrapper is for stdout or stderr (so we can redirect to current stream)
|
|
86
|
+
self._sf_is_stdout = out is sys.stdout or out is sys.__stdout__
|
|
87
|
+
self._sf_is_stderr = out is sys.stderr or out is sys.__stderr__
|
|
88
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
89
|
+
print(
|
|
90
|
+
f"[Django OutputWrapper] Created: stdout={self._sf_is_stdout}, stderr={self._sf_is_stderr}",
|
|
91
|
+
log=False,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def patched_write(self, msg="", style_func=None, ending=None):
|
|
95
|
+
"""
|
|
96
|
+
Patched write that uses CURRENT sys.stdout/stderr instead of the stored reference.
|
|
97
|
+
This ensures our UnifiedInterceptor captures Django output.
|
|
98
|
+
"""
|
|
99
|
+
# If this wrapper was created for stdout, redirect to CURRENT sys.stdout
|
|
100
|
+
if getattr(self, "_sf_is_stdout", False):
|
|
101
|
+
original_out = self._out
|
|
102
|
+
self._out = sys.stdout
|
|
103
|
+
try:
|
|
104
|
+
return _original_write(self, msg, style_func, ending)
|
|
105
|
+
finally:
|
|
106
|
+
self._out = original_out
|
|
107
|
+
|
|
108
|
+
# If this wrapper was created for stderr, redirect to CURRENT sys.stderr
|
|
109
|
+
elif getattr(self, "_sf_is_stderr", False):
|
|
110
|
+
original_out = self._out
|
|
111
|
+
self._out = sys.stderr
|
|
112
|
+
try:
|
|
113
|
+
return _original_write(self, msg, style_func, ending)
|
|
114
|
+
finally:
|
|
115
|
+
self._out = original_out
|
|
116
|
+
|
|
117
|
+
# Otherwise use original behavior
|
|
118
|
+
return _original_write(self, msg, style_func, ending)
|
|
119
|
+
|
|
120
|
+
# Apply patches
|
|
121
|
+
OutputWrapper.__init__ = patched_init
|
|
122
|
+
OutputWrapper.write = patched_write
|
|
123
|
+
OutputWrapper._sf_patched = True
|
|
124
|
+
|
|
125
|
+
if PRINT_CONFIGURATION_STATUSES:
|
|
126
|
+
print("find_and_modify_output_wrapper...DONE (monkey-patched)", log=False)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class SailfishMiddleware(MiddlewareMixin):
|
|
130
|
+
"""
|
|
131
|
+
• process_request – capture inbound SAILFISH_TRACING_HEADER header.
|
|
132
|
+
• process_view – emit one NetworkHop per view (skip Strawberry).
|
|
133
|
+
• __call__ override – last-chance catcher for uncaught exceptions.
|
|
134
|
+
• got_request_exception signal – main hook for 500-level errors.
|
|
135
|
+
• process_exception – fallback for view-raised exceptions.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
# ------------------------------------------------------------------ #
|
|
139
|
+
# 0 | Signal registration (called once at server start-up)
|
|
140
|
+
# ------------------------------------------------------------------ #
|
|
141
|
+
def __init__(self, get_response):
|
|
142
|
+
super().__init__(get_response)
|
|
143
|
+
|
|
144
|
+
# Attach to Django's global exception signal so we ALWAYS
|
|
145
|
+
# see real exceptions that become HTTP-500 responses.
|
|
146
|
+
from django.core.signals import got_request_exception
|
|
147
|
+
|
|
148
|
+
got_request_exception.disconnect( # avoid dupes on reload
|
|
149
|
+
self._on_exception_signal, dispatch_uid="sf_veritas_signal"
|
|
150
|
+
)
|
|
151
|
+
got_request_exception.connect(
|
|
152
|
+
self._on_exception_signal,
|
|
153
|
+
weak=False,
|
|
154
|
+
dispatch_uid="sf_veritas_signal",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# ------------------------------------------------------------------ #
|
|
158
|
+
# 1 | Signal handler ← FIXED
|
|
159
|
+
# ------------------------------------------------------------------ #
|
|
160
|
+
def _on_exception_signal(self, sender, request, **kwargs):
|
|
161
|
+
"""
|
|
162
|
+
Handle django.core.signals.got_request_exception.
|
|
163
|
+
|
|
164
|
+
The signal doesn't pass the exception object; per Django's own
|
|
165
|
+
implementation (and Sentry's approach) we fetch it from
|
|
166
|
+
sys.exc_info().
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
exc_type, exc_value, exc_tb = sys.exc_info()
|
|
170
|
+
|
|
171
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
172
|
+
print(
|
|
173
|
+
f"[[SailfishMiddleware._on_exception_signal]] "
|
|
174
|
+
f"exc_value={exc_value!r}",
|
|
175
|
+
log=False,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if exc_value:
|
|
179
|
+
custom_excepthook(exc_type, exc_value, exc_tb)
|
|
180
|
+
|
|
181
|
+
# ------------------------------------------------------------------ #
|
|
182
|
+
# 2 | Last-chance wrapper (rarely triggered in WSGI but free)
|
|
183
|
+
# ------------------------------------------------------------------ #
|
|
184
|
+
def __call__(self, request):
|
|
185
|
+
try:
|
|
186
|
+
return super().__call__(request)
|
|
187
|
+
except Exception as exc:
|
|
188
|
+
custom_excepthook(type(exc), exc, exc.__traceback__)
|
|
189
|
+
raise # preserve default Django 500
|
|
190
|
+
|
|
191
|
+
# ------------------------------------------------------------------ #
|
|
192
|
+
# 3 | Header capture
|
|
193
|
+
# ------------------------------------------------------------------ #
|
|
194
|
+
def process_request(self, request):
|
|
195
|
+
# CRITICAL: Clear trace_id FIRST at request start to ensure fresh start
|
|
196
|
+
# Django reuses threads, so we must clear the ContextVar from previous request
|
|
197
|
+
try:
|
|
198
|
+
clear_trace_id()
|
|
199
|
+
clear_outbound_header_base()
|
|
200
|
+
clear_c_tls_parent_trace_id()
|
|
201
|
+
clear_current_request_path()
|
|
202
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
203
|
+
print(
|
|
204
|
+
f"[[SailfishMiddleware.process_request]] Cleared all context at request start",
|
|
205
|
+
log=False,
|
|
206
|
+
)
|
|
207
|
+
except Exception as e:
|
|
208
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
209
|
+
print(
|
|
210
|
+
f"[[SailfishMiddleware.process_request]] Failed to clear context: {e}",
|
|
211
|
+
log=False,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Set current request path for route-based suppression (SF_DISABLE_INBOUND_NETWORK_TRACING_ON_ROUTES)
|
|
215
|
+
set_current_request_path(request.path)
|
|
216
|
+
|
|
217
|
+
# PERFORMANCE: Single-pass bytes-level header scan (convert Django META to bytes for consistent scanning)
|
|
218
|
+
# Django stores headers in META with HTTP_ prefix, scan once and extract what we need
|
|
219
|
+
incoming_trace_raw = None
|
|
220
|
+
funcspan_override_header = None
|
|
221
|
+
|
|
222
|
+
# Django uses string headers in META, scan for our headers
|
|
223
|
+
header_key = f"HTTP_{SAILFISH_TRACING_HEADER.upper().replace('-', '_')}"
|
|
224
|
+
incoming_trace_raw = request.META.get(header_key)
|
|
225
|
+
|
|
226
|
+
funcspan_override_key = "HTTP_X_SF3_FUNCTIONSPANCAPTUREOVERRIDE"
|
|
227
|
+
funcspan_override_header = request.META.get(funcspan_override_key)
|
|
228
|
+
|
|
229
|
+
# CRITICAL: Seed/ensure trace_id immediately (BEFORE any outbound work)
|
|
230
|
+
if incoming_trace_raw:
|
|
231
|
+
# Incoming X-Sf3-Rid header provided - use it
|
|
232
|
+
get_or_set_sf_trace_id(
|
|
233
|
+
incoming_trace_raw, is_associated_with_inbound_request=True
|
|
234
|
+
)
|
|
235
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
236
|
+
trace_id = get_sf_trace_id()
|
|
237
|
+
print(
|
|
238
|
+
f"[[SailfishMiddleware.process_request]] "
|
|
239
|
+
f"Using incoming trace: {incoming_trace_raw} → trace_id={trace_id}",
|
|
240
|
+
log=False,
|
|
241
|
+
)
|
|
242
|
+
else:
|
|
243
|
+
# No incoming X-Sf3-Rid header - generate fresh trace_id for this request
|
|
244
|
+
new_trace = generate_new_trace_id()
|
|
245
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
246
|
+
trace_id = get_sf_trace_id()
|
|
247
|
+
print(
|
|
248
|
+
f"[[SailfishMiddleware.process_request]] "
|
|
249
|
+
f"Generated new trace_id: {new_trace} → trace_id={trace_id}",
|
|
250
|
+
log=False,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Optional funcspan override
|
|
254
|
+
if funcspan_override_header:
|
|
255
|
+
try:
|
|
256
|
+
_sffuncspan_config.set_thread_override(funcspan_override_header)
|
|
257
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
258
|
+
print(
|
|
259
|
+
f"[[SailfishMiddleware.process_request]] Set function span override from header: {funcspan_override_header}",
|
|
260
|
+
log=False,
|
|
261
|
+
)
|
|
262
|
+
except Exception as e:
|
|
263
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
264
|
+
print(
|
|
265
|
+
f"[[SailfishMiddleware.process_request]] Failed to set function span override: {e}",
|
|
266
|
+
log=False,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Initialize outbound header base with parent trace ID
|
|
270
|
+
try:
|
|
271
|
+
trace_id = get_sf_trace_id()
|
|
272
|
+
if trace_id:
|
|
273
|
+
s = str(trace_id)
|
|
274
|
+
i = s.find("/") # session
|
|
275
|
+
j = s.find("/", i + 1) if i != -1 else -1 # page
|
|
276
|
+
if j != -1:
|
|
277
|
+
base_trace = s[:j] # "session/page"
|
|
278
|
+
set_outbound_header_base(
|
|
279
|
+
base_trace=base_trace,
|
|
280
|
+
parent_trace_id=s, # "session/page/uuid"
|
|
281
|
+
funcspan=funcspan_override_header,
|
|
282
|
+
)
|
|
283
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
284
|
+
print(
|
|
285
|
+
f"[[SailfishMiddleware.process_request]] Initialized outbound header base (base={base_trace[:16] if len(base_trace) > 16 else base_trace}...)",
|
|
286
|
+
log=False,
|
|
287
|
+
)
|
|
288
|
+
except Exception as e:
|
|
289
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
290
|
+
print(
|
|
291
|
+
f"[[SailfishMiddleware.process_request]] Failed to initialize outbound header base: {e}",
|
|
292
|
+
log=False,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# ------------------------------------------------------------------ #
|
|
296
|
+
# 4 | Network-hop emission (unchanged)
|
|
297
|
+
# ------------------------------------------------------------------ #
|
|
298
|
+
def process_view(self, request, view_func, view_args, view_kwargs):
|
|
299
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
300
|
+
print(
|
|
301
|
+
f"[[SailfishMiddleware.process_view]] view_func={view_func.__name__ if hasattr(view_func, '__name__') else view_func}, "
|
|
302
|
+
f"path={request.path}",
|
|
303
|
+
log=False,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
module = getattr(view_func, "__module__", "")
|
|
307
|
+
if module.startswith("strawberry"):
|
|
308
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
309
|
+
print(
|
|
310
|
+
f"[[Django.process_view]] Skipping Strawberry GraphQL view",
|
|
311
|
+
log=False,
|
|
312
|
+
)
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
# Unwrap decorated views to get the actual user code
|
|
316
|
+
# Django decorators (csrf_exempt, require_http_methods, etc.) wrap views
|
|
317
|
+
actual_view = _unwrap_user_func(view_func)
|
|
318
|
+
|
|
319
|
+
if actual_view is not view_func and SF_DEBUG and app_config._interceptors_initialized:
|
|
320
|
+
print(
|
|
321
|
+
f"[[Django.process_view]] Unwrapped decorator: "
|
|
322
|
+
f"{view_func.__name__} → {getattr(actual_view, '__name__', 'unknown')}",
|
|
323
|
+
log=False,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# Get code object and verify it's user code
|
|
327
|
+
code = getattr(actual_view, "__code__", None)
|
|
328
|
+
if not code:
|
|
329
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
330
|
+
print(
|
|
331
|
+
f"[[Django.process_view]] No code object for view_func", log=False
|
|
332
|
+
)
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
fname, lno = code.co_filename, code.co_firstlineno
|
|
336
|
+
|
|
337
|
+
# The unwrap function already checks for user code, but double-check
|
|
338
|
+
if not _is_user_code(fname):
|
|
339
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
340
|
+
print(f"[[Django.process_view]] Not user code: {fname}", log=False)
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
# Extract route pattern from Django's resolver
|
|
344
|
+
route_pattern = None
|
|
345
|
+
if hasattr(request, "resolver_match") and request.resolver_match:
|
|
346
|
+
route_pattern = getattr(request.resolver_match, "route", None)
|
|
347
|
+
|
|
348
|
+
# Check if route should be skipped
|
|
349
|
+
if should_skip_route(route_pattern, _ROUTES_TO_SKIP):
|
|
350
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
351
|
+
print(
|
|
352
|
+
f"[[Django.process_view]] Skipping view (route matches skip pattern): {route_pattern}",
|
|
353
|
+
log=False,
|
|
354
|
+
)
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
# Get or register endpoint_id (use actual_view for consistent tracking)
|
|
358
|
+
view_id = id(actual_view)
|
|
359
|
+
endpoint_id = _ENDPOINT_REGISTRY.get(view_id)
|
|
360
|
+
|
|
361
|
+
if endpoint_id is None:
|
|
362
|
+
# First time seeing this view - register it
|
|
363
|
+
view_name = getattr(actual_view, "__name__", "unknown")
|
|
364
|
+
endpoint_id = register_endpoint(
|
|
365
|
+
line=str(lno),
|
|
366
|
+
column="0",
|
|
367
|
+
name=view_name,
|
|
368
|
+
entrypoint=fname,
|
|
369
|
+
route=route_pattern,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
if endpoint_id >= 0:
|
|
373
|
+
_ENDPOINT_REGISTRY[view_id] = endpoint_id
|
|
374
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
375
|
+
print(
|
|
376
|
+
f"[[Django]] Registered endpoint: {view_name} "
|
|
377
|
+
f"({fname}:{lno}) → id={endpoint_id}",
|
|
378
|
+
log=False,
|
|
379
|
+
)
|
|
380
|
+
else:
|
|
381
|
+
# Failed to register, don't track
|
|
382
|
+
return None
|
|
383
|
+
|
|
384
|
+
# Store endpoint_id for process_response()
|
|
385
|
+
request._sf_endpoint_id = endpoint_id
|
|
386
|
+
|
|
387
|
+
# Capture request headers if enabled
|
|
388
|
+
if SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS:
|
|
389
|
+
try:
|
|
390
|
+
# Django stores headers in request.META with HTTP_ prefix
|
|
391
|
+
headers = {}
|
|
392
|
+
for key, value in request.META.items():
|
|
393
|
+
if key.startswith("HTTP_"):
|
|
394
|
+
# Remove HTTP_ prefix and convert to standard format
|
|
395
|
+
header_name = key[5:].replace("_", "-")
|
|
396
|
+
headers[header_name] = str(value)
|
|
397
|
+
elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"):
|
|
398
|
+
headers[key.replace("_", "-")] = str(value)
|
|
399
|
+
request._sf_request_headers = headers
|
|
400
|
+
except Exception:
|
|
401
|
+
request._sf_request_headers = None
|
|
402
|
+
|
|
403
|
+
# Capture request body if enabled
|
|
404
|
+
if SF_NETWORKHOP_CAPTURE_REQUEST_BODY:
|
|
405
|
+
try:
|
|
406
|
+
# Read body (Django caches it, so this is safe)
|
|
407
|
+
limit = SF_NETWORKHOP_REQUEST_LIMIT_MB * 1024 * 1024
|
|
408
|
+
body = request.body if hasattr(request, "body") else b""
|
|
409
|
+
if body and len(body) > limit:
|
|
410
|
+
body = body[:limit]
|
|
411
|
+
request._sf_request_body = body if body else None
|
|
412
|
+
except Exception as e:
|
|
413
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
414
|
+
print(f"[[Django]] Failed to capture request body: {e}", log=False)
|
|
415
|
+
request._sf_request_body = None
|
|
416
|
+
|
|
417
|
+
return None
|
|
418
|
+
|
|
419
|
+
# ------------------------------------------------------------------ #
|
|
420
|
+
# 5 | View-level exception hook (unchanged)
|
|
421
|
+
# ------------------------------------------------------------------ #
|
|
422
|
+
def process_response(self, request, response):
|
|
423
|
+
"""
|
|
424
|
+
Emit network hop AFTER response is built (OTEL-style zero-overhead).
|
|
425
|
+
Uses pre-registered endpoint_id for ultra-fast C path.
|
|
426
|
+
Captures response headers/body if enabled.
|
|
427
|
+
"""
|
|
428
|
+
endpoint_id = getattr(request, "_sf_endpoint_id", None)
|
|
429
|
+
|
|
430
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
431
|
+
print(
|
|
432
|
+
f"[[SailfishMiddleware.process_response]] endpoint_id={endpoint_id}, "
|
|
433
|
+
f"has_endpoint_attr={hasattr(request, '_sf_endpoint_id')}",
|
|
434
|
+
log=False,
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
if endpoint_id is not None and endpoint_id >= 0:
|
|
438
|
+
try:
|
|
439
|
+
_, session_id = get_or_set_sf_trace_id()
|
|
440
|
+
|
|
441
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
442
|
+
print(
|
|
443
|
+
f"[[SailfishMiddleware.process_response]] session_id={session_id}, "
|
|
444
|
+
f"endpoint_id={endpoint_id}, path={request.path}",
|
|
445
|
+
log=False,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# Capture response headers if enabled
|
|
449
|
+
resp_headers = None
|
|
450
|
+
if SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS:
|
|
451
|
+
try:
|
|
452
|
+
resp_headers = dict(response.items())
|
|
453
|
+
except Exception as e:
|
|
454
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
455
|
+
print(
|
|
456
|
+
f"[[Django]] Failed to capture response headers: {e}",
|
|
457
|
+
log=False,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
# Capture response body if enabled
|
|
461
|
+
resp_body = None
|
|
462
|
+
if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY:
|
|
463
|
+
try:
|
|
464
|
+
limit = SF_NETWORKHOP_RESPONSE_LIMIT_MB * 1024 * 1024
|
|
465
|
+
if hasattr(response, "content"):
|
|
466
|
+
resp_body = response.content[:limit]
|
|
467
|
+
except Exception as e:
|
|
468
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
469
|
+
print(
|
|
470
|
+
f"[[Django]] Failed to capture response body: {e}",
|
|
471
|
+
log=False,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
# Get request data if captured
|
|
475
|
+
req_headers = getattr(request, "_sf_request_headers", None)
|
|
476
|
+
req_body = getattr(request, "_sf_request_body", None)
|
|
477
|
+
|
|
478
|
+
# Extract raw path and query string for C to parse
|
|
479
|
+
raw_path = request.path # e.g., "/log"
|
|
480
|
+
raw_query = request.META.get("QUERY_STRING", "").encode(
|
|
481
|
+
"utf-8"
|
|
482
|
+
) # e.g., b"foo=5"
|
|
483
|
+
|
|
484
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
485
|
+
print(
|
|
486
|
+
f"[[Django]] About to emit network hop: endpoint_id={endpoint_id}, "
|
|
487
|
+
f"req_headers={'present' if req_headers else 'None'}, "
|
|
488
|
+
f"req_body={len(req_body) if req_body else 0} bytes, "
|
|
489
|
+
f"resp_headers={'present' if resp_headers else 'None'}, "
|
|
490
|
+
f"resp_body={len(resp_body) if resp_body else 0} bytes",
|
|
491
|
+
log=False,
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
# Direct C call - queues to background worker, returns instantly
|
|
495
|
+
# C will parse route and query_params from raw data
|
|
496
|
+
fast_send_network_hop_fast(
|
|
497
|
+
session_id=session_id,
|
|
498
|
+
endpoint_id=endpoint_id,
|
|
499
|
+
raw_path=raw_path,
|
|
500
|
+
raw_query_string=raw_query,
|
|
501
|
+
request_headers=req_headers,
|
|
502
|
+
request_body=req_body,
|
|
503
|
+
response_headers=resp_headers,
|
|
504
|
+
response_body=resp_body,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
508
|
+
print(
|
|
509
|
+
f"[[Django]] Emitted network hop: endpoint_id={endpoint_id} "
|
|
510
|
+
f"session={session_id}",
|
|
511
|
+
log=False,
|
|
512
|
+
)
|
|
513
|
+
except Exception as e: # noqa: BLE001 S110
|
|
514
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
515
|
+
print(f"[[Django]] Failed to emit network hop: {e}", log=False)
|
|
516
|
+
|
|
517
|
+
traceback.print_exc()
|
|
518
|
+
|
|
519
|
+
# Clear function span override for this request (thread-local cleanup)
|
|
520
|
+
try:
|
|
521
|
+
_sffuncspan_config.clear_thread_override()
|
|
522
|
+
except Exception:
|
|
523
|
+
pass
|
|
524
|
+
|
|
525
|
+
# CRITICAL: Clear C TLS to prevent stale data in thread pools
|
|
526
|
+
try:
|
|
527
|
+
clear_c_tls_parent_trace_id()
|
|
528
|
+
except Exception:
|
|
529
|
+
pass
|
|
530
|
+
|
|
531
|
+
# CRITICAL: Clear outbound header base to prevent stale cached headers
|
|
532
|
+
# ContextVar does NOT automatically clean up in thread pools - must clear explicitly
|
|
533
|
+
try:
|
|
534
|
+
clear_outbound_header_base()
|
|
535
|
+
except Exception:
|
|
536
|
+
pass
|
|
537
|
+
|
|
538
|
+
# CRITICAL: Clear trace_id to ensure fresh generation for next request
|
|
539
|
+
# Without this, get_or_set_sf_trace_id() reuses trace_id from previous request
|
|
540
|
+
# causing X-Sf4-Prid to stay constant when no incoming X-Sf3-Rid header
|
|
541
|
+
try:
|
|
542
|
+
clear_trace_id()
|
|
543
|
+
except Exception:
|
|
544
|
+
pass
|
|
545
|
+
|
|
546
|
+
return response
|
|
547
|
+
|
|
548
|
+
# ------------------------------------------------------------------ #
|
|
549
|
+
# 6 | View-level exception hook (unchanged)
|
|
550
|
+
# ------------------------------------------------------------------ #
|
|
551
|
+
def process_exception(self, request, exception):
|
|
552
|
+
print("[[SailfishMiddleware.process_exception]]", log=False)
|
|
553
|
+
custom_excepthook(type(exception), exception, exception.__traceback__)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
# --------------------------------------------------------------------------- #
|
|
557
|
+
# Helper – patch django.core.wsgi.get_wsgi_application once
|
|
558
|
+
# --------------------------------------------------------------------------- #
|
|
559
|
+
# --------------------------------------------------------------------------- #
|
|
560
|
+
# Helper – patch django.core.asgi.get_asgi_application once
|
|
561
|
+
# --------------------------------------------------------------------------- #
|
|
562
|
+
def _patch_get_asgi_application() -> None:
|
|
563
|
+
"""
|
|
564
|
+
Replace ``django.core.asgi.get_asgi_application`` with a wrapper that:
|
|
565
|
+
|
|
566
|
+
1. Runs ``django.setup()`` (as the original does),
|
|
567
|
+
2. **Then** injects ``SailfishMiddleware`` into *settings.MIDDLEWARE*
|
|
568
|
+
*after* settings are configured but *before* the first ``ASGIHandler``
|
|
569
|
+
is built,
|
|
570
|
+
3. Returns the handler (ASGI handlers handle exceptions internally).
|
|
571
|
+
|
|
572
|
+
This mirrors the WSGI patching approach.
|
|
573
|
+
"""
|
|
574
|
+
try:
|
|
575
|
+
from django.core import asgi as _asgi_mod
|
|
576
|
+
except ImportError: # pragma: no cover
|
|
577
|
+
return
|
|
578
|
+
|
|
579
|
+
if getattr(_asgi_mod, "_sf_patched", False):
|
|
580
|
+
return # idempotent
|
|
581
|
+
|
|
582
|
+
_orig_get_asgi = _asgi_mod.get_asgi_application
|
|
583
|
+
_MW_PATH = "sf_veritas.patches.web_frameworks.django.SailfishMiddleware"
|
|
584
|
+
|
|
585
|
+
def _sf_get_asgi_application(*args, **kwargs):
|
|
586
|
+
# --- Step 1: exactly replicate original behaviour -----------------
|
|
587
|
+
import django
|
|
588
|
+
|
|
589
|
+
django.setup(set_prefix=False) # configures settings & apps
|
|
590
|
+
|
|
591
|
+
# --- Step 2: inject middleware *now* (settings are configured) ----
|
|
592
|
+
from django.conf import settings
|
|
593
|
+
|
|
594
|
+
if (
|
|
595
|
+
hasattr(settings, "MIDDLEWARE")
|
|
596
|
+
and isinstance(settings.MIDDLEWARE, list)
|
|
597
|
+
and _MW_PATH not in settings.MIDDLEWARE
|
|
598
|
+
):
|
|
599
|
+
settings.MIDDLEWARE.insert(0, _MW_PATH)
|
|
600
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
601
|
+
print(f"[[_patch_get_asgi_application]] Injected {_MW_PATH}", log=False)
|
|
602
|
+
|
|
603
|
+
# --- Step 2.5: inject CORS headers if configured ----
|
|
604
|
+
if hasattr(settings, 'CORS_ALLOW_HEADERS'):
|
|
605
|
+
original_headers = settings.CORS_ALLOW_HEADERS
|
|
606
|
+
|
|
607
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
608
|
+
print(
|
|
609
|
+
f"[[_patch_get_asgi_application]] Found CORS_ALLOW_HEADERS: {original_headers}",
|
|
610
|
+
log=False,
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
if should_inject_headers(original_headers):
|
|
614
|
+
patched_headers = inject_sailfish_headers(original_headers)
|
|
615
|
+
settings.CORS_ALLOW_HEADERS = patched_headers
|
|
616
|
+
|
|
617
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
618
|
+
print(
|
|
619
|
+
f"[[_patch_get_asgi_application]] Injected Sailfish headers into CORS_ALLOW_HEADERS: {patched_headers}",
|
|
620
|
+
log=False,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
# --- Step 3: build and return ASGI handler ----
|
|
624
|
+
return _orig_get_asgi(*args, **kwargs)
|
|
625
|
+
|
|
626
|
+
_asgi_mod.get_asgi_application = _sf_get_asgi_application
|
|
627
|
+
_asgi_mod._sf_patched = True
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
# --------------------------------------------------------------------------- #
|
|
631
|
+
# Helper – patch django.core.wsgi.get_wsgi_application once
|
|
632
|
+
# --------------------------------------------------------------------------- #
|
|
633
|
+
def _patch_get_wsgi_application() -> None:
|
|
634
|
+
"""
|
|
635
|
+
Replace ``django.core.wsgi.get_wsgi_application`` with a wrapper that:
|
|
636
|
+
|
|
637
|
+
1. Runs ``django.setup()`` (as the original does),
|
|
638
|
+
2. **Then** injects ``SailfishMiddleware`` into *settings.MIDDLEWARE*
|
|
639
|
+
*after* settings are configured but *before* the first ``WSGIHandler``
|
|
640
|
+
is built,
|
|
641
|
+
3. Wraps the returned handler in our ``CustomExceptionMiddleware`` so we
|
|
642
|
+
still have a last-chance catcher outside Django's stack.
|
|
643
|
+
|
|
644
|
+
This mirrors the flow used by Sentry's Django integration.
|
|
645
|
+
"""
|
|
646
|
+
try:
|
|
647
|
+
from django.core import wsgi as _wsgi_mod
|
|
648
|
+
except ImportError: # pragma: no cover
|
|
649
|
+
return
|
|
650
|
+
|
|
651
|
+
if getattr(_wsgi_mod, "_sf_patched", False):
|
|
652
|
+
return # idempotent
|
|
653
|
+
|
|
654
|
+
_orig_get_wsgi = _wsgi_mod.get_wsgi_application
|
|
655
|
+
_MW_PATH = "sf_veritas.patches.web_frameworks.django.SailfishMiddleware"
|
|
656
|
+
|
|
657
|
+
def _sf_get_wsgi_application(*args, **kwargs):
|
|
658
|
+
# --- Step 1: exactly replicate original behaviour -----------------
|
|
659
|
+
import django
|
|
660
|
+
|
|
661
|
+
django.setup(set_prefix=False) # configures settings & apps
|
|
662
|
+
|
|
663
|
+
# --- Step 2: inject middleware *now* (settings are configured) ----
|
|
664
|
+
from django.conf import settings
|
|
665
|
+
|
|
666
|
+
if (
|
|
667
|
+
hasattr(settings, "MIDDLEWARE")
|
|
668
|
+
and isinstance(settings.MIDDLEWARE, list)
|
|
669
|
+
and _MW_PATH not in settings.MIDDLEWARE
|
|
670
|
+
):
|
|
671
|
+
settings.MIDDLEWARE.insert(0, _MW_PATH)
|
|
672
|
+
|
|
673
|
+
# --- Step 2.5: inject CORS headers if configured ----
|
|
674
|
+
if hasattr(settings, 'CORS_ALLOW_HEADERS'):
|
|
675
|
+
original_headers = settings.CORS_ALLOW_HEADERS
|
|
676
|
+
|
|
677
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
678
|
+
print(
|
|
679
|
+
f"[[_patch_get_wsgi_application]] Found CORS_ALLOW_HEADERS: {original_headers}",
|
|
680
|
+
log=False,
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
if should_inject_headers(original_headers):
|
|
684
|
+
patched_headers = inject_sailfish_headers(original_headers)
|
|
685
|
+
settings.CORS_ALLOW_HEADERS = patched_headers
|
|
686
|
+
|
|
687
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
688
|
+
print(
|
|
689
|
+
f"[[_patch_get_wsgi_application]] Injected Sailfish headers into CORS_ALLOW_HEADERS: {patched_headers}",
|
|
690
|
+
log=False,
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
# --- Step 3: build handler and wrap for last-chance exceptions ----
|
|
694
|
+
from django.core.handlers.wsgi import WSGIHandler
|
|
695
|
+
from sf_veritas.patches.web_frameworks.django import CustomExceptionMiddleware
|
|
696
|
+
|
|
697
|
+
handler = WSGIHandler()
|
|
698
|
+
return CustomExceptionMiddleware(handler)
|
|
699
|
+
|
|
700
|
+
_wsgi_mod.get_wsgi_application = _sf_get_wsgi_application
|
|
701
|
+
_wsgi_mod._sf_patched = True
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def patch_django_middleware(routes_to_skip: Optional[List[str]] = None) -> None:
|
|
705
|
+
"""
|
|
706
|
+
Public entry-point called by ``setup_interceptors``.
|
|
707
|
+
|
|
708
|
+
• Inserts ``SailfishMiddleware`` for *already-configured* settings
|
|
709
|
+
(run-server or ASGI).
|
|
710
|
+
• Patches ``get_wsgi_application`` so *future* WSGI handlers created
|
|
711
|
+
by third-party code inherit the middleware without relying on a
|
|
712
|
+
configured settings object at import time.
|
|
713
|
+
"""
|
|
714
|
+
global _ROUTES_TO_SKIP
|
|
715
|
+
_ROUTES_TO_SKIP = routes_to_skip or []
|
|
716
|
+
|
|
717
|
+
try:
|
|
718
|
+
from django.conf import settings
|
|
719
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
720
|
+
except ImportError: # Django not installed
|
|
721
|
+
return
|
|
722
|
+
|
|
723
|
+
_MW_PATH = "sf_veritas.patches.web_frameworks.django.SailfishMiddleware"
|
|
724
|
+
|
|
725
|
+
# ---------- If settings are *already* configured, patch immediately ---
|
|
726
|
+
try:
|
|
727
|
+
if settings.configured and isinstance(
|
|
728
|
+
getattr(settings, "MIDDLEWARE", None), list
|
|
729
|
+
):
|
|
730
|
+
if _MW_PATH not in settings.MIDDLEWARE:
|
|
731
|
+
settings.MIDDLEWARE.insert(0, _MW_PATH)
|
|
732
|
+
except ImproperlyConfigured:
|
|
733
|
+
# Settings not yet configured – safe to ignore; the WSGI patch below
|
|
734
|
+
# will handle insertion once ``django.setup()`` runs.
|
|
735
|
+
pass
|
|
736
|
+
|
|
737
|
+
# ---------- Always patch get_wsgi/asgi_application (idempotent) ------------
|
|
738
|
+
_patch_get_wsgi_application()
|
|
739
|
+
_patch_get_asgi_application()
|
|
740
|
+
|
|
741
|
+
# ---------- Patch CORS to inject Sailfish headers (idempotent) ------------
|
|
742
|
+
patch_django_cors()
|
|
743
|
+
|
|
744
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
745
|
+
print(
|
|
746
|
+
"[[patch_django_middleware]] Sailfish Django integration ready", log=False
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
class CustomExceptionMiddleware:
|
|
751
|
+
"""
|
|
752
|
+
A universal last-chance exception wrapper that works for either
|
|
753
|
+
• ASGI call signature: (scope, receive, send) → coroutine
|
|
754
|
+
• WSGI call signature: (environ, start_response) → iterable
|
|
755
|
+
Every un-handled exception is funneled through ``custom_excepthook`` once.
|
|
756
|
+
"""
|
|
757
|
+
|
|
758
|
+
def __init__(self, app):
|
|
759
|
+
self.app = app
|
|
760
|
+
|
|
761
|
+
# ------------------------------------------------------------------ #
|
|
762
|
+
# Dispatcher – routes ASGI vs WSGI based on arity / argument shape
|
|
763
|
+
# ------------------------------------------------------------------ #
|
|
764
|
+
def __call__(self, *args, **kwargs):
|
|
765
|
+
if len(args) == 3:
|
|
766
|
+
# Heuristic: (scope, receive, send) for ASGI
|
|
767
|
+
return self._asgi_call(*args) # returns coroutine
|
|
768
|
+
# Else assume classic WSGI: (environ, start_response)
|
|
769
|
+
return self._wsgi_call(*args) # returns iterable
|
|
770
|
+
|
|
771
|
+
# ------------------------------------------------------------------ #
|
|
772
|
+
# ASGI branch
|
|
773
|
+
# ------------------------------------------------------------------ #
|
|
774
|
+
async def _asgi_call(self, scope, receive, send):
|
|
775
|
+
try:
|
|
776
|
+
await self.app(scope, receive, send)
|
|
777
|
+
except Exception as exc: # noqa: BLE001
|
|
778
|
+
custom_excepthook(type(exc), exc, exc.__traceback__)
|
|
779
|
+
raise
|
|
780
|
+
finally:
|
|
781
|
+
# CRITICAL: Clear C TLS to prevent stale data in thread pools
|
|
782
|
+
try:
|
|
783
|
+
clear_c_tls_parent_trace_id()
|
|
784
|
+
except Exception:
|
|
785
|
+
pass
|
|
786
|
+
|
|
787
|
+
# CRITICAL: Clear outbound header base to prevent stale cached headers
|
|
788
|
+
try:
|
|
789
|
+
clear_outbound_header_base()
|
|
790
|
+
except Exception:
|
|
791
|
+
pass
|
|
792
|
+
|
|
793
|
+
# CRITICAL: Clear trace_id to ensure fresh generation for next request
|
|
794
|
+
try:
|
|
795
|
+
clear_trace_id()
|
|
796
|
+
except Exception:
|
|
797
|
+
pass
|
|
798
|
+
|
|
799
|
+
# ------------------------------------------------------------------ #
|
|
800
|
+
# WSGI branch
|
|
801
|
+
# ------------------------------------------------------------------ #
|
|
802
|
+
def _wsgi_call(self, environ, start_response):
|
|
803
|
+
try:
|
|
804
|
+
return self.app(environ, start_response)
|
|
805
|
+
except Exception as exc: # noqa: BLE001
|
|
806
|
+
custom_excepthook(type(exc), exc, exc.__traceback__)
|
|
807
|
+
raise
|
|
808
|
+
finally:
|
|
809
|
+
# CRITICAL: Clear C TLS to prevent stale data in thread pools
|
|
810
|
+
try:
|
|
811
|
+
clear_c_tls_parent_trace_id()
|
|
812
|
+
except Exception:
|
|
813
|
+
pass
|
|
814
|
+
|
|
815
|
+
# CRITICAL: Clear outbound header base to prevent stale cached headers
|
|
816
|
+
try:
|
|
817
|
+
clear_outbound_header_base()
|
|
818
|
+
except Exception:
|
|
819
|
+
pass
|
|
820
|
+
|
|
821
|
+
# CRITICAL: Clear trace_id to ensure fresh generation for next request
|
|
822
|
+
try:
|
|
823
|
+
clear_trace_id()
|
|
824
|
+
except Exception:
|
|
825
|
+
pass
|
|
826
|
+
|
|
827
|
+
# ------------------------------------------------------------------ #
|
|
828
|
+
# Delegate attribute access so the wrapped app still behaves normally
|
|
829
|
+
# ------------------------------------------------------------------ #
|
|
830
|
+
def __getattr__(self, attr):
|
|
831
|
+
return getattr(self.app, attr)
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
# --------------------------------------------------------------------------- #
|
|
835
|
+
# CORS Header Injection – django-cors-headers
|
|
836
|
+
# --------------------------------------------------------------------------- #
|
|
837
|
+
def patch_django_cors():
|
|
838
|
+
"""
|
|
839
|
+
Patch django-cors-headers to automatically inject Sailfish headers.
|
|
840
|
+
|
|
841
|
+
Two-pronged approach:
|
|
842
|
+
1. Directly modify Django settings.CORS_ALLOW_HEADERS if already configured
|
|
843
|
+
2. Patch corsheaders.conf property to inject headers dynamically
|
|
844
|
+
|
|
845
|
+
SAFE: Only modifies CORS if django-cors-headers is installed and configured.
|
|
846
|
+
"""
|
|
847
|
+
try:
|
|
848
|
+
from django.conf import settings
|
|
849
|
+
except ImportError:
|
|
850
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
851
|
+
print(
|
|
852
|
+
"[[patch_django_cors]] Django not available, skipping",
|
|
853
|
+
log=False,
|
|
854
|
+
)
|
|
855
|
+
return
|
|
856
|
+
|
|
857
|
+
try:
|
|
858
|
+
from corsheaders import conf as cors_conf
|
|
859
|
+
except ImportError:
|
|
860
|
+
# django-cors-headers not installed, skip patching
|
|
861
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
862
|
+
print(
|
|
863
|
+
"[[patch_django_cors]] django-cors-headers not installed, skipping",
|
|
864
|
+
log=False,
|
|
865
|
+
)
|
|
866
|
+
return
|
|
867
|
+
|
|
868
|
+
# Check if already patched
|
|
869
|
+
if hasattr(cors_conf, "_sf_cors_patched"):
|
|
870
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
871
|
+
print("[[patch_django_cors]] Already patched, skipping", log=False)
|
|
872
|
+
return
|
|
873
|
+
|
|
874
|
+
# APPROACH 1: Directly modify Django settings if CORS is configured
|
|
875
|
+
try:
|
|
876
|
+
if hasattr(settings, 'CORS_ALLOW_HEADERS'):
|
|
877
|
+
original_headers = settings.CORS_ALLOW_HEADERS
|
|
878
|
+
|
|
879
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
880
|
+
print(
|
|
881
|
+
f"[[patch_django_cors]] Found CORS_ALLOW_HEADERS in settings: {original_headers}",
|
|
882
|
+
log=False,
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
if should_inject_headers(original_headers):
|
|
886
|
+
patched_headers = inject_sailfish_headers(original_headers)
|
|
887
|
+
settings.CORS_ALLOW_HEADERS = patched_headers
|
|
888
|
+
|
|
889
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
890
|
+
print(
|
|
891
|
+
f"[[patch_django_cors]] Modified settings.CORS_ALLOW_HEADERS to: {patched_headers}",
|
|
892
|
+
log=False,
|
|
893
|
+
)
|
|
894
|
+
except Exception as e:
|
|
895
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
896
|
+
print(f"[[patch_django_cors]] Failed to modify settings: {e}", log=False)
|
|
897
|
+
|
|
898
|
+
# APPROACH 2: Patch the Conf class property for dynamic access
|
|
899
|
+
try:
|
|
900
|
+
conf_class = type(cors_conf)
|
|
901
|
+
|
|
902
|
+
if hasattr(conf_class, 'CORS_ALLOW_HEADERS'):
|
|
903
|
+
original_property = getattr(conf_class, 'CORS_ALLOW_HEADERS')
|
|
904
|
+
|
|
905
|
+
if isinstance(original_property, property):
|
|
906
|
+
original_fget = original_property.fget
|
|
907
|
+
|
|
908
|
+
def patched_fget(self):
|
|
909
|
+
original_headers = original_fget(self)
|
|
910
|
+
|
|
911
|
+
if should_inject_headers(original_headers):
|
|
912
|
+
patched_headers = inject_sailfish_headers(original_headers)
|
|
913
|
+
|
|
914
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
915
|
+
print(
|
|
916
|
+
f"[[patch_django_cors]] Property access: injected headers -> {patched_headers}",
|
|
917
|
+
log=False,
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
return patched_headers
|
|
921
|
+
|
|
922
|
+
return original_headers
|
|
923
|
+
|
|
924
|
+
setattr(
|
|
925
|
+
conf_class,
|
|
926
|
+
'CORS_ALLOW_HEADERS',
|
|
927
|
+
property(patched_fget, original_property.fset, original_property.fdel)
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
931
|
+
print(
|
|
932
|
+
"[[patch_django_cors]] Successfully patched CORS_ALLOW_HEADERS property",
|
|
933
|
+
log=False,
|
|
934
|
+
)
|
|
935
|
+
except Exception as e:
|
|
936
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
937
|
+
print(f"[[patch_django_cors]] Failed to patch property: {e}", log=False)
|
|
938
|
+
|
|
939
|
+
cors_conf._sf_cors_patched = True
|
|
940
|
+
|
|
941
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
942
|
+
print(
|
|
943
|
+
"[[patch_django_cors]] Successfully patched django-cors-headers", log=False
|
|
944
|
+
)
|