sf-veritas 0.10.3__cp311-cp311-manylinux_2_28_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sf-veritas might be problematic. Click here for more details.
- sf_veritas/__init__.py +20 -0
- sf_veritas/_sffastlog.c +889 -0
- sf_veritas/_sffastlog.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnet.c +924 -0
- sf_veritas/_sffastnet.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnetworkrequest.c +730 -0
- sf_veritas/_sffastnetworkrequest.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan.c +2155 -0
- sf_veritas/_sffuncspan.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan_config.c +617 -0
- sf_veritas/_sffuncspan_config.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfheadercheck.c +341 -0
- sf_veritas/_sfheadercheck.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfnetworkhop.c +1451 -0
- sf_veritas/_sfnetworkhop.cpython-311-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfservice.c +1175 -0
- sf_veritas/_sfservice.cpython-311-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,813 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
import traceback
|
|
5
|
+
from importlib.util import find_spec
|
|
6
|
+
from typing import Any, Callable, Set, Tuple
|
|
7
|
+
|
|
8
|
+
from ... import app_config
|
|
9
|
+
from ...custom_excepthook import custom_excepthook
|
|
10
|
+
from ...env_vars import (
|
|
11
|
+
CAPTURE_STRAWBERRY_ERRORS_WITH_DATA,
|
|
12
|
+
PRINT_CONFIGURATION_STATUSES,
|
|
13
|
+
SF_DEBUG,
|
|
14
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_BODY,
|
|
15
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS,
|
|
16
|
+
SF_NETWORKHOP_CAPTURE_RESPONSE_BODY,
|
|
17
|
+
SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS,
|
|
18
|
+
SF_NETWORKHOP_REQUEST_LIMIT_MB,
|
|
19
|
+
SF_NETWORKHOP_RESPONSE_LIMIT_MB,
|
|
20
|
+
STRAWBERRY_DEBUG,
|
|
21
|
+
)
|
|
22
|
+
from ...fast_network_hop import (
|
|
23
|
+
fast_send_network_hop,
|
|
24
|
+
fast_send_network_hop_fast,
|
|
25
|
+
register_endpoint,
|
|
26
|
+
)
|
|
27
|
+
from ...thread_local import get_or_set_sf_trace_id
|
|
28
|
+
from ...transmit_exception_to_sailfish import transmit_exception_to_sailfish
|
|
29
|
+
from .utils import _is_user_code, _unwrap_user_func
|
|
30
|
+
|
|
31
|
+
# JSON serialization - try fast orjson first, fallback to stdlib json
|
|
32
|
+
try:
|
|
33
|
+
import orjson
|
|
34
|
+
|
|
35
|
+
HAS_ORJSON = True
|
|
36
|
+
except ImportError:
|
|
37
|
+
import json
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
# Size limits in bytes
|
|
43
|
+
_REQUEST_LIMIT_BYTES = SF_NETWORKHOP_REQUEST_LIMIT_MB * 1024 * 1024
|
|
44
|
+
_RESPONSE_LIMIT_BYTES = SF_NETWORKHOP_RESPONSE_LIMIT_MB * 1024 * 1024
|
|
45
|
+
|
|
46
|
+
# Pre-registered endpoint IDs
|
|
47
|
+
_ENDPOINT_REGISTRY: dict[tuple, int] = {}
|
|
48
|
+
|
|
49
|
+
# Track if Strawberry has already been patched to prevent multiple patches
|
|
50
|
+
_is_strawberry_patched = False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Cache for function definition line numbers (keyed by code object id)
|
|
54
|
+
_FUNCTION_DEF_LINE_CACHE: dict[int, int] = {}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _get_function_def_line(frame):
|
|
58
|
+
"""
|
|
59
|
+
Get the line number of the 'def' statement, skipping decorators.
|
|
60
|
+
|
|
61
|
+
Python's co_firstlineno includes decorators, so we need to scan the source
|
|
62
|
+
to find the actual function definition line.
|
|
63
|
+
|
|
64
|
+
PERFORMANCE: Results are cached by code object ID, so the file I/O only
|
|
65
|
+
happens once per function (first request). Subsequent requests are instant.
|
|
66
|
+
"""
|
|
67
|
+
code_id = id(frame.f_code)
|
|
68
|
+
|
|
69
|
+
# Check cache first - this is the fast path for all requests after the first
|
|
70
|
+
if code_id in _FUNCTION_DEF_LINE_CACHE:
|
|
71
|
+
return _FUNCTION_DEF_LINE_CACHE[code_id]
|
|
72
|
+
|
|
73
|
+
# Cache miss - do the expensive source file lookup
|
|
74
|
+
try:
|
|
75
|
+
# Get source lines for this code object (SLOW: reads file from disk)
|
|
76
|
+
source_lines, start_line = inspect.getsourcelines(frame.f_code)
|
|
77
|
+
|
|
78
|
+
# Find the first line that starts with 'def' or 'async def'
|
|
79
|
+
for i, line in enumerate(source_lines):
|
|
80
|
+
stripped = line.strip()
|
|
81
|
+
if stripped.startswith("def ") or stripped.startswith("async def "):
|
|
82
|
+
def_line = start_line + i
|
|
83
|
+
# Cache the result for next time
|
|
84
|
+
_FUNCTION_DEF_LINE_CACHE[code_id] = def_line
|
|
85
|
+
return def_line
|
|
86
|
+
|
|
87
|
+
# Fallback: return co_firstlineno if we can't find def
|
|
88
|
+
result = frame.f_code.co_firstlineno
|
|
89
|
+
_FUNCTION_DEF_LINE_CACHE[code_id] = result
|
|
90
|
+
return result
|
|
91
|
+
except Exception:
|
|
92
|
+
# If anything fails, fallback to co_firstlineno
|
|
93
|
+
result = frame.f_code.co_firstlineno
|
|
94
|
+
_FUNCTION_DEF_LINE_CACHE[code_id] = result
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_extension():
|
|
99
|
+
from strawberry.extensions import SchemaExtension
|
|
100
|
+
|
|
101
|
+
class CustomErrorHandlingExtension(SchemaExtension):
|
|
102
|
+
def __init__(self, *, execution_context):
|
|
103
|
+
self.execution_context = execution_context
|
|
104
|
+
|
|
105
|
+
def on_request_start(self):
|
|
106
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
107
|
+
print("Starting GraphQL request", log=False)
|
|
108
|
+
|
|
109
|
+
def on_request_end(self):
|
|
110
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
111
|
+
print("Ending GraphQL request", log=False)
|
|
112
|
+
if not self.execution_context.errors:
|
|
113
|
+
return
|
|
114
|
+
for error in self.execution_context.errors:
|
|
115
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
116
|
+
print(f"Handling GraphQL error: {error}", log=False)
|
|
117
|
+
custom_excepthook(type(error), error, error.__traceback__)
|
|
118
|
+
|
|
119
|
+
def on_validation_start(self):
|
|
120
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
121
|
+
print("Starting validation of GraphQL request", log=False)
|
|
122
|
+
|
|
123
|
+
def on_validation_end(self):
|
|
124
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
125
|
+
print("Ending validation of GraphQL request", log=False)
|
|
126
|
+
|
|
127
|
+
def on_execution_start(self):
|
|
128
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
129
|
+
print("Starting execution of GraphQL request", log=False)
|
|
130
|
+
|
|
131
|
+
def on_resolver_start(self, resolver, obj, info, **kwargs):
|
|
132
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
133
|
+
print(f"Starting resolver {resolver.__name__}", log=False)
|
|
134
|
+
|
|
135
|
+
def on_resolver_end(self, resolver, obj, info, **kwargs):
|
|
136
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
137
|
+
print(f"Ending resolver {resolver.__name__}", log=False)
|
|
138
|
+
|
|
139
|
+
def on_error(self, error: Exception):
|
|
140
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
141
|
+
print(f"Handling error in resolver: {error}", log=False)
|
|
142
|
+
custom_excepthook(type(error), error, error.__traceback__)
|
|
143
|
+
|
|
144
|
+
return CustomErrorHandlingExtension
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def get_network_hop_extension() -> "type[SchemaExtension]":
|
|
148
|
+
"""
|
|
149
|
+
Strawberry SchemaExtension that emits a collectNetworkHops mutation for the
|
|
150
|
+
*first* user-land frame executed inside every resolver (sync or async).
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
from strawberry.extensions import SchemaExtension
|
|
154
|
+
|
|
155
|
+
# --------------------------------------------------------------------- #
|
|
156
|
+
# Helper predicates
|
|
157
|
+
# --------------------------------------------------------------------- #
|
|
158
|
+
# Extended dig: __wrapped__, closure cells *and* common attribute names
|
|
159
|
+
# --------------------------------------------------------------------- #
|
|
160
|
+
# Extension class
|
|
161
|
+
# --------------------------------------------------------------------- #
|
|
162
|
+
class NetworkHopExtension(SchemaExtension):
|
|
163
|
+
supports_sync = supports_async = True
|
|
164
|
+
_sent: Set[Tuple[str, int]] = set() # class-level: de-dupe per request
|
|
165
|
+
|
|
166
|
+
def __init__(self, *, execution_context):
|
|
167
|
+
super().__init__(execution_context=execution_context)
|
|
168
|
+
self._captured_endpoints = (
|
|
169
|
+
[]
|
|
170
|
+
) # Store endpoint info for post-response emission
|
|
171
|
+
self._request_data = {} # Store request headers/body
|
|
172
|
+
self._response_data = {} # Store response headers/body
|
|
173
|
+
|
|
174
|
+
# ---------------- internal capture helper ---------------- #
|
|
175
|
+
def _capture(self, frame, info):
|
|
176
|
+
"""OTEL-STYLE: Capture endpoint metadata and pre-register."""
|
|
177
|
+
filename = frame.f_code.co_filename
|
|
178
|
+
func_name = frame.f_code.co_name
|
|
179
|
+
|
|
180
|
+
# Get the actual function definition line (skipping decorators)
|
|
181
|
+
line_no = _get_function_def_line(frame)
|
|
182
|
+
|
|
183
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
184
|
+
print(
|
|
185
|
+
f"[[Strawberry]] _capture: {func_name} @ {filename} "
|
|
186
|
+
f"co_firstlineno={frame.f_code.co_firstlineno} -> def_line={line_no}",
|
|
187
|
+
log=False,
|
|
188
|
+
)
|
|
189
|
+
if (filename, line_no) in NetworkHopExtension._sent:
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
hop_key = (filename, line_no)
|
|
193
|
+
|
|
194
|
+
# Pre-register endpoint if not already registered
|
|
195
|
+
endpoint_id = _ENDPOINT_REGISTRY.get(hop_key)
|
|
196
|
+
if endpoint_id is None:
|
|
197
|
+
endpoint_id = register_endpoint(
|
|
198
|
+
line=str(line_no),
|
|
199
|
+
column="0",
|
|
200
|
+
name=func_name,
|
|
201
|
+
entrypoint=filename,
|
|
202
|
+
route=None,
|
|
203
|
+
)
|
|
204
|
+
if endpoint_id >= 0:
|
|
205
|
+
_ENDPOINT_REGISTRY[hop_key] = endpoint_id
|
|
206
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
207
|
+
print(
|
|
208
|
+
f"[[Strawberry]] Registered resolver: {func_name} @ "
|
|
209
|
+
f"{filename}:{line_no} (id={endpoint_id})",
|
|
210
|
+
log=False,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Store for on_request_end to emit
|
|
214
|
+
self._captured_endpoints.append(
|
|
215
|
+
{
|
|
216
|
+
"filename": filename,
|
|
217
|
+
"line": line_no,
|
|
218
|
+
"name": func_name,
|
|
219
|
+
"endpoint_id": endpoint_id,
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
NetworkHopExtension._sent.add((filename, line_no))
|
|
223
|
+
|
|
224
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
225
|
+
print(
|
|
226
|
+
f"[[Strawberry]] Captured resolver: {func_name} "
|
|
227
|
+
f"({filename}:{line_no}) endpoint_id={endpoint_id}",
|
|
228
|
+
log=False,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# ---------------- tracer factory ---------------- #
|
|
232
|
+
def _make_tracer(self, info):
|
|
233
|
+
def tracer(frame, event, arg):
|
|
234
|
+
if event.startswith("c_"):
|
|
235
|
+
return
|
|
236
|
+
if event == "call":
|
|
237
|
+
if _is_user_code(frame.f_code.co_filename):
|
|
238
|
+
self._capture(frame, info)
|
|
239
|
+
sys.setprofile(None)
|
|
240
|
+
return
|
|
241
|
+
return tracer # keep tracing until we hit user code
|
|
242
|
+
|
|
243
|
+
return tracer
|
|
244
|
+
|
|
245
|
+
# ---------------- request/response capture ---------------- #
|
|
246
|
+
def on_request_start(self):
|
|
247
|
+
"""Capture GraphQL request data when request starts."""
|
|
248
|
+
# IMPORTANT: Clear captured endpoints from previous requests
|
|
249
|
+
# SchemaExtension instances may be reused across requests
|
|
250
|
+
self._captured_endpoints = []
|
|
251
|
+
self._request_data = {}
|
|
252
|
+
self._response_data = {}
|
|
253
|
+
|
|
254
|
+
if (
|
|
255
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS
|
|
256
|
+
or SF_NETWORKHOP_CAPTURE_REQUEST_BODY
|
|
257
|
+
):
|
|
258
|
+
try:
|
|
259
|
+
# Access the GraphQL query from execution context
|
|
260
|
+
if (
|
|
261
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_BODY
|
|
262
|
+
and self.execution_context.query
|
|
263
|
+
):
|
|
264
|
+
|
|
265
|
+
query_data = {
|
|
266
|
+
"query": self.execution_context.query,
|
|
267
|
+
"variables": self.execution_context.variables or {},
|
|
268
|
+
"operation_name": self.execution_context.operation_name,
|
|
269
|
+
}
|
|
270
|
+
# Convert to JSON string and limit size
|
|
271
|
+
if HAS_ORJSON:
|
|
272
|
+
query_str = orjson.dumps(query_data)[:_REQUEST_LIMIT_BYTES]
|
|
273
|
+
else:
|
|
274
|
+
query_str = json.dumps(query_data)[:_REQUEST_LIMIT_BYTES]
|
|
275
|
+
self._request_data["body"] = query_str.encode("utf-8")
|
|
276
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
277
|
+
print(
|
|
278
|
+
f"[[Strawberry]] Captured GraphQL query: {len(query_str)} chars",
|
|
279
|
+
log=False,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Try to capture HTTP headers if available (depends on integration)
|
|
283
|
+
if SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS:
|
|
284
|
+
# For Django/Flask integrations, headers might be in context
|
|
285
|
+
if hasattr(self.execution_context, "context"):
|
|
286
|
+
ctx = self.execution_context.context
|
|
287
|
+
if hasattr(ctx, "request") and hasattr(
|
|
288
|
+
ctx.request, "headers"
|
|
289
|
+
):
|
|
290
|
+
self._request_data["headers"] = dict(
|
|
291
|
+
ctx.request.headers
|
|
292
|
+
)
|
|
293
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
294
|
+
print(
|
|
295
|
+
f"[[Strawberry]] Captured {len(self._request_data['headers'])} request headers",
|
|
296
|
+
log=False,
|
|
297
|
+
)
|
|
298
|
+
except Exception as e:
|
|
299
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
300
|
+
print(
|
|
301
|
+
f"[[Strawberry]] Failed to capture request data: {e}",
|
|
302
|
+
log=False,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# ---------------- wrappers ---------------- #
|
|
306
|
+
def resolve(self, _next, root, info, *args, **kwargs):
|
|
307
|
+
user_fn = _unwrap_user_func(_next)
|
|
308
|
+
tracer = self._make_tracer(info)
|
|
309
|
+
sys.setprofile(tracer)
|
|
310
|
+
try:
|
|
311
|
+
return _next(root, info, *args, **kwargs)
|
|
312
|
+
finally:
|
|
313
|
+
sys.setprofile(None) # safety-net
|
|
314
|
+
|
|
315
|
+
async def resolve_async(self, _next, root, info, *args, **kwargs):
|
|
316
|
+
user_fn = _unwrap_user_func(_next)
|
|
317
|
+
tracer = self._make_tracer(info)
|
|
318
|
+
sys.setprofile(tracer)
|
|
319
|
+
try:
|
|
320
|
+
return await _next(root, info, *args, **kwargs)
|
|
321
|
+
finally:
|
|
322
|
+
sys.setprofile(None)
|
|
323
|
+
|
|
324
|
+
# ---------------- OTEL-STYLE: Emit after request completes ---------------- #
|
|
325
|
+
def on_request_end(self):
|
|
326
|
+
"""Capture response data and emit network hops AFTER GraphQL response is built."""
|
|
327
|
+
# Capture response data first
|
|
328
|
+
if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY and self.execution_context.result:
|
|
329
|
+
try:
|
|
330
|
+
# GraphQL result includes data and errors
|
|
331
|
+
result_data = {
|
|
332
|
+
"data": (
|
|
333
|
+
self.execution_context.result.data
|
|
334
|
+
if self.execution_context.result.data
|
|
335
|
+
else None
|
|
336
|
+
),
|
|
337
|
+
"errors": (
|
|
338
|
+
[str(e) for e in self.execution_context.result.errors]
|
|
339
|
+
if self.execution_context.result.errors
|
|
340
|
+
else None
|
|
341
|
+
),
|
|
342
|
+
}
|
|
343
|
+
if HAS_ORJSON:
|
|
344
|
+
result_str = orjson.dumps(result_data, default=str)[
|
|
345
|
+
:_RESPONSE_LIMIT_BYTES
|
|
346
|
+
]
|
|
347
|
+
else:
|
|
348
|
+
result_str = json.dumps(result_data, default=str)[
|
|
349
|
+
:_RESPONSE_LIMIT_BYTES
|
|
350
|
+
]
|
|
351
|
+
self._response_data["body"] = result_str.encode("utf-8")
|
|
352
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
353
|
+
print(
|
|
354
|
+
f"[[Strawberry]] Captured GraphQL result: {len(result_str)} chars",
|
|
355
|
+
log=False,
|
|
356
|
+
)
|
|
357
|
+
except Exception as e:
|
|
358
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
359
|
+
print(
|
|
360
|
+
f"[[Strawberry]] Failed to capture response data: {e}",
|
|
361
|
+
log=False,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Get captured data
|
|
365
|
+
req_headers = self._request_data.get("headers")
|
|
366
|
+
req_body = self._request_data.get("body")
|
|
367
|
+
resp_headers = self._response_data.get(
|
|
368
|
+
"headers"
|
|
369
|
+
) # Not typically available in GraphQL
|
|
370
|
+
resp_body = self._response_data.get("body")
|
|
371
|
+
|
|
372
|
+
# Emit network hops for all captured resolvers
|
|
373
|
+
for endpoint_info in self._captured_endpoints:
|
|
374
|
+
endpoint_id = endpoint_info.get("endpoint_id")
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
_, session_id = get_or_set_sf_trace_id()
|
|
378
|
+
|
|
379
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
380
|
+
print(
|
|
381
|
+
f"[[Strawberry]] Emitting hop for {endpoint_info['name']}: "
|
|
382
|
+
f"req_headers={'present' if req_headers else 'None'}, "
|
|
383
|
+
f"req_body={len(req_body) if req_body else 0} bytes, "
|
|
384
|
+
f"resp_body={len(resp_body) if resp_body else 0} bytes",
|
|
385
|
+
log=False,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# Extract raw path and query string for C to parse (if available from context)
|
|
389
|
+
raw_path = None
|
|
390
|
+
raw_query = b""
|
|
391
|
+
try:
|
|
392
|
+
if hasattr(self.execution_context, "context"):
|
|
393
|
+
ctx = self.execution_context.context
|
|
394
|
+
if hasattr(ctx, "request"):
|
|
395
|
+
req = ctx.request
|
|
396
|
+
# Try to get path - different frameworks have different attributes
|
|
397
|
+
if hasattr(req, "path"):
|
|
398
|
+
raw_path = str(req.path)
|
|
399
|
+
elif hasattr(req, "url") and hasattr(req.url, "path"):
|
|
400
|
+
raw_path = str(req.url.path)
|
|
401
|
+
|
|
402
|
+
# Try to get query string
|
|
403
|
+
if hasattr(req, "query_string"):
|
|
404
|
+
raw_query = (
|
|
405
|
+
req.query_string
|
|
406
|
+
if isinstance(req.query_string, bytes)
|
|
407
|
+
else req.query_string.encode("utf-8")
|
|
408
|
+
)
|
|
409
|
+
elif (
|
|
410
|
+
hasattr(req, "META") and "QUERY_STRING" in req.META
|
|
411
|
+
):
|
|
412
|
+
raw_query = req.META["QUERY_STRING"].encode("utf-8")
|
|
413
|
+
except Exception as e:
|
|
414
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
415
|
+
print(
|
|
416
|
+
f"[[Strawberry]] Failed to extract path/query: {e}",
|
|
417
|
+
log=False,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
# Use fast path if C extension available
|
|
421
|
+
if endpoint_id is not None and endpoint_id >= 0:
|
|
422
|
+
fast_send_network_hop_fast(
|
|
423
|
+
session_id=session_id,
|
|
424
|
+
endpoint_id=endpoint_id,
|
|
425
|
+
raw_path=raw_path,
|
|
426
|
+
raw_query_string=raw_query,
|
|
427
|
+
request_headers=req_headers,
|
|
428
|
+
request_body=req_body,
|
|
429
|
+
response_headers=resp_headers,
|
|
430
|
+
response_body=resp_body,
|
|
431
|
+
)
|
|
432
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
433
|
+
print(
|
|
434
|
+
f"[[Strawberry]] Emitted network hop (fast path): {endpoint_info['name']} "
|
|
435
|
+
f"endpoint_id={endpoint_id} session={session_id}",
|
|
436
|
+
log=False,
|
|
437
|
+
)
|
|
438
|
+
else:
|
|
439
|
+
# Fallback to old Python API (doesn't support body/header capture)
|
|
440
|
+
fast_send_network_hop(
|
|
441
|
+
session_id=session_id,
|
|
442
|
+
line=str(endpoint_info["line"]),
|
|
443
|
+
column="0",
|
|
444
|
+
name=endpoint_info["name"],
|
|
445
|
+
entrypoint=endpoint_info["filename"],
|
|
446
|
+
)
|
|
447
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
448
|
+
print(
|
|
449
|
+
f"[[Strawberry]] Emitted network hop (fallback): {endpoint_info['name']} "
|
|
450
|
+
f"session={session_id}",
|
|
451
|
+
log=False,
|
|
452
|
+
)
|
|
453
|
+
except Exception as e: # noqa: BLE001 S110
|
|
454
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
455
|
+
print(
|
|
456
|
+
f"[[Strawberry]] Failed to emit network hop: {e}", log=False
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
return NetworkHopExtension
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def patch_strawberry_module(strawberry):
|
|
463
|
+
"""Patch Strawberry to ensure exceptions go through the custom excepthook."""
|
|
464
|
+
global _is_strawberry_patched
|
|
465
|
+
if _is_strawberry_patched:
|
|
466
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
467
|
+
print(
|
|
468
|
+
"[[DEBUG]] Strawberry has already been patched, skipping. [[/DEBUG]]",
|
|
469
|
+
log=False,
|
|
470
|
+
)
|
|
471
|
+
return
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
# Backup the original execute method from Strawberry
|
|
475
|
+
original_execute = strawberry.execution.execute.execute
|
|
476
|
+
|
|
477
|
+
async def custom_execute(*args, **kwargs):
|
|
478
|
+
try:
|
|
479
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
480
|
+
print(
|
|
481
|
+
"[[DEBUG]] Executing patched Strawberry execute function. [[/DEBUG]]",
|
|
482
|
+
log=False,
|
|
483
|
+
)
|
|
484
|
+
return await original_execute(*args, **kwargs)
|
|
485
|
+
except Exception as e:
|
|
486
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
487
|
+
print(
|
|
488
|
+
"[[DEBUG]] Intercepted exception in Strawberry execute. [[/DEBUG]]",
|
|
489
|
+
log=False,
|
|
490
|
+
)
|
|
491
|
+
# Invoke custom excepthook globally
|
|
492
|
+
sys.excepthook(type(e), e, e.__traceback__)
|
|
493
|
+
raise
|
|
494
|
+
|
|
495
|
+
# Replace Strawberry's execute function with the patched version
|
|
496
|
+
strawberry.execution.execute.execute = custom_execute
|
|
497
|
+
_is_strawberry_patched = True
|
|
498
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
499
|
+
print(
|
|
500
|
+
"[[DEBUG]] Successfully patched Strawberry execute function. [[/DEBUG]]",
|
|
501
|
+
log=False,
|
|
502
|
+
)
|
|
503
|
+
except Exception as error:
|
|
504
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
505
|
+
print(
|
|
506
|
+
f"[[DEBUG]] Failed to patch Strawberry: {error}. [[/DEBUG]]", log=False
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
class CustomImportHook:
|
|
511
|
+
"""Import hook to intercept the import of 'strawberry' modules."""
|
|
512
|
+
|
|
513
|
+
def find_spec(self, fullname, path, target=None):
|
|
514
|
+
global _is_strawberry_patched
|
|
515
|
+
if fullname == "strawberry" and not _is_strawberry_patched:
|
|
516
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
517
|
+
print(
|
|
518
|
+
f"[[DEBUG]] Intercepting import of {fullname}. [[/DEBUG]]",
|
|
519
|
+
log=False,
|
|
520
|
+
)
|
|
521
|
+
return find_spec(fullname)
|
|
522
|
+
if fullname.startswith("strawberry_django"):
|
|
523
|
+
return None # Let default import handle strawberry_django
|
|
524
|
+
|
|
525
|
+
def exec_module(self, module):
|
|
526
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
527
|
+
print(
|
|
528
|
+
f"[[DEBUG]] Executing module: {module.__name__}. [[/DEBUG]]", log=False
|
|
529
|
+
)
|
|
530
|
+
# Execute the module normally
|
|
531
|
+
module_spec = module.__spec__
|
|
532
|
+
if module_spec and module_spec.loader:
|
|
533
|
+
module_spec.loader.exec_module(module)
|
|
534
|
+
# Once strawberry is loaded, patch it
|
|
535
|
+
if module.__name__ == "strawberry" and not _is_strawberry_patched:
|
|
536
|
+
patch_strawberry_module(module)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def patch_schema():
|
|
540
|
+
"""Patch strawberry.Schema to include both Sailfish and NetworkHop extensions by default."""
|
|
541
|
+
try:
|
|
542
|
+
import strawberry
|
|
543
|
+
|
|
544
|
+
original_schema_init = strawberry.Schema.__init__
|
|
545
|
+
|
|
546
|
+
def patched_schema_init(self, *args, extensions=None, **kwargs):
|
|
547
|
+
if extensions is None:
|
|
548
|
+
extensions = []
|
|
549
|
+
|
|
550
|
+
# Add the custom error handling extension
|
|
551
|
+
sailfish_ext = get_extension()
|
|
552
|
+
if sailfish_ext not in extensions:
|
|
553
|
+
extensions.append(sailfish_ext)
|
|
554
|
+
|
|
555
|
+
# Add the network hop extension
|
|
556
|
+
hop_ext = get_network_hop_extension()
|
|
557
|
+
if hop_ext not in extensions:
|
|
558
|
+
extensions.append(hop_ext)
|
|
559
|
+
|
|
560
|
+
# Call the original constructor
|
|
561
|
+
original_schema_init(self, *args, extensions=extensions, **kwargs)
|
|
562
|
+
|
|
563
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
564
|
+
print(
|
|
565
|
+
"[[DEBUG]] Patched strawberry.Schema to include Sailfish & NetworkHop extensions. [[/DEBUG]]",
|
|
566
|
+
log=False,
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
# Apply the patch
|
|
570
|
+
strawberry.Schema.__init__ = patched_schema_init
|
|
571
|
+
|
|
572
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
573
|
+
print(
|
|
574
|
+
"[[DEBUG]] Successfully patched strawberry.Schema. [[/DEBUG]]",
|
|
575
|
+
log=False,
|
|
576
|
+
)
|
|
577
|
+
except ImportError:
|
|
578
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
579
|
+
print(
|
|
580
|
+
"[[DEBUG]] Strawberry is not installed. Skipping schema patching. [[/DEBUG]]",
|
|
581
|
+
log=False,
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def patch_views():
|
|
586
|
+
"""
|
|
587
|
+
Patch Strawberry view classes to capture and print request data on errors.
|
|
588
|
+
This helps debug malformed requests when STRAWBERRY_DEBUG is enabled.
|
|
589
|
+
Also transmits exceptions with full stack traces when CAPTURE_STRAWBERRY_ERRORS_WITH_DATA is enabled.
|
|
590
|
+
"""
|
|
591
|
+
if not STRAWBERRY_DEBUG and not CAPTURE_STRAWBERRY_ERRORS_WITH_DATA:
|
|
592
|
+
return # Skip patching if neither debug mode nor capture mode is enabled
|
|
593
|
+
|
|
594
|
+
try:
|
|
595
|
+
# Try to import Strawberry Django view
|
|
596
|
+
try:
|
|
597
|
+
from strawberry.django.views import GraphQLView as DjangoGraphQLView
|
|
598
|
+
|
|
599
|
+
_patch_view_class(DjangoGraphQLView, "Django")
|
|
600
|
+
except ImportError:
|
|
601
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
602
|
+
print(
|
|
603
|
+
"[[DEBUG]] Strawberry Django view not found. [[/DEBUG]]", log=False
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
# Try to import base async view (used by other integrations)
|
|
607
|
+
try:
|
|
608
|
+
from strawberry.http.async_base_view import AsyncBaseHTTPView
|
|
609
|
+
|
|
610
|
+
_patch_async_base_view(AsyncBaseHTTPView)
|
|
611
|
+
except ImportError:
|
|
612
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
613
|
+
print(
|
|
614
|
+
"[[DEBUG]] Strawberry AsyncBaseHTTPView not found. [[/DEBUG]]",
|
|
615
|
+
log=False,
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
except Exception as e:
|
|
619
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
620
|
+
print(
|
|
621
|
+
f"[[DEBUG]] Failed to patch Strawberry views: {e}. [[/DEBUG]]",
|
|
622
|
+
log=False,
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def _patch_view_class(view_class, integration_name):
|
|
627
|
+
"""Patch a Strawberry view class to capture request data on errors."""
|
|
628
|
+
if hasattr(view_class, "_sf_patched"):
|
|
629
|
+
return # Already patched
|
|
630
|
+
|
|
631
|
+
original_dispatch = view_class.dispatch
|
|
632
|
+
|
|
633
|
+
async def patched_dispatch(self, request, *args, **kwargs):
|
|
634
|
+
# Capture raw request body before processing
|
|
635
|
+
raw_body = None
|
|
636
|
+
if STRAWBERRY_DEBUG or CAPTURE_STRAWBERRY_ERRORS_WITH_DATA:
|
|
637
|
+
try:
|
|
638
|
+
raw_body = request.body if hasattr(request, "body") else None
|
|
639
|
+
except Exception:
|
|
640
|
+
pass
|
|
641
|
+
|
|
642
|
+
try:
|
|
643
|
+
return await original_dispatch(self, request, *args, **kwargs)
|
|
644
|
+
except Exception as e:
|
|
645
|
+
if (
|
|
646
|
+
STRAWBERRY_DEBUG or CAPTURE_STRAWBERRY_ERRORS_WITH_DATA
|
|
647
|
+
) and raw_body is not None:
|
|
648
|
+
_print_request_debug_info(raw_body, e, integration_name)
|
|
649
|
+
raise
|
|
650
|
+
|
|
651
|
+
view_class.dispatch = patched_dispatch
|
|
652
|
+
view_class._sf_patched = True
|
|
653
|
+
|
|
654
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
655
|
+
print(
|
|
656
|
+
f"[[DEBUG]] Patched Strawberry {integration_name} view for error debugging. [[/DEBUG]]",
|
|
657
|
+
log=False,
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def _patch_async_base_view(view_class):
|
|
662
|
+
"""Patch AsyncBaseHTTPView to capture request data on parse errors."""
|
|
663
|
+
if hasattr(view_class, "_sf_parse_patched"):
|
|
664
|
+
return # Already patched
|
|
665
|
+
|
|
666
|
+
original_parse = view_class.parse_http_body
|
|
667
|
+
|
|
668
|
+
async def patched_parse_http_body(self, request_adapter):
|
|
669
|
+
# Capture raw body before parsing (but avoid consuming the stream twice)
|
|
670
|
+
raw_body = None
|
|
671
|
+
if STRAWBERRY_DEBUG or CAPTURE_STRAWBERRY_ERRORS_WITH_DATA:
|
|
672
|
+
try:
|
|
673
|
+
# Read the body once
|
|
674
|
+
raw_body = await request_adapter.get_body()
|
|
675
|
+
|
|
676
|
+
# Patch request_adapter.get_body to return cached body
|
|
677
|
+
# (body streams can only be read once)
|
|
678
|
+
async def cached_get_body():
|
|
679
|
+
return raw_body
|
|
680
|
+
|
|
681
|
+
request_adapter.get_body = cached_get_body
|
|
682
|
+
except Exception:
|
|
683
|
+
pass
|
|
684
|
+
|
|
685
|
+
try:
|
|
686
|
+
return await original_parse(self, request_adapter)
|
|
687
|
+
except Exception as e:
|
|
688
|
+
logger.info("=" * 20 + " <STRAWBERRY> " + "=" * 20)
|
|
689
|
+
logger.error(e)
|
|
690
|
+
logger.info("=" * 20 + " </STRAWBERRY> " + "=" * 20)
|
|
691
|
+
if (
|
|
692
|
+
STRAWBERRY_DEBUG or CAPTURE_STRAWBERRY_ERRORS_WITH_DATA
|
|
693
|
+
) and raw_body is not None:
|
|
694
|
+
_print_request_debug_info(raw_body, e, "AsyncBaseHTTPView")
|
|
695
|
+
raise
|
|
696
|
+
|
|
697
|
+
view_class.parse_http_body = patched_parse_http_body
|
|
698
|
+
view_class._sf_parse_patched = True
|
|
699
|
+
|
|
700
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
701
|
+
print(
|
|
702
|
+
"[[DEBUG]] Patched Strawberry AsyncBaseHTTPView.parse_http_body for error debugging. [[/DEBUG]]",
|
|
703
|
+
log=False,
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def _count_traceback_frames(tb):
|
|
708
|
+
"""Count the number of frames in a traceback."""
|
|
709
|
+
count = 0
|
|
710
|
+
while tb is not None:
|
|
711
|
+
count += 1
|
|
712
|
+
tb = tb.tb_next
|
|
713
|
+
return count
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def _print_request_debug_info(raw_body, exception, source):
|
|
717
|
+
"""Print debug information about the request that caused an error."""
|
|
718
|
+
|
|
719
|
+
# Transmit exception to Sailfish with full stack trace if enabled
|
|
720
|
+
if CAPTURE_STRAWBERRY_ERRORS_WITH_DATA:
|
|
721
|
+
try:
|
|
722
|
+
# Verify that the exception has a traceback attached
|
|
723
|
+
if (
|
|
724
|
+
not hasattr(exception, "__traceback__")
|
|
725
|
+
or exception.__traceback__ is None
|
|
726
|
+
):
|
|
727
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
728
|
+
print(
|
|
729
|
+
f"[[STRAWBERRY_DEBUG]] WARNING: Exception {type(exception).__name__} has no __traceback__ attribute!",
|
|
730
|
+
log=False,
|
|
731
|
+
)
|
|
732
|
+
else:
|
|
733
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
734
|
+
print(
|
|
735
|
+
f"[[STRAWBERRY_DEBUG]] Exception has traceback with {_count_traceback_frames(exception.__traceback__)} frames",
|
|
736
|
+
log=False,
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
transmit_exception_to_sailfish(exception, force_transmit=False)
|
|
740
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
741
|
+
print(
|
|
742
|
+
f"[[STRAWBERRY_DEBUG]] Transmitted exception to Sailfish: {type(exception).__name__}",
|
|
743
|
+
log=False,
|
|
744
|
+
)
|
|
745
|
+
except Exception as transmit_err:
|
|
746
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
747
|
+
print(
|
|
748
|
+
f"[[STRAWBERRY_DEBUG]] Failed to transmit exception: {transmit_err}",
|
|
749
|
+
log=False,
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
print(
|
|
753
|
+
f"[[STRAWBERRY_DEBUG]] Transmission error traceback:\n{traceback.format_exc()}",
|
|
754
|
+
log=False,
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
# Print debug info if STRAWBERRY_DEBUG is enabled
|
|
758
|
+
if not STRAWBERRY_DEBUG:
|
|
759
|
+
return # Skip printing if debug mode is disabled
|
|
760
|
+
|
|
761
|
+
print("\n" + "=" * 80, log=False)
|
|
762
|
+
print(f"[[STRAWBERRY_DEBUG]] Error in {source}", log=False)
|
|
763
|
+
print("=" * 80, log=False)
|
|
764
|
+
|
|
765
|
+
# Print the exception
|
|
766
|
+
print(f"\nException: {type(exception).__name__}: {exception}", log=False)
|
|
767
|
+
print("\nTraceback:", log=False)
|
|
768
|
+
print(traceback.format_exc(), log=False)
|
|
769
|
+
|
|
770
|
+
# Print raw body
|
|
771
|
+
print("\n" + "-" * 80, log=False)
|
|
772
|
+
print("Raw HTTP Body (bytes):", log=False)
|
|
773
|
+
print("-" * 80, log=False)
|
|
774
|
+
if isinstance(raw_body, bytes):
|
|
775
|
+
print(f"Length: {len(raw_body)} bytes", log=False)
|
|
776
|
+
print(f"Raw: {raw_body!r}", log=False)
|
|
777
|
+
|
|
778
|
+
# Try to decode and pretty-print as JSON
|
|
779
|
+
try:
|
|
780
|
+
decoded = raw_body.decode("utf-8")
|
|
781
|
+
print(f"\nDecoded (UTF-8): {decoded}", log=False)
|
|
782
|
+
|
|
783
|
+
# Try to parse as JSON
|
|
784
|
+
try:
|
|
785
|
+
if HAS_ORJSON:
|
|
786
|
+
parsed = orjson.loads(decoded)
|
|
787
|
+
else:
|
|
788
|
+
parsed = json.loads(decoded)
|
|
789
|
+
print(f"\nParsed JSON (type: {type(parsed).__name__}):", log=False)
|
|
790
|
+
if HAS_ORJSON:
|
|
791
|
+
parsed = print(orjson.dumps(parsed, indent=2), log=False)
|
|
792
|
+
else:
|
|
793
|
+
parsed = print(json.dumps(parsed, indent=2), log=False)
|
|
794
|
+
except json.JSONDecodeError as json_err:
|
|
795
|
+
print(f"\nFailed to parse as JSON: {json_err}", log=False)
|
|
796
|
+
except UnicodeDecodeError as decode_err:
|
|
797
|
+
print(f"\nFailed to decode as UTF-8: {decode_err}", log=False)
|
|
798
|
+
else:
|
|
799
|
+
print(f"Body type: {type(raw_body).__name__}", log=False)
|
|
800
|
+
print(f"Body: {raw_body!r}", log=False)
|
|
801
|
+
|
|
802
|
+
print("\n" + "=" * 80, log=False)
|
|
803
|
+
print("[[/STRAWBERRY_DEBUG]]", log=False)
|
|
804
|
+
print("=" * 80 + "\n", log=False)
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def patch_strawberry():
|
|
808
|
+
"""
|
|
809
|
+
Main entry point for patching Strawberry GraphQL.
|
|
810
|
+
Applies both schema extensions and error debugging patches.
|
|
811
|
+
"""
|
|
812
|
+
patch_schema()
|
|
813
|
+
patch_views()
|