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,1446 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import json
|
|
3
|
+
import inspect
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import random
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
9
|
+
import traceback
|
|
10
|
+
from importlib.util import find_spec
|
|
11
|
+
from typing import Any, Callable, Set, Tuple
|
|
12
|
+
|
|
13
|
+
from ... import _sffuncspan
|
|
14
|
+
from ... import app_config
|
|
15
|
+
from ...custom_excepthook import custom_excepthook
|
|
16
|
+
from ...env_vars import (
|
|
17
|
+
CAPTURE_STRAWBERRY_ERRORS_WITH_DATA,
|
|
18
|
+
PRINT_CONFIGURATION_STATUSES,
|
|
19
|
+
SF_DEBUG,
|
|
20
|
+
SF_FUNCSPAN_CAPTURE_INSTALLED_PACKAGES,
|
|
21
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_BODY,
|
|
22
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS,
|
|
23
|
+
SF_NETWORKHOP_CAPTURE_RESPONSE_BODY,
|
|
24
|
+
SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS,
|
|
25
|
+
SF_NETWORKHOP_REQUEST_LIMIT_MB,
|
|
26
|
+
SF_NETWORKHOP_RESPONSE_LIMIT_MB,
|
|
27
|
+
STRAWBERRY_DEBUG,
|
|
28
|
+
)
|
|
29
|
+
from ...fast_network_hop import (
|
|
30
|
+
fast_send_network_hop,
|
|
31
|
+
fast_send_network_hop_fast,
|
|
32
|
+
register_endpoint,
|
|
33
|
+
)
|
|
34
|
+
from ...function_span_profiler import _HAS_NATIVE
|
|
35
|
+
from ...thread_local import get_or_set_sf_trace_id, get_current_function_span_id
|
|
36
|
+
from ...transmit_exception_to_sailfish import transmit_exception_to_sailfish
|
|
37
|
+
from .utils import _is_user_code, _unwrap_user_func
|
|
38
|
+
|
|
39
|
+
# JSON serialization - try fast orjson first, fallback to stdlib json
|
|
40
|
+
try:
|
|
41
|
+
import orjson
|
|
42
|
+
|
|
43
|
+
HAS_ORJSON = True
|
|
44
|
+
except ImportError:
|
|
45
|
+
import json
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
logger = logging.getLogger(__name__)
|
|
49
|
+
|
|
50
|
+
# Size limits in bytes
|
|
51
|
+
_REQUEST_LIMIT_BYTES = SF_NETWORKHOP_REQUEST_LIMIT_MB * 1024 * 1024
|
|
52
|
+
_RESPONSE_LIMIT_BYTES = SF_NETWORKHOP_RESPONSE_LIMIT_MB * 1024 * 1024
|
|
53
|
+
|
|
54
|
+
# Pre-registered endpoint IDs
|
|
55
|
+
_ENDPOINT_REGISTRY: dict[tuple, int] = {}
|
|
56
|
+
|
|
57
|
+
# Track if Strawberry has already been patched to prevent multiple patches
|
|
58
|
+
_is_strawberry_patched = False
|
|
59
|
+
|
|
60
|
+
# Thread-local tracker for preventing double invocation of resolve methods
|
|
61
|
+
_sf_processing_tracker = threading.local()
|
|
62
|
+
|
|
63
|
+
# Cache for function definition line numbers (keyed by code object id)
|
|
64
|
+
_FUNCTION_DEF_LINE_CACHE: dict[int, int] = {}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _get_function_def_line(frame):
|
|
68
|
+
"""
|
|
69
|
+
Get the line number of the 'def' statement, skipping decorators.
|
|
70
|
+
|
|
71
|
+
Python's co_firstlineno includes decorators, so we need to scan the source
|
|
72
|
+
to find the actual function definition line.
|
|
73
|
+
|
|
74
|
+
PERFORMANCE: Results are cached by code object ID, so the file I/O only
|
|
75
|
+
happens once per function (first request). Subsequent requests are instant.
|
|
76
|
+
"""
|
|
77
|
+
code_id = id(frame.f_code)
|
|
78
|
+
|
|
79
|
+
# Check cache first - this is the fast path for all requests after the first
|
|
80
|
+
if code_id in _FUNCTION_DEF_LINE_CACHE:
|
|
81
|
+
return _FUNCTION_DEF_LINE_CACHE[code_id]
|
|
82
|
+
|
|
83
|
+
# Cache miss - do the expensive source file lookup
|
|
84
|
+
try:
|
|
85
|
+
# Get source lines for this code object (SLOW: reads file from disk)
|
|
86
|
+
source_lines, start_line = inspect.getsourcelines(frame.f_code)
|
|
87
|
+
|
|
88
|
+
# Find the first line that starts with 'def' or 'async def'
|
|
89
|
+
for i, line in enumerate(source_lines):
|
|
90
|
+
stripped = line.strip()
|
|
91
|
+
if stripped.startswith("def ") or stripped.startswith("async def "):
|
|
92
|
+
def_line = start_line + i
|
|
93
|
+
# Cache the result for next time
|
|
94
|
+
_FUNCTION_DEF_LINE_CACHE[code_id] = def_line
|
|
95
|
+
return def_line
|
|
96
|
+
|
|
97
|
+
# Fallback: return co_firstlineno if we can't find def
|
|
98
|
+
result = frame.f_code.co_firstlineno
|
|
99
|
+
_FUNCTION_DEF_LINE_CACHE[code_id] = result
|
|
100
|
+
return result
|
|
101
|
+
except Exception:
|
|
102
|
+
# If anything fails, fallback to co_firstlineno
|
|
103
|
+
result = frame.f_code.co_firstlineno
|
|
104
|
+
_FUNCTION_DEF_LINE_CACHE[code_id] = result
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_extension():
|
|
109
|
+
from strawberry.extensions import SchemaExtension
|
|
110
|
+
|
|
111
|
+
class CustomErrorHandlingExtension(SchemaExtension):
|
|
112
|
+
def __init__(self, *, execution_context):
|
|
113
|
+
self.execution_context = execution_context
|
|
114
|
+
|
|
115
|
+
def on_request_start(self):
|
|
116
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
117
|
+
print("Starting GraphQL request", log=False)
|
|
118
|
+
|
|
119
|
+
def on_request_end(self):
|
|
120
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
121
|
+
print("Ending GraphQL request", log=False)
|
|
122
|
+
if not self.execution_context.errors:
|
|
123
|
+
return
|
|
124
|
+
for error in self.execution_context.errors:
|
|
125
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
126
|
+
print(f"Handling GraphQL error: {error}", log=False)
|
|
127
|
+
custom_excepthook(type(error), error, error.__traceback__)
|
|
128
|
+
|
|
129
|
+
def on_validation_start(self):
|
|
130
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
131
|
+
print("Starting validation of GraphQL request", log=False)
|
|
132
|
+
|
|
133
|
+
def on_validation_end(self):
|
|
134
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
135
|
+
print("Ending validation of GraphQL request", log=False)
|
|
136
|
+
|
|
137
|
+
def on_execution_start(self):
|
|
138
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
139
|
+
print("Starting execution of GraphQL request", log=False)
|
|
140
|
+
|
|
141
|
+
def on_resolver_start(self, resolver, obj, info, **kwargs):
|
|
142
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
143
|
+
print(f"Starting resolver {resolver.__name__}", log=False)
|
|
144
|
+
|
|
145
|
+
def on_resolver_end(self, resolver, obj, info, **kwargs):
|
|
146
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
147
|
+
print(f"Ending resolver {resolver.__name__}", log=False)
|
|
148
|
+
|
|
149
|
+
def on_error(self, error: Exception):
|
|
150
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
151
|
+
print(f"Handling error in resolver: {error}", log=False)
|
|
152
|
+
custom_excepthook(type(error), error, error.__traceback__)
|
|
153
|
+
|
|
154
|
+
return CustomErrorHandlingExtension
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def get_network_hop_extension() -> "type[SchemaExtension]":
|
|
158
|
+
"""
|
|
159
|
+
Strawberry SchemaExtension that emits a collectNetworkHops mutation for the
|
|
160
|
+
*first* user-land frame executed inside every resolver (sync or async).
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
from strawberry.extensions import SchemaExtension
|
|
164
|
+
|
|
165
|
+
# --------------------------------------------------------------------- #
|
|
166
|
+
# Helper predicates
|
|
167
|
+
# --------------------------------------------------------------------- #
|
|
168
|
+
# Extended dig: __wrapped__, closure cells *and* common attribute names
|
|
169
|
+
# --------------------------------------------------------------------- #
|
|
170
|
+
# Extension class
|
|
171
|
+
# --------------------------------------------------------------------- #
|
|
172
|
+
class NetworkHopExtension(SchemaExtension):
|
|
173
|
+
supports_sync = supports_async = True
|
|
174
|
+
_sent: Set[Tuple[str, int]] = set() # class-level: de-dupe per request
|
|
175
|
+
|
|
176
|
+
def __init__(self, *, execution_context):
|
|
177
|
+
super().__init__(execution_context=execution_context)
|
|
178
|
+
self._captured_endpoints = (
|
|
179
|
+
[]
|
|
180
|
+
) # Store endpoint info for post-response emission
|
|
181
|
+
self._request_data = {} # Store request headers/body
|
|
182
|
+
self._response_data = {} # Store response headers/body
|
|
183
|
+
|
|
184
|
+
# ---------------- internal capture helper ---------------- #
|
|
185
|
+
def _capture(self, frame, info):
|
|
186
|
+
"""OTEL-STYLE: Capture endpoint metadata and pre-register."""
|
|
187
|
+
filename = frame.f_code.co_filename
|
|
188
|
+
func_name = frame.f_code.co_name
|
|
189
|
+
|
|
190
|
+
# Get the actual function definition line (skipping decorators)
|
|
191
|
+
line_no = _get_function_def_line(frame)
|
|
192
|
+
|
|
193
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
194
|
+
print(
|
|
195
|
+
f"[[Strawberry]] _capture: {func_name} @ {filename} "
|
|
196
|
+
f"co_firstlineno={frame.f_code.co_firstlineno} -> def_line={line_no}",
|
|
197
|
+
log=False,
|
|
198
|
+
)
|
|
199
|
+
if (filename, line_no) in NetworkHopExtension._sent:
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
hop_key = (filename, line_no)
|
|
203
|
+
|
|
204
|
+
# Pre-register endpoint if not already registered
|
|
205
|
+
endpoint_id = _ENDPOINT_REGISTRY.get(hop_key)
|
|
206
|
+
if endpoint_id is None:
|
|
207
|
+
endpoint_id = register_endpoint(
|
|
208
|
+
line=str(line_no),
|
|
209
|
+
column="0",
|
|
210
|
+
name=func_name,
|
|
211
|
+
entrypoint=filename,
|
|
212
|
+
route=None,
|
|
213
|
+
)
|
|
214
|
+
if endpoint_id >= 0:
|
|
215
|
+
_ENDPOINT_REGISTRY[hop_key] = endpoint_id
|
|
216
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
217
|
+
print(
|
|
218
|
+
f"[[Strawberry]] Registered resolver: {func_name} @ "
|
|
219
|
+
f"{filename}:{line_no} (id={endpoint_id})",
|
|
220
|
+
log=False,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Store for on_request_end to emit
|
|
224
|
+
self._captured_endpoints.append(
|
|
225
|
+
{
|
|
226
|
+
"filename": filename,
|
|
227
|
+
"line": line_no,
|
|
228
|
+
"name": func_name,
|
|
229
|
+
"endpoint_id": endpoint_id,
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
NetworkHopExtension._sent.add((filename, line_no))
|
|
233
|
+
|
|
234
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
235
|
+
print(
|
|
236
|
+
f"[[Strawberry]] Captured resolver: {func_name} "
|
|
237
|
+
f"({filename}:{line_no}) endpoint_id={endpoint_id}",
|
|
238
|
+
log=False,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# ---------------- tracer factory ---------------- #
|
|
242
|
+
def _make_tracer(self, info):
|
|
243
|
+
def tracer(frame, event, arg):
|
|
244
|
+
if event.startswith("c_"):
|
|
245
|
+
return
|
|
246
|
+
if event == "call":
|
|
247
|
+
if _is_user_code(frame.f_code.co_filename):
|
|
248
|
+
self._capture(frame, info)
|
|
249
|
+
sys.setprofile(None)
|
|
250
|
+
return
|
|
251
|
+
return tracer # keep tracing until we hit user code
|
|
252
|
+
|
|
253
|
+
return tracer
|
|
254
|
+
|
|
255
|
+
# ---------------- request/response capture ---------------- #
|
|
256
|
+
def on_request_start(self):
|
|
257
|
+
"""Capture GraphQL request data when request starts."""
|
|
258
|
+
# IMPORTANT: Clear captured endpoints from previous requests
|
|
259
|
+
# SchemaExtension instances may be reused across requests
|
|
260
|
+
self._captured_endpoints = []
|
|
261
|
+
self._request_data = {}
|
|
262
|
+
self._response_data = {}
|
|
263
|
+
|
|
264
|
+
if (
|
|
265
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS
|
|
266
|
+
or SF_NETWORKHOP_CAPTURE_REQUEST_BODY
|
|
267
|
+
):
|
|
268
|
+
try:
|
|
269
|
+
# Access the GraphQL query from execution context
|
|
270
|
+
if (
|
|
271
|
+
SF_NETWORKHOP_CAPTURE_REQUEST_BODY
|
|
272
|
+
and self.execution_context.query
|
|
273
|
+
):
|
|
274
|
+
|
|
275
|
+
query_data = {
|
|
276
|
+
"query": self.execution_context.query,
|
|
277
|
+
"variables": self.execution_context.variables or {},
|
|
278
|
+
"operation_name": self.execution_context.operation_name,
|
|
279
|
+
}
|
|
280
|
+
# Convert to JSON string and limit size
|
|
281
|
+
if HAS_ORJSON:
|
|
282
|
+
query_str = orjson.dumps(query_data)[:_REQUEST_LIMIT_BYTES]
|
|
283
|
+
else:
|
|
284
|
+
query_str = json.dumps(query_data)[:_REQUEST_LIMIT_BYTES]
|
|
285
|
+
self._request_data["body"] = query_str.encode("utf-8")
|
|
286
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
287
|
+
print(
|
|
288
|
+
f"[[Strawberry]] Captured GraphQL query: {len(query_str)} chars",
|
|
289
|
+
log=False,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Try to capture HTTP headers if available (depends on integration)
|
|
293
|
+
if SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS:
|
|
294
|
+
# For Django/Flask integrations, headers might be in context
|
|
295
|
+
if hasattr(self.execution_context, "context"):
|
|
296
|
+
ctx = self.execution_context.context
|
|
297
|
+
if hasattr(ctx, "request") and hasattr(
|
|
298
|
+
ctx.request, "headers"
|
|
299
|
+
):
|
|
300
|
+
self._request_data["headers"] = dict(
|
|
301
|
+
ctx.request.headers
|
|
302
|
+
)
|
|
303
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
304
|
+
print(
|
|
305
|
+
f"[[Strawberry]] Captured {len(self._request_data['headers'])} request headers",
|
|
306
|
+
log=False,
|
|
307
|
+
)
|
|
308
|
+
except Exception as e:
|
|
309
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
310
|
+
print(
|
|
311
|
+
f"[[Strawberry]] Failed to capture request data: {e}",
|
|
312
|
+
log=False,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# ---------------- wrappers ---------------- #
|
|
316
|
+
def resolve(self, _next, root, info, *args, **kwargs):
|
|
317
|
+
# CRITICAL: Prevent double-invocation
|
|
318
|
+
# Strawberry calls each extension's resolve method multiple times in the chain
|
|
319
|
+
# We only want to process once per actual GraphQL field resolution
|
|
320
|
+
# Use a thread-local tracking dict since GraphQLResolveInfo might not allow attribute assignment
|
|
321
|
+
if not hasattr(_sf_processing_tracker, 'processing'):
|
|
322
|
+
_sf_processing_tracker.processing = {}
|
|
323
|
+
|
|
324
|
+
field_key = (id(info), info.field_name if hasattr(info, 'field_name') else None)
|
|
325
|
+
|
|
326
|
+
# Check if we're already processing this field
|
|
327
|
+
if field_key in _sf_processing_tracker.processing:
|
|
328
|
+
# Already processing this field, just pass through to next extension
|
|
329
|
+
return _next(root, info, *args, **kwargs)
|
|
330
|
+
|
|
331
|
+
# Mark this field as being processed
|
|
332
|
+
_sf_processing_tracker.processing[field_key] = True
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
# Get the actual resolver from info.field_name
|
|
336
|
+
# _next is another extension in the chain, not the actual user resolver!
|
|
337
|
+
resolver_func = None
|
|
338
|
+
func_name = "<unknown>"
|
|
339
|
+
filename = "<unknown>"
|
|
340
|
+
line_no = 0
|
|
341
|
+
|
|
342
|
+
# Try multiple approaches to get the actual resolver function
|
|
343
|
+
# Strawberry stores resolvers in different places depending on the field type
|
|
344
|
+
|
|
345
|
+
# Approach 1: Check if info has python_name (Strawberry field attribute)
|
|
346
|
+
if hasattr(info, 'python_name') and hasattr(root, info.python_name):
|
|
347
|
+
attr = getattr(root, info.python_name, None)
|
|
348
|
+
if callable(attr):
|
|
349
|
+
resolver_func = attr
|
|
350
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
351
|
+
print(f"[[Strawberry]] Got resolver from root.{info.python_name}: {resolver_func}", log=False)
|
|
352
|
+
|
|
353
|
+
# Approach 2: Try _field.base_resolver (works for root queries where root=None)
|
|
354
|
+
if not resolver_func and hasattr(info, '_field'):
|
|
355
|
+
# For root queries, info._field should have the resolver
|
|
356
|
+
field_obj = info._field
|
|
357
|
+
# Try base_resolver first
|
|
358
|
+
if hasattr(field_obj, 'base_resolver') and field_obj.base_resolver:
|
|
359
|
+
resolver_func = field_obj.base_resolver
|
|
360
|
+
|
|
361
|
+
# If base_resolver is None, try origin (Strawberry stores the original function here)
|
|
362
|
+
elif hasattr(field_obj, 'origin') and callable(field_obj.origin):
|
|
363
|
+
resolver_func = field_obj.origin
|
|
364
|
+
|
|
365
|
+
# Approach 3: Try parent_type and field_name to look up the resolver
|
|
366
|
+
if not resolver_func and hasattr(info, 'parent_type') and hasattr(info, 'field_name'):
|
|
367
|
+
# Get the Strawberry type definition from the parent
|
|
368
|
+
parent_type_name = info.parent_type.name if hasattr(info.parent_type, 'name') else None
|
|
369
|
+
if parent_type_name and hasattr(root, '__class__'):
|
|
370
|
+
# Try to get the method from the root object's class
|
|
371
|
+
method = getattr(root.__class__, info.field_name, None)
|
|
372
|
+
if method and callable(method):
|
|
373
|
+
resolver_func = method
|
|
374
|
+
|
|
375
|
+
# Approach 4: Try to get resolver directly from the field definition
|
|
376
|
+
if not resolver_func and hasattr(info, '_field'):
|
|
377
|
+
field = info._field
|
|
378
|
+
# Try multiple attributes where Strawberry might store the resolver
|
|
379
|
+
for attr_name in ['base_resolver', 'resolver', 'wrapped_func', 'python_name']:
|
|
380
|
+
if hasattr(field, attr_name):
|
|
381
|
+
candidate = getattr(field, attr_name)
|
|
382
|
+
if callable(candidate):
|
|
383
|
+
resolver_func = candidate
|
|
384
|
+
break
|
|
385
|
+
|
|
386
|
+
# Approach 5: Get resolver from GraphQL field definition (when _field is not available)
|
|
387
|
+
if not resolver_func and hasattr(info, 'parent_type') and hasattr(info, 'field_name'):
|
|
388
|
+
# Access the GraphQL field from the parent type
|
|
389
|
+
try:
|
|
390
|
+
graphql_field = info.parent_type.fields.get(info.field_name)
|
|
391
|
+
if graphql_field and hasattr(graphql_field, 'resolve'):
|
|
392
|
+
resolver_func = graphql_field.resolve
|
|
393
|
+
except Exception as e:
|
|
394
|
+
pass
|
|
395
|
+
|
|
396
|
+
# Fallback: just use field_name
|
|
397
|
+
if not resolver_func and hasattr(info, 'field_name'):
|
|
398
|
+
func_name = info.field_name
|
|
399
|
+
|
|
400
|
+
# If we found a resolver function, extract its details
|
|
401
|
+
if resolver_func:
|
|
402
|
+
# Unwrap decorators and get the actual function
|
|
403
|
+
actual_func = resolver_func
|
|
404
|
+
# Handle bound methods
|
|
405
|
+
if hasattr(actual_func, '__func__'):
|
|
406
|
+
actual_func = actual_func.__func__
|
|
407
|
+
# Handle wrapped functions
|
|
408
|
+
while hasattr(actual_func, '__wrapped__'):
|
|
409
|
+
actual_func = actual_func.__wrapped__
|
|
410
|
+
|
|
411
|
+
# CRITICAL: If we got Strawberry's _async_resolver wrapper, try to extract the real user function
|
|
412
|
+
# Strawberry wraps user resolvers in multiple layers:
|
|
413
|
+
# _async_resolver -> extension_resolver -> actual user function
|
|
414
|
+
# We need to recursively unwrap to find the actual user function
|
|
415
|
+
def unwrap_strawberry_resolver(func, depth=0, max_depth=5):
|
|
416
|
+
"""Recursively unwrap Strawberry resolver wrappers to find the user function."""
|
|
417
|
+
if depth >= max_depth:
|
|
418
|
+
return None
|
|
419
|
+
|
|
420
|
+
if not (hasattr(func, '__code__') and hasattr(func, '__closure__')):
|
|
421
|
+
return None
|
|
422
|
+
|
|
423
|
+
# Check if this is a Strawberry wrapper function
|
|
424
|
+
is_strawberry = 'site-packages/strawberry' in func.__code__.co_filename
|
|
425
|
+
if not is_strawberry:
|
|
426
|
+
# Found a non-Strawberry function!
|
|
427
|
+
return func
|
|
428
|
+
|
|
429
|
+
# Look through closure for wrapped function
|
|
430
|
+
if func.__closure__:
|
|
431
|
+
for idx, cell in enumerate(func.__closure__):
|
|
432
|
+
try:
|
|
433
|
+
cell_content = cell.cell_contents
|
|
434
|
+
|
|
435
|
+
if callable(cell_content) and hasattr(cell_content, '__code__'):
|
|
436
|
+
cell_filename = cell_content.__code__.co_filename
|
|
437
|
+
cell_funcname = cell_content.__name__
|
|
438
|
+
|
|
439
|
+
# Check if this is user code
|
|
440
|
+
is_user_code = 'site-packages' not in cell_filename and 'dist-packages' not in cell_filename
|
|
441
|
+
if is_user_code:
|
|
442
|
+
# Found user function!
|
|
443
|
+
return cell_content
|
|
444
|
+
|
|
445
|
+
# Recursively check this function's closure
|
|
446
|
+
result = unwrap_strawberry_resolver(cell_content, depth + 1, max_depth)
|
|
447
|
+
if result:
|
|
448
|
+
return result
|
|
449
|
+
|
|
450
|
+
# Also check if the cell content has a 'base_resolver' attribute (StrawberryField)
|
|
451
|
+
# and try to extract the actual resolver function from it
|
|
452
|
+
if hasattr(cell_content, 'base_resolver'):
|
|
453
|
+
base_res = cell_content.base_resolver
|
|
454
|
+
if base_res and callable(base_res):
|
|
455
|
+
# If base_resolver is user code, return it directly
|
|
456
|
+
if hasattr(base_res, '__code__'):
|
|
457
|
+
resolver_file = base_res.__code__.co_filename
|
|
458
|
+
resolver_name = base_res.__name__
|
|
459
|
+
if 'site-packages' not in resolver_file and 'dist-packages' not in resolver_file:
|
|
460
|
+
return base_res
|
|
461
|
+
# StrawberryResolver object - extract the wrapped function
|
|
462
|
+
# Try common attributes where Strawberry stores the actual function
|
|
463
|
+
for attr in ['wrapped_func', '_wrapped_func', 'func', '_func', '__wrapped__']:
|
|
464
|
+
if hasattr(base_res, attr):
|
|
465
|
+
wrapped = getattr(base_res, attr)
|
|
466
|
+
if wrapped and callable(wrapped) and hasattr(wrapped, '__code__'):
|
|
467
|
+
resolver_file = wrapped.__code__.co_filename
|
|
468
|
+
resolver_name = wrapped.__name__
|
|
469
|
+
if 'site-packages' not in resolver_file and 'dist-packages' not in resolver_file:
|
|
470
|
+
return wrapped
|
|
471
|
+
# Otherwise recursively unwrap it
|
|
472
|
+
result = unwrap_strawberry_resolver(base_res, depth + 1, max_depth)
|
|
473
|
+
if result:
|
|
474
|
+
return result
|
|
475
|
+
|
|
476
|
+
except (AttributeError, ValueError):
|
|
477
|
+
pass
|
|
478
|
+
|
|
479
|
+
return None
|
|
480
|
+
|
|
481
|
+
if hasattr(actual_func, '__code__') and actual_func.__name__ == '_async_resolver':
|
|
482
|
+
if 'site-packages/strawberry' in actual_func.__code__.co_filename:
|
|
483
|
+
unwrapped = unwrap_strawberry_resolver(actual_func)
|
|
484
|
+
if unwrapped:
|
|
485
|
+
actual_func = unwrapped
|
|
486
|
+
|
|
487
|
+
if hasattr(actual_func, '__code__'):
|
|
488
|
+
filename = actual_func.__code__.co_filename
|
|
489
|
+
func_name = actual_func.__name__
|
|
490
|
+
line_no = actual_func.__code__.co_firstlineno
|
|
491
|
+
|
|
492
|
+
# CRITICAL: Skip profiling telemetry collection resolvers to prevent infinite loops
|
|
493
|
+
# When function spans are sent to Django's GraphQL endpoint, they trigger these resolvers
|
|
494
|
+
# If we profile them, we create MORE function spans, which trigger MORE resolver calls, etc.
|
|
495
|
+
_TELEMETRY_RESOLVERS_TO_SKIP = {
|
|
496
|
+
'collectFunctionSpans', 'collect_function_spans',
|
|
497
|
+
'collectNetworkRequest', 'collect_network_request',
|
|
498
|
+
'collectNetworkHops', 'collect_network_hops',
|
|
499
|
+
'collectLogs', 'collect_logs',
|
|
500
|
+
'collectPrintStatements', 'collect_print_statements',
|
|
501
|
+
'collectExceptions', 'collect_exceptions',
|
|
502
|
+
'collectConsoleLogs', 'collect_console_logs',
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
# Also check the GraphQL field name from info (might differ from Python function name)
|
|
506
|
+
graphql_field_name = info.field_name if hasattr(info, 'field_name') else None
|
|
507
|
+
|
|
508
|
+
# Detect if this is a trivial field resolver (simple property accessor)
|
|
509
|
+
# IMPORTANT: Only skip trivial resolvers if we couldn't extract func details
|
|
510
|
+
# If we successfully extracted filename/line_no, it's a real resolver we should capture
|
|
511
|
+
is_trivial_resolver = False
|
|
512
|
+
if not resolver_func:
|
|
513
|
+
# No resolver function found at all - this is a property accessor
|
|
514
|
+
is_trivial_resolver = True
|
|
515
|
+
elif filename == "<unknown>" or func_name == "<unknown>":
|
|
516
|
+
# Failed to extract details - likely trivial
|
|
517
|
+
is_trivial_resolver = True
|
|
518
|
+
elif resolver_func and hasattr(resolver_func, '__code__'):
|
|
519
|
+
code = resolver_func.__code__
|
|
520
|
+
# Very short bytecode (< 30 bytes) indicates simple property access like "return self.field"
|
|
521
|
+
if code.co_code and len(code.co_code) < 30:
|
|
522
|
+
is_trivial_resolver = True
|
|
523
|
+
|
|
524
|
+
# Check both Python function name and GraphQL field name for telemetry resolvers
|
|
525
|
+
is_telemetry_resolver = (
|
|
526
|
+
func_name in _TELEMETRY_RESOLVERS_TO_SKIP or
|
|
527
|
+
graphql_field_name in _TELEMETRY_RESOLVERS_TO_SKIP
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
# Skip if it's a telemetry resolver or trivial field resolver
|
|
531
|
+
if is_telemetry_resolver or is_trivial_resolver:
|
|
532
|
+
# Skip profiling entirely - no network hops, no function spans
|
|
533
|
+
# This prevents infinite loops when function spans are sent to GraphQL endpoint
|
|
534
|
+
return _next(root, info, *args, **kwargs)
|
|
535
|
+
|
|
536
|
+
# CRITICAL: Skip resolvers from site-packages (unless explicitly enabled)
|
|
537
|
+
# This respects the SF_FUNCSPAN_CAPTURE_INSTALLED_PACKAGES setting
|
|
538
|
+
if not SF_FUNCSPAN_CAPTURE_INSTALLED_PACKAGES and filename != "<unknown>":
|
|
539
|
+
if "site-packages" in filename or "dist-packages" in filename:
|
|
540
|
+
return _next(root, info, *args, **kwargs)
|
|
541
|
+
|
|
542
|
+
# Auto-profile sync GraphQL resolvers with function span capture
|
|
543
|
+
try:
|
|
544
|
+
if _HAS_NATIVE and app_config._interceptors_initialized:
|
|
545
|
+
# Apply sampling BEFORE pushing span - respect per-function sample_rate config
|
|
546
|
+
try:
|
|
547
|
+
func_config = _sffuncspan.get_function_config(filename, func_name)
|
|
548
|
+
sample_rate = func_config.get('sample_rate', 1.0) if func_config else 1.0
|
|
549
|
+
if sample_rate < 1.0 and random.random() >= sample_rate:
|
|
550
|
+
# Sampled out - skip span capture, but still do network hop tracing
|
|
551
|
+
tracer = self._make_tracer(info)
|
|
552
|
+
sys.setprofile(tracer)
|
|
553
|
+
try:
|
|
554
|
+
result = _next(root, info, *args, **kwargs)
|
|
555
|
+
finally:
|
|
556
|
+
sys.setprofile(None)
|
|
557
|
+
return result
|
|
558
|
+
except:
|
|
559
|
+
# If config lookup fails, proceed with capture (default to capturing)
|
|
560
|
+
pass
|
|
561
|
+
|
|
562
|
+
# Generate span ID
|
|
563
|
+
span_id = _sffuncspan.generate_span_id()
|
|
564
|
+
parent_span_id = _sffuncspan.peek_parent_span_id()
|
|
565
|
+
|
|
566
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
567
|
+
print(f"[[Strawberry]] BEFORE PUSH: resolver={func_name}, parent_span_id={parent_span_id} (should be None for top-level resolver)", log=False)
|
|
568
|
+
|
|
569
|
+
# NOTE: We do NOT call cache_config here because:
|
|
570
|
+
# 1. Nested async function capture doesn't work (Python profiler limitation)
|
|
571
|
+
# 2. Hardcoded values would override user configuration
|
|
572
|
+
# 3. Let the function span profiler use its default configuration
|
|
573
|
+
|
|
574
|
+
_sffuncspan.push_span(span_id)
|
|
575
|
+
|
|
576
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
577
|
+
print(f"[[Strawberry]] AFTER PUSH: Pushed span_id={span_id} for resolver={func_name}", log=False)
|
|
578
|
+
# Verify it's in the stack
|
|
579
|
+
current = get_current_function_span_id()
|
|
580
|
+
print(f"[[Strawberry]] AFTER PUSH: get_current_function_span_id()={current}", log=False)
|
|
581
|
+
|
|
582
|
+
# resolver_func, func_name, filename, line_no already set above from info
|
|
583
|
+
|
|
584
|
+
# Capture arguments (lightweight - just resolver name)
|
|
585
|
+
arguments_json = json.dumps({
|
|
586
|
+
"resolver": func_name,
|
|
587
|
+
"root_type": type(root).__name__ if root else None
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
# Record start time
|
|
591
|
+
start_ns = _sffuncspan.get_epoch_ns()
|
|
592
|
+
|
|
593
|
+
# NOTE: C profiler is already running globally (started during app init)
|
|
594
|
+
# We do NOT start/stop it per resolver - just manage the span stack
|
|
595
|
+
|
|
596
|
+
# Execute resolver
|
|
597
|
+
exception_occurred = True # Initialize before try block
|
|
598
|
+
try:
|
|
599
|
+
result = _next(root, info, *args, **kwargs)
|
|
600
|
+
exception_occurred = False
|
|
601
|
+
except Exception as e:
|
|
602
|
+
exception_occurred = True
|
|
603
|
+
result = None
|
|
604
|
+
raise
|
|
605
|
+
|
|
606
|
+
# CRITICAL: Check if result is a coroutine (async resolver)
|
|
607
|
+
# If so, we need to wrap it to keep the span on the stack during execution
|
|
608
|
+
if inspect.iscoroutine(result):
|
|
609
|
+
# Async resolver - wrap the coroutine to maintain span context
|
|
610
|
+
async def wrapped_async_result():
|
|
611
|
+
# Span is still on stack from push above
|
|
612
|
+
# C profiler is already running globally, so it will capture nested calls
|
|
613
|
+
exception_occurred_inner = True # Initialize before try block
|
|
614
|
+
try:
|
|
615
|
+
actual_result = await result
|
|
616
|
+
exception_occurred_inner = False
|
|
617
|
+
return actual_result
|
|
618
|
+
except Exception as e:
|
|
619
|
+
exception_occurred_inner = True
|
|
620
|
+
raise
|
|
621
|
+
finally:
|
|
622
|
+
# Now pop the span after async execution completes
|
|
623
|
+
end_ns = _sffuncspan.get_epoch_ns()
|
|
624
|
+
duration_ns = end_ns - start_ns
|
|
625
|
+
_sffuncspan.pop_span()
|
|
626
|
+
|
|
627
|
+
# Record span
|
|
628
|
+
return_value_json = None
|
|
629
|
+
if not exception_occurred_inner and 'actual_result' in locals():
|
|
630
|
+
try:
|
|
631
|
+
# Use C serializer for full value capture (not just type metadata)
|
|
632
|
+
return_value_json = _sffuncspan.serialize_value(actual_result)
|
|
633
|
+
except:
|
|
634
|
+
return_value_json = None
|
|
635
|
+
|
|
636
|
+
try:
|
|
637
|
+
_, session_id = get_or_set_sf_trace_id()
|
|
638
|
+
_sffuncspan.record_span(
|
|
639
|
+
session_id, span_id, parent_span_id,
|
|
640
|
+
filename, line_no, 0, func_name,
|
|
641
|
+
arguments_json, return_value_json,
|
|
642
|
+
start_ns, duration_ns,
|
|
643
|
+
)
|
|
644
|
+
except Exception as e:
|
|
645
|
+
pass
|
|
646
|
+
|
|
647
|
+
return wrapped_async_result()
|
|
648
|
+
|
|
649
|
+
# Sync resolver - pop span immediately (C profiler stays running)
|
|
650
|
+
end_ns = _sffuncspan.get_epoch_ns()
|
|
651
|
+
duration_ns = end_ns - start_ns
|
|
652
|
+
_sffuncspan.pop_span()
|
|
653
|
+
|
|
654
|
+
# Capture return value using C serializer
|
|
655
|
+
return_value_json = None
|
|
656
|
+
if not exception_occurred:
|
|
657
|
+
try:
|
|
658
|
+
# Use C serializer for full value capture (not just type metadata)
|
|
659
|
+
return_value_json = _sffuncspan.serialize_value(result)
|
|
660
|
+
except:
|
|
661
|
+
return_value_json = None
|
|
662
|
+
|
|
663
|
+
# Record span
|
|
664
|
+
try:
|
|
665
|
+
_, session_id = get_or_set_sf_trace_id()
|
|
666
|
+
_sffuncspan.record_span(
|
|
667
|
+
session_id,
|
|
668
|
+
span_id,
|
|
669
|
+
parent_span_id,
|
|
670
|
+
filename,
|
|
671
|
+
line_no,
|
|
672
|
+
0, # column
|
|
673
|
+
func_name,
|
|
674
|
+
arguments_json,
|
|
675
|
+
return_value_json,
|
|
676
|
+
start_ns,
|
|
677
|
+
duration_ns,
|
|
678
|
+
)
|
|
679
|
+
except Exception as e:
|
|
680
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
681
|
+
print(f"[[Strawberry]] Failed to record funcspan: {e}", log=False)
|
|
682
|
+
|
|
683
|
+
return result
|
|
684
|
+
else:
|
|
685
|
+
# Fallback: just do network hop tracing
|
|
686
|
+
tracer = self._make_tracer(info)
|
|
687
|
+
sys.setprofile(tracer)
|
|
688
|
+
try:
|
|
689
|
+
return _next(root, info, *args, **kwargs)
|
|
690
|
+
finally:
|
|
691
|
+
sys.setprofile(None)
|
|
692
|
+
except ImportError:
|
|
693
|
+
# sf_funcspan not available, just do network hop tracing
|
|
694
|
+
tracer = self._make_tracer(info)
|
|
695
|
+
sys.setprofile(tracer)
|
|
696
|
+
try:
|
|
697
|
+
return _next(root, info, *args, **kwargs)
|
|
698
|
+
finally:
|
|
699
|
+
sys.setprofile(None) # safety-net
|
|
700
|
+
finally:
|
|
701
|
+
# Clean up tracking flag to prevent memory leaks
|
|
702
|
+
if hasattr(_sf_processing_tracker, 'processing') and field_key in _sf_processing_tracker.processing:
|
|
703
|
+
del _sf_processing_tracker.processing[field_key]
|
|
704
|
+
|
|
705
|
+
async def resolve_async(self, _next, root, info, *args, **kwargs):
|
|
706
|
+
# Get the actual resolver from info.field_name
|
|
707
|
+
# _next is another extension in the chain, not the actual user resolver!
|
|
708
|
+
resolver_func = None
|
|
709
|
+
func_name = "<unknown>"
|
|
710
|
+
filename = "<unknown>"
|
|
711
|
+
line_no = 0
|
|
712
|
+
|
|
713
|
+
# Try multiple approaches to get the actual resolver function
|
|
714
|
+
# Strawberry stores resolvers in different places depending on the field type
|
|
715
|
+
|
|
716
|
+
# Approach 1: Check if info has python_name (Strawberry field attribute)
|
|
717
|
+
if hasattr(info, 'python_name') and hasattr(root, info.python_name):
|
|
718
|
+
attr = getattr(root, info.python_name, None)
|
|
719
|
+
if callable(attr):
|
|
720
|
+
resolver_func = attr
|
|
721
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
722
|
+
print(f"[[Strawberry]] ASYNC Got resolver from root.{info.python_name}: {resolver_func}", log=False)
|
|
723
|
+
|
|
724
|
+
# Approach 2: Try _field.base_resolver
|
|
725
|
+
if not resolver_func and hasattr(info, '_field') and hasattr(info._field, 'base_resolver'):
|
|
726
|
+
resolver_func = info._field.base_resolver
|
|
727
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
728
|
+
print(f"[[Strawberry]] ASYNC Got resolver from info._field.base_resolver: {resolver_func}", log=False)
|
|
729
|
+
|
|
730
|
+
# Approach 3: Try parent_type and field_name to look up the resolver
|
|
731
|
+
if not resolver_func and hasattr(info, 'parent_type') and hasattr(info, 'field_name'):
|
|
732
|
+
# Get the Strawberry type definition from the parent
|
|
733
|
+
parent_type_name = info.parent_type.name if hasattr(info.parent_type, 'name') else None
|
|
734
|
+
if parent_type_name and hasattr(root, '__class__'):
|
|
735
|
+
# Try to get the method from the root object's class
|
|
736
|
+
method = getattr(root.__class__, info.field_name, None)
|
|
737
|
+
if method and callable(method):
|
|
738
|
+
resolver_func = method
|
|
739
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
740
|
+
print(f"[[Strawberry]] ASYNC Got resolver from root.__class__.{info.field_name}: {resolver_func}", log=False)
|
|
741
|
+
|
|
742
|
+
# Approach 4: Try to get resolver directly from the field definition
|
|
743
|
+
if not resolver_func and hasattr(info, '_field'):
|
|
744
|
+
field = info._field
|
|
745
|
+
# Try multiple attributes where Strawberry might store the resolver
|
|
746
|
+
for attr_name in ['base_resolver', 'resolver', 'wrapped_func', 'python_name']:
|
|
747
|
+
if hasattr(field, attr_name):
|
|
748
|
+
candidate = getattr(field, attr_name)
|
|
749
|
+
if callable(candidate):
|
|
750
|
+
resolver_func = candidate
|
|
751
|
+
break
|
|
752
|
+
|
|
753
|
+
# Fallback: just use field_name
|
|
754
|
+
if not resolver_func and hasattr(info, 'field_name'):
|
|
755
|
+
func_name = info.field_name
|
|
756
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
757
|
+
print(f"[[Strawberry]] ASYNC Using field_name only: {func_name}", log=False)
|
|
758
|
+
|
|
759
|
+
# If we found a resolver function, extract its details
|
|
760
|
+
if resolver_func:
|
|
761
|
+
# Unwrap decorators and get the actual function
|
|
762
|
+
actual_func = resolver_func
|
|
763
|
+
# Handle bound methods
|
|
764
|
+
if hasattr(actual_func, '__func__'):
|
|
765
|
+
actual_func = actual_func.__func__
|
|
766
|
+
# Handle wrapped functions
|
|
767
|
+
while hasattr(actual_func, '__wrapped__'):
|
|
768
|
+
actual_func = actual_func.__wrapped__
|
|
769
|
+
|
|
770
|
+
# CRITICAL: If we got Strawberry's _async_resolver wrapper, try to extract the real user function
|
|
771
|
+
# Strawberry wraps user resolvers in _async_resolver (defined in schema_converter.py)
|
|
772
|
+
# We need to look inside the closure to find the actual user function
|
|
773
|
+
if hasattr(actual_func, '__code__') and actual_func.__name__ == '_async_resolver':
|
|
774
|
+
if 'site-packages/strawberry' in actual_func.__code__.co_filename:
|
|
775
|
+
# This is Strawberry's wrapper - try to extract the real resolver from closure
|
|
776
|
+
if hasattr(actual_func, '__closure__') and actual_func.__closure__:
|
|
777
|
+
for cell in actual_func.__closure__:
|
|
778
|
+
try:
|
|
779
|
+
cell_content = cell.cell_contents
|
|
780
|
+
# Look for a callable that's not from site-packages
|
|
781
|
+
if callable(cell_content) and hasattr(cell_content, '__code__'):
|
|
782
|
+
cell_filename = cell_content.__code__.co_filename
|
|
783
|
+
if 'site-packages' not in cell_filename and 'dist-packages' not in cell_filename:
|
|
784
|
+
# Found the user function!
|
|
785
|
+
actual_func = cell_content
|
|
786
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
787
|
+
print(f"[[Strawberry]] ASYNC Unwrapped _async_resolver to user function: {cell_content.__name__} @ {cell_filename}", log=False)
|
|
788
|
+
break
|
|
789
|
+
except (AttributeError, ValueError):
|
|
790
|
+
pass
|
|
791
|
+
|
|
792
|
+
if hasattr(actual_func, '__code__'):
|
|
793
|
+
filename = actual_func.__code__.co_filename
|
|
794
|
+
func_name = actual_func.__name__
|
|
795
|
+
line_no = actual_func.__code__.co_firstlineno
|
|
796
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
797
|
+
print(f"[[Strawberry]] ASYNC Resolver details: {func_name} @ {filename}:{line_no}, is_async={inspect.iscoroutinefunction(actual_func)}", log=False)
|
|
798
|
+
|
|
799
|
+
# CRITICAL: Skip profiling telemetry collection resolvers to prevent infinite loops
|
|
800
|
+
_TELEMETRY_RESOLVERS_TO_SKIP = {
|
|
801
|
+
'collectFunctionSpans', 'collect_function_spans',
|
|
802
|
+
'collectNetworkRequest', 'collect_network_request',
|
|
803
|
+
'collectNetworkHops', 'collect_network_hops',
|
|
804
|
+
'collectLogs', 'collect_logs',
|
|
805
|
+
'collectPrintStatements', 'collect_print_statements',
|
|
806
|
+
'collectExceptions', 'collect_exceptions',
|
|
807
|
+
'collectConsoleLogs', 'collect_console_logs',
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
# Also check the GraphQL field name from info (might differ from Python function name)
|
|
811
|
+
graphql_field_name = info.field_name if hasattr(info, 'field_name') else None
|
|
812
|
+
|
|
813
|
+
# Detect if this is a trivial field resolver (simple property accessor)
|
|
814
|
+
# IMPORTANT: Only skip trivial resolvers if we couldn't extract func details
|
|
815
|
+
# If we successfully extracted filename/line_no, it's a real resolver we should capture
|
|
816
|
+
is_trivial_resolver = False
|
|
817
|
+
if not resolver_func:
|
|
818
|
+
# No resolver function found at all - this is a property accessor
|
|
819
|
+
is_trivial_resolver = True
|
|
820
|
+
elif filename == "<unknown>" or func_name == "<unknown>":
|
|
821
|
+
# Failed to extract details - likely trivial
|
|
822
|
+
is_trivial_resolver = True
|
|
823
|
+
elif resolver_func and hasattr(resolver_func, '__code__'):
|
|
824
|
+
code = resolver_func.__code__
|
|
825
|
+
# Very short bytecode (< 30 bytes) indicates simple property access like "return self.field"
|
|
826
|
+
if code.co_code and len(code.co_code) < 30:
|
|
827
|
+
is_trivial_resolver = True
|
|
828
|
+
|
|
829
|
+
# Check both Python function name and GraphQL field name for telemetry resolvers
|
|
830
|
+
is_telemetry_resolver = (
|
|
831
|
+
func_name in _TELEMETRY_RESOLVERS_TO_SKIP or
|
|
832
|
+
graphql_field_name in _TELEMETRY_RESOLVERS_TO_SKIP
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
if is_telemetry_resolver or is_trivial_resolver:
|
|
836
|
+
# Skip profiling entirely - no network hops, no function spans
|
|
837
|
+
# This prevents infinite loops when function spans are sent to GraphQL endpoint
|
|
838
|
+
return await _next(root, info, *args, **kwargs)
|
|
839
|
+
|
|
840
|
+
# CRITICAL: Skip resolvers from site-packages (unless explicitly enabled)
|
|
841
|
+
# This respects the SF_FUNCSPAN_CAPTURE_INSTALLED_PACKAGES setting
|
|
842
|
+
if not SF_FUNCSPAN_CAPTURE_INSTALLED_PACKAGES and filename != "<unknown>":
|
|
843
|
+
if "site-packages" in filename or "dist-packages" in filename:
|
|
844
|
+
return await _next(root, info, *args, **kwargs)
|
|
845
|
+
|
|
846
|
+
# Auto-profile async GraphQL resolvers with function span capture
|
|
847
|
+
# This works around Python's sys.setprofile limitation with async functions
|
|
848
|
+
try:
|
|
849
|
+
if _HAS_NATIVE and app_config._interceptors_initialized:
|
|
850
|
+
# Apply sampling BEFORE pushing span - respect per-function sample_rate config
|
|
851
|
+
try:
|
|
852
|
+
func_config = _sffuncspan.get_function_config(filename, func_name)
|
|
853
|
+
sample_rate = func_config.get('sample_rate', 1.0) if func_config else 1.0
|
|
854
|
+
if sample_rate < 1.0 and random.random() >= sample_rate:
|
|
855
|
+
# Sampled out - skip span capture, but still do network hop tracing
|
|
856
|
+
tracer = self._make_tracer(info)
|
|
857
|
+
sys.setprofile(tracer)
|
|
858
|
+
try:
|
|
859
|
+
result = await _next(root, info, *args, **kwargs)
|
|
860
|
+
finally:
|
|
861
|
+
sys.setprofile(None)
|
|
862
|
+
return result
|
|
863
|
+
except:
|
|
864
|
+
# If config lookup fails, proceed with capture (default to capturing)
|
|
865
|
+
pass
|
|
866
|
+
|
|
867
|
+
# Generate span ID
|
|
868
|
+
span_id = _sffuncspan.generate_span_id()
|
|
869
|
+
parent_span_id = _sffuncspan.peek_parent_span_id()
|
|
870
|
+
_sffuncspan.push_span(span_id)
|
|
871
|
+
|
|
872
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
873
|
+
print(f"[[Strawberry]] ASYNC: Pushed span_id={span_id} for resolver={func_name}, parent={parent_span_id}", log=False)
|
|
874
|
+
# Verify it's in the stack
|
|
875
|
+
current = get_current_function_span_id()
|
|
876
|
+
print(f"[[Strawberry]] ASYNC: get_current_function_span_id()={current}", log=False)
|
|
877
|
+
|
|
878
|
+
# resolver_func, func_name, filename, line_no already set above from info
|
|
879
|
+
|
|
880
|
+
# Capture arguments (lightweight - just resolver name)
|
|
881
|
+
arguments_json = json.dumps({
|
|
882
|
+
"resolver": func_name,
|
|
883
|
+
"root_type": type(root).__name__ if root else None
|
|
884
|
+
})
|
|
885
|
+
|
|
886
|
+
# Record start time
|
|
887
|
+
start_ns = _sffuncspan.get_epoch_ns()
|
|
888
|
+
|
|
889
|
+
# Note: Profiler is already installed by unified_interceptor.py
|
|
890
|
+
|
|
891
|
+
# Execute resolver - C profiler will capture all child functions
|
|
892
|
+
exception_occurred = True # Initialize before try block
|
|
893
|
+
try:
|
|
894
|
+
result = await _next(root, info, *args, **kwargs)
|
|
895
|
+
exception_occurred = False
|
|
896
|
+
except Exception as e:
|
|
897
|
+
exception_occurred = True
|
|
898
|
+
result = None
|
|
899
|
+
raise
|
|
900
|
+
finally:
|
|
901
|
+
# Record end time
|
|
902
|
+
end_ns = _sffuncspan.get_epoch_ns()
|
|
903
|
+
duration_ns = end_ns - start_ns
|
|
904
|
+
|
|
905
|
+
# Pop span
|
|
906
|
+
_sffuncspan.pop_span()
|
|
907
|
+
|
|
908
|
+
# Capture return value using C serializer
|
|
909
|
+
return_value_json = None
|
|
910
|
+
if not exception_occurred:
|
|
911
|
+
try:
|
|
912
|
+
# Use C serializer for full value capture (not just type metadata)
|
|
913
|
+
return_value_json = _sffuncspan.serialize_value(result)
|
|
914
|
+
except:
|
|
915
|
+
return_value_json = None
|
|
916
|
+
|
|
917
|
+
# Record span
|
|
918
|
+
try:
|
|
919
|
+
_, session_id = get_or_set_sf_trace_id()
|
|
920
|
+
_sffuncspan.record_span(
|
|
921
|
+
session_id,
|
|
922
|
+
span_id,
|
|
923
|
+
parent_span_id,
|
|
924
|
+
filename,
|
|
925
|
+
line_no,
|
|
926
|
+
0, # column
|
|
927
|
+
func_name,
|
|
928
|
+
arguments_json,
|
|
929
|
+
return_value_json,
|
|
930
|
+
start_ns,
|
|
931
|
+
duration_ns,
|
|
932
|
+
)
|
|
933
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
934
|
+
print(f"[[Strawberry]] Recorded funcspan for async resolver: {func_name}", log=False)
|
|
935
|
+
except Exception as e:
|
|
936
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
937
|
+
print(f"[[Strawberry]] Failed to record funcspan: {e}", log=False)
|
|
938
|
+
|
|
939
|
+
return result
|
|
940
|
+
else:
|
|
941
|
+
# Fallback: just do network hop tracing
|
|
942
|
+
tracer = self._make_tracer(info)
|
|
943
|
+
sys.setprofile(tracer)
|
|
944
|
+
try:
|
|
945
|
+
return await _next(root, info, *args, **kwargs)
|
|
946
|
+
finally:
|
|
947
|
+
sys.setprofile(None)
|
|
948
|
+
except ImportError:
|
|
949
|
+
# sf_funcspan not available, just do network hop tracing
|
|
950
|
+
tracer = self._make_tracer(info)
|
|
951
|
+
sys.setprofile(tracer)
|
|
952
|
+
try:
|
|
953
|
+
return await _next(root, info, *args, **kwargs)
|
|
954
|
+
finally:
|
|
955
|
+
sys.setprofile(None)
|
|
956
|
+
|
|
957
|
+
# ---------------- OTEL-STYLE: Emit after request completes ---------------- #
|
|
958
|
+
def on_request_end(self):
|
|
959
|
+
"""Capture response data and emit network hops AFTER GraphQL response is built."""
|
|
960
|
+
# Capture response data first
|
|
961
|
+
if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY and self.execution_context.result:
|
|
962
|
+
try:
|
|
963
|
+
# GraphQL result includes data and errors
|
|
964
|
+
result_data = {
|
|
965
|
+
"data": (
|
|
966
|
+
self.execution_context.result.data
|
|
967
|
+
if self.execution_context.result.data
|
|
968
|
+
else None
|
|
969
|
+
),
|
|
970
|
+
"errors": (
|
|
971
|
+
[str(e) for e in self.execution_context.result.errors]
|
|
972
|
+
if self.execution_context.result.errors
|
|
973
|
+
else None
|
|
974
|
+
),
|
|
975
|
+
}
|
|
976
|
+
if HAS_ORJSON:
|
|
977
|
+
result_str = orjson.dumps(result_data, default=str)[
|
|
978
|
+
:_RESPONSE_LIMIT_BYTES
|
|
979
|
+
]
|
|
980
|
+
else:
|
|
981
|
+
result_str = json.dumps(result_data, default=str)[
|
|
982
|
+
:_RESPONSE_LIMIT_BYTES
|
|
983
|
+
]
|
|
984
|
+
self._response_data["body"] = result_str.encode("utf-8")
|
|
985
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
986
|
+
print(
|
|
987
|
+
f"[[Strawberry]] Captured GraphQL result: {len(result_str)} chars",
|
|
988
|
+
log=False,
|
|
989
|
+
)
|
|
990
|
+
except Exception as e:
|
|
991
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
992
|
+
print(
|
|
993
|
+
f"[[Strawberry]] Failed to capture response data: {e}",
|
|
994
|
+
log=False,
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
# Get captured data
|
|
998
|
+
req_headers = self._request_data.get("headers")
|
|
999
|
+
req_body = self._request_data.get("body")
|
|
1000
|
+
resp_headers = self._response_data.get(
|
|
1001
|
+
"headers"
|
|
1002
|
+
) # Not typically available in GraphQL
|
|
1003
|
+
resp_body = self._response_data.get("body")
|
|
1004
|
+
|
|
1005
|
+
# Emit network hops for all captured resolvers
|
|
1006
|
+
for endpoint_info in self._captured_endpoints:
|
|
1007
|
+
endpoint_id = endpoint_info.get("endpoint_id")
|
|
1008
|
+
|
|
1009
|
+
try:
|
|
1010
|
+
_, session_id = get_or_set_sf_trace_id()
|
|
1011
|
+
|
|
1012
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1013
|
+
print(
|
|
1014
|
+
f"[[Strawberry]] Emitting hop for {endpoint_info['name']}: "
|
|
1015
|
+
f"req_headers={'present' if req_headers else 'None'}, "
|
|
1016
|
+
f"req_body={len(req_body) if req_body else 0} bytes, "
|
|
1017
|
+
f"resp_body={len(resp_body) if resp_body else 0} bytes",
|
|
1018
|
+
log=False,
|
|
1019
|
+
)
|
|
1020
|
+
|
|
1021
|
+
# Extract raw path and query string for C to parse (if available from context)
|
|
1022
|
+
raw_path = None
|
|
1023
|
+
raw_query = b""
|
|
1024
|
+
try:
|
|
1025
|
+
if hasattr(self.execution_context, "context"):
|
|
1026
|
+
ctx = self.execution_context.context
|
|
1027
|
+
if hasattr(ctx, "request"):
|
|
1028
|
+
req = ctx.request
|
|
1029
|
+
# Try to get path - different frameworks have different attributes
|
|
1030
|
+
if hasattr(req, "path"):
|
|
1031
|
+
raw_path = str(req.path)
|
|
1032
|
+
elif hasattr(req, "url") and hasattr(req.url, "path"):
|
|
1033
|
+
raw_path = str(req.url.path)
|
|
1034
|
+
|
|
1035
|
+
# Try to get query string
|
|
1036
|
+
if hasattr(req, "query_string"):
|
|
1037
|
+
raw_query = (
|
|
1038
|
+
req.query_string
|
|
1039
|
+
if isinstance(req.query_string, bytes)
|
|
1040
|
+
else req.query_string.encode("utf-8")
|
|
1041
|
+
)
|
|
1042
|
+
elif (
|
|
1043
|
+
hasattr(req, "META") and "QUERY_STRING" in req.META
|
|
1044
|
+
):
|
|
1045
|
+
raw_query = req.META["QUERY_STRING"].encode("utf-8")
|
|
1046
|
+
except Exception as e:
|
|
1047
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1048
|
+
print(
|
|
1049
|
+
f"[[Strawberry]] Failed to extract path/query: {e}",
|
|
1050
|
+
log=False,
|
|
1051
|
+
)
|
|
1052
|
+
|
|
1053
|
+
# Use fast path if C extension available
|
|
1054
|
+
if endpoint_id is not None and endpoint_id >= 0:
|
|
1055
|
+
fast_send_network_hop_fast(
|
|
1056
|
+
session_id=session_id,
|
|
1057
|
+
endpoint_id=endpoint_id,
|
|
1058
|
+
raw_path=raw_path,
|
|
1059
|
+
raw_query_string=raw_query,
|
|
1060
|
+
request_headers=req_headers,
|
|
1061
|
+
request_body=req_body,
|
|
1062
|
+
response_headers=resp_headers,
|
|
1063
|
+
response_body=resp_body,
|
|
1064
|
+
)
|
|
1065
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1066
|
+
print(
|
|
1067
|
+
f"[[Strawberry]] Emitted network hop (fast path): {endpoint_info['name']} "
|
|
1068
|
+
f"endpoint_id={endpoint_id} session={session_id}",
|
|
1069
|
+
log=False,
|
|
1070
|
+
)
|
|
1071
|
+
else:
|
|
1072
|
+
# Fallback to old Python API (doesn't support body/header capture)
|
|
1073
|
+
fast_send_network_hop(
|
|
1074
|
+
session_id=session_id,
|
|
1075
|
+
line=str(endpoint_info["line"]),
|
|
1076
|
+
column="0",
|
|
1077
|
+
name=endpoint_info["name"],
|
|
1078
|
+
entrypoint=endpoint_info["filename"],
|
|
1079
|
+
)
|
|
1080
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1081
|
+
print(
|
|
1082
|
+
f"[[Strawberry]] Emitted network hop (fallback): {endpoint_info['name']} "
|
|
1083
|
+
f"session={session_id}",
|
|
1084
|
+
log=False,
|
|
1085
|
+
)
|
|
1086
|
+
except Exception as e: # noqa: BLE001 S110
|
|
1087
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1088
|
+
print(
|
|
1089
|
+
f"[[Strawberry]] Failed to emit network hop: {e}", log=False
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1092
|
+
return NetworkHopExtension
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
def patch_strawberry_module(strawberry):
|
|
1096
|
+
"""Patch Strawberry to ensure exceptions go through the custom excepthook."""
|
|
1097
|
+
global _is_strawberry_patched
|
|
1098
|
+
if _is_strawberry_patched:
|
|
1099
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1100
|
+
print(
|
|
1101
|
+
"[[DEBUG]] Strawberry has already been patched, skipping. [[/DEBUG]]",
|
|
1102
|
+
log=False,
|
|
1103
|
+
)
|
|
1104
|
+
return
|
|
1105
|
+
|
|
1106
|
+
try:
|
|
1107
|
+
# Backup the original execute method from Strawberry
|
|
1108
|
+
original_execute = strawberry.execution.execute.execute
|
|
1109
|
+
|
|
1110
|
+
async def custom_execute(*args, **kwargs):
|
|
1111
|
+
try:
|
|
1112
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1113
|
+
print(
|
|
1114
|
+
"[[DEBUG]] Executing patched Strawberry execute function. [[/DEBUG]]",
|
|
1115
|
+
log=False,
|
|
1116
|
+
)
|
|
1117
|
+
return await original_execute(*args, **kwargs)
|
|
1118
|
+
except Exception as e:
|
|
1119
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1120
|
+
print(
|
|
1121
|
+
"[[DEBUG]] Intercepted exception in Strawberry execute. [[/DEBUG]]",
|
|
1122
|
+
log=False,
|
|
1123
|
+
)
|
|
1124
|
+
# Invoke custom excepthook globally
|
|
1125
|
+
sys.excepthook(type(e), e, e.__traceback__)
|
|
1126
|
+
raise
|
|
1127
|
+
|
|
1128
|
+
# Replace Strawberry's execute function with the patched version
|
|
1129
|
+
strawberry.execution.execute.execute = custom_execute
|
|
1130
|
+
_is_strawberry_patched = True
|
|
1131
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1132
|
+
print(
|
|
1133
|
+
"[[DEBUG]] Successfully patched Strawberry execute function. [[/DEBUG]]",
|
|
1134
|
+
log=False,
|
|
1135
|
+
)
|
|
1136
|
+
except Exception as error:
|
|
1137
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1138
|
+
print(
|
|
1139
|
+
f"[[DEBUG]] Failed to patch Strawberry: {error}. [[/DEBUG]]", log=False
|
|
1140
|
+
)
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
class CustomImportHook:
|
|
1144
|
+
"""Import hook to intercept the import of 'strawberry' modules."""
|
|
1145
|
+
|
|
1146
|
+
def find_spec(self, fullname, path, target=None):
|
|
1147
|
+
global _is_strawberry_patched
|
|
1148
|
+
if fullname == "strawberry" and not _is_strawberry_patched:
|
|
1149
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1150
|
+
print(
|
|
1151
|
+
f"[[DEBUG]] Intercepting import of {fullname}. [[/DEBUG]]",
|
|
1152
|
+
log=False,
|
|
1153
|
+
)
|
|
1154
|
+
return find_spec(fullname)
|
|
1155
|
+
if fullname.startswith("strawberry_django"):
|
|
1156
|
+
return None # Let default import handle strawberry_django
|
|
1157
|
+
|
|
1158
|
+
def exec_module(self, module):
|
|
1159
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1160
|
+
print(
|
|
1161
|
+
f"[[DEBUG]] Executing module: {module.__name__}. [[/DEBUG]]", log=False
|
|
1162
|
+
)
|
|
1163
|
+
# Execute the module normally
|
|
1164
|
+
module_spec = module.__spec__
|
|
1165
|
+
if module_spec and module_spec.loader:
|
|
1166
|
+
module_spec.loader.exec_module(module)
|
|
1167
|
+
# Once strawberry is loaded, patch it
|
|
1168
|
+
if module.__name__ == "strawberry" and not _is_strawberry_patched:
|
|
1169
|
+
patch_strawberry_module(module)
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
def patch_schema():
|
|
1173
|
+
"""Patch strawberry.Schema to include both Sailfish and NetworkHop extensions by default."""
|
|
1174
|
+
try:
|
|
1175
|
+
import strawberry
|
|
1176
|
+
|
|
1177
|
+
original_schema_init = strawberry.Schema.__init__
|
|
1178
|
+
|
|
1179
|
+
def patched_schema_init(self, *args, extensions=None, **kwargs):
|
|
1180
|
+
if extensions is None:
|
|
1181
|
+
extensions = []
|
|
1182
|
+
|
|
1183
|
+
# Add the custom error handling extension
|
|
1184
|
+
sailfish_ext = get_extension()
|
|
1185
|
+
if sailfish_ext not in extensions:
|
|
1186
|
+
extensions.append(sailfish_ext)
|
|
1187
|
+
|
|
1188
|
+
# Add the network hop extension
|
|
1189
|
+
hop_ext = get_network_hop_extension()
|
|
1190
|
+
if hop_ext not in extensions:
|
|
1191
|
+
extensions.append(hop_ext)
|
|
1192
|
+
|
|
1193
|
+
# Call the original constructor
|
|
1194
|
+
original_schema_init(self, *args, extensions=extensions, **kwargs)
|
|
1195
|
+
|
|
1196
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1197
|
+
print(
|
|
1198
|
+
"[[DEBUG]] Patched strawberry.Schema to include Sailfish & NetworkHop extensions. [[/DEBUG]]",
|
|
1199
|
+
log=False,
|
|
1200
|
+
)
|
|
1201
|
+
|
|
1202
|
+
# Apply the patch
|
|
1203
|
+
strawberry.Schema.__init__ = patched_schema_init
|
|
1204
|
+
|
|
1205
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1206
|
+
print(
|
|
1207
|
+
"[[DEBUG]] Successfully patched strawberry.Schema. [[/DEBUG]]",
|
|
1208
|
+
log=False,
|
|
1209
|
+
)
|
|
1210
|
+
except ImportError:
|
|
1211
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1212
|
+
print(
|
|
1213
|
+
"[[DEBUG]] Strawberry is not installed. Skipping schema patching. [[/DEBUG]]",
|
|
1214
|
+
log=False,
|
|
1215
|
+
)
|
|
1216
|
+
|
|
1217
|
+
|
|
1218
|
+
def patch_views():
|
|
1219
|
+
"""
|
|
1220
|
+
Patch Strawberry view classes to capture and print request data on errors.
|
|
1221
|
+
This helps debug malformed requests when STRAWBERRY_DEBUG is enabled.
|
|
1222
|
+
Also transmits exceptions with full stack traces when CAPTURE_STRAWBERRY_ERRORS_WITH_DATA is enabled.
|
|
1223
|
+
"""
|
|
1224
|
+
if not STRAWBERRY_DEBUG and not CAPTURE_STRAWBERRY_ERRORS_WITH_DATA:
|
|
1225
|
+
return # Skip patching if neither debug mode nor capture mode is enabled
|
|
1226
|
+
|
|
1227
|
+
try:
|
|
1228
|
+
# Try to import Strawberry Django view
|
|
1229
|
+
try:
|
|
1230
|
+
from strawberry.django.views import GraphQLView as DjangoGraphQLView
|
|
1231
|
+
|
|
1232
|
+
_patch_view_class(DjangoGraphQLView, "Django")
|
|
1233
|
+
except ImportError:
|
|
1234
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1235
|
+
print(
|
|
1236
|
+
"[[DEBUG]] Strawberry Django view not found. [[/DEBUG]]", log=False
|
|
1237
|
+
)
|
|
1238
|
+
|
|
1239
|
+
# Try to import base async view (used by other integrations)
|
|
1240
|
+
try:
|
|
1241
|
+
from strawberry.http.async_base_view import AsyncBaseHTTPView
|
|
1242
|
+
|
|
1243
|
+
_patch_async_base_view(AsyncBaseHTTPView)
|
|
1244
|
+
except ImportError:
|
|
1245
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1246
|
+
print(
|
|
1247
|
+
"[[DEBUG]] Strawberry AsyncBaseHTTPView not found. [[/DEBUG]]",
|
|
1248
|
+
log=False,
|
|
1249
|
+
)
|
|
1250
|
+
|
|
1251
|
+
except Exception as e:
|
|
1252
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1253
|
+
print(
|
|
1254
|
+
f"[[DEBUG]] Failed to patch Strawberry views: {e}. [[/DEBUG]]",
|
|
1255
|
+
log=False,
|
|
1256
|
+
)
|
|
1257
|
+
|
|
1258
|
+
|
|
1259
|
+
def _patch_view_class(view_class, integration_name):
|
|
1260
|
+
"""Patch a Strawberry view class to capture request data on errors."""
|
|
1261
|
+
if hasattr(view_class, "_sf_patched"):
|
|
1262
|
+
return # Already patched
|
|
1263
|
+
|
|
1264
|
+
original_dispatch = view_class.dispatch
|
|
1265
|
+
|
|
1266
|
+
async def patched_dispatch(self, request, *args, **kwargs):
|
|
1267
|
+
# Capture raw request body before processing
|
|
1268
|
+
raw_body = None
|
|
1269
|
+
if STRAWBERRY_DEBUG or CAPTURE_STRAWBERRY_ERRORS_WITH_DATA:
|
|
1270
|
+
try:
|
|
1271
|
+
raw_body = request.body if hasattr(request, "body") else None
|
|
1272
|
+
except Exception:
|
|
1273
|
+
pass
|
|
1274
|
+
|
|
1275
|
+
try:
|
|
1276
|
+
return await original_dispatch(self, request, *args, **kwargs)
|
|
1277
|
+
except Exception as e:
|
|
1278
|
+
if (
|
|
1279
|
+
STRAWBERRY_DEBUG or CAPTURE_STRAWBERRY_ERRORS_WITH_DATA
|
|
1280
|
+
) and raw_body is not None:
|
|
1281
|
+
_print_request_debug_info(raw_body, e, integration_name)
|
|
1282
|
+
raise
|
|
1283
|
+
|
|
1284
|
+
view_class.dispatch = patched_dispatch
|
|
1285
|
+
view_class._sf_patched = True
|
|
1286
|
+
|
|
1287
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1288
|
+
print(
|
|
1289
|
+
f"[[DEBUG]] Patched Strawberry {integration_name} view for error debugging. [[/DEBUG]]",
|
|
1290
|
+
log=False,
|
|
1291
|
+
)
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
def _patch_async_base_view(view_class):
|
|
1295
|
+
"""Patch AsyncBaseHTTPView to capture request data on parse errors."""
|
|
1296
|
+
if hasattr(view_class, "_sf_parse_patched"):
|
|
1297
|
+
return # Already patched
|
|
1298
|
+
|
|
1299
|
+
original_parse = view_class.parse_http_body
|
|
1300
|
+
|
|
1301
|
+
async def patched_parse_http_body(self, request_adapter):
|
|
1302
|
+
# Capture raw body before parsing (but avoid consuming the stream twice)
|
|
1303
|
+
raw_body = None
|
|
1304
|
+
if STRAWBERRY_DEBUG or CAPTURE_STRAWBERRY_ERRORS_WITH_DATA:
|
|
1305
|
+
try:
|
|
1306
|
+
# Read the body once
|
|
1307
|
+
raw_body = await request_adapter.get_body()
|
|
1308
|
+
|
|
1309
|
+
# Patch request_adapter.get_body to return cached body
|
|
1310
|
+
# (body streams can only be read once)
|
|
1311
|
+
async def cached_get_body():
|
|
1312
|
+
return raw_body
|
|
1313
|
+
|
|
1314
|
+
request_adapter.get_body = cached_get_body
|
|
1315
|
+
except Exception:
|
|
1316
|
+
pass
|
|
1317
|
+
|
|
1318
|
+
try:
|
|
1319
|
+
return await original_parse(self, request_adapter)
|
|
1320
|
+
except Exception as e:
|
|
1321
|
+
logger.info("=" * 20 + " <STRAWBERRY> " + "=" * 20)
|
|
1322
|
+
logger.error(e)
|
|
1323
|
+
logger.info("=" * 20 + " </STRAWBERRY> " + "=" * 20)
|
|
1324
|
+
if (
|
|
1325
|
+
STRAWBERRY_DEBUG or CAPTURE_STRAWBERRY_ERRORS_WITH_DATA
|
|
1326
|
+
) and raw_body is not None:
|
|
1327
|
+
_print_request_debug_info(raw_body, e, "AsyncBaseHTTPView")
|
|
1328
|
+
raise
|
|
1329
|
+
|
|
1330
|
+
view_class.parse_http_body = patched_parse_http_body
|
|
1331
|
+
view_class._sf_parse_patched = True
|
|
1332
|
+
|
|
1333
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1334
|
+
print(
|
|
1335
|
+
"[[DEBUG]] Patched Strawberry AsyncBaseHTTPView.parse_http_body for error debugging. [[/DEBUG]]",
|
|
1336
|
+
log=False,
|
|
1337
|
+
)
|
|
1338
|
+
|
|
1339
|
+
|
|
1340
|
+
def _count_traceback_frames(tb):
|
|
1341
|
+
"""Count the number of frames in a traceback."""
|
|
1342
|
+
count = 0
|
|
1343
|
+
while tb is not None:
|
|
1344
|
+
count += 1
|
|
1345
|
+
tb = tb.tb_next
|
|
1346
|
+
return count
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
def _print_request_debug_info(raw_body, exception, source):
|
|
1350
|
+
"""Print debug information about the request that caused an error."""
|
|
1351
|
+
|
|
1352
|
+
# Transmit exception to Sailfish with full stack trace if enabled
|
|
1353
|
+
if CAPTURE_STRAWBERRY_ERRORS_WITH_DATA:
|
|
1354
|
+
try:
|
|
1355
|
+
# Verify that the exception has a traceback attached
|
|
1356
|
+
if (
|
|
1357
|
+
not hasattr(exception, "__traceback__")
|
|
1358
|
+
or exception.__traceback__ is None
|
|
1359
|
+
):
|
|
1360
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1361
|
+
print(
|
|
1362
|
+
f"[[STRAWBERRY_DEBUG]] WARNING: Exception {type(exception).__name__} has no __traceback__ attribute!",
|
|
1363
|
+
log=False,
|
|
1364
|
+
)
|
|
1365
|
+
else:
|
|
1366
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1367
|
+
print(
|
|
1368
|
+
f"[[STRAWBERRY_DEBUG]] Exception has traceback with {_count_traceback_frames(exception.__traceback__)} frames",
|
|
1369
|
+
log=False,
|
|
1370
|
+
)
|
|
1371
|
+
|
|
1372
|
+
transmit_exception_to_sailfish(exception, force_transmit=False)
|
|
1373
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1374
|
+
print(
|
|
1375
|
+
f"[[STRAWBERRY_DEBUG]] Transmitted exception to Sailfish: {type(exception).__name__}",
|
|
1376
|
+
log=False,
|
|
1377
|
+
)
|
|
1378
|
+
except Exception as transmit_err:
|
|
1379
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
1380
|
+
print(
|
|
1381
|
+
f"[[STRAWBERRY_DEBUG]] Failed to transmit exception: {transmit_err}",
|
|
1382
|
+
log=False,
|
|
1383
|
+
)
|
|
1384
|
+
|
|
1385
|
+
print(
|
|
1386
|
+
f"[[STRAWBERRY_DEBUG]] Transmission error traceback:\n{traceback.format_exc()}",
|
|
1387
|
+
log=False,
|
|
1388
|
+
)
|
|
1389
|
+
|
|
1390
|
+
# Print debug info if STRAWBERRY_DEBUG is enabled
|
|
1391
|
+
if not STRAWBERRY_DEBUG:
|
|
1392
|
+
return # Skip printing if debug mode is disabled
|
|
1393
|
+
|
|
1394
|
+
print("\n" + "=" * 80, log=False)
|
|
1395
|
+
print(f"[[STRAWBERRY_DEBUG]] Error in {source}", log=False)
|
|
1396
|
+
print("=" * 80, log=False)
|
|
1397
|
+
|
|
1398
|
+
# Print the exception
|
|
1399
|
+
print(f"\nException: {type(exception).__name__}: {exception}", log=False)
|
|
1400
|
+
print("\nTraceback:", log=False)
|
|
1401
|
+
print(traceback.format_exc(), log=False)
|
|
1402
|
+
|
|
1403
|
+
# Print raw body
|
|
1404
|
+
print("\n" + "-" * 80, log=False)
|
|
1405
|
+
print("Raw HTTP Body (bytes):", log=False)
|
|
1406
|
+
print("-" * 80, log=False)
|
|
1407
|
+
if isinstance(raw_body, bytes):
|
|
1408
|
+
print(f"Length: {len(raw_body)} bytes", log=False)
|
|
1409
|
+
print(f"Raw: {raw_body!r}", log=False)
|
|
1410
|
+
|
|
1411
|
+
# Try to decode and pretty-print as JSON
|
|
1412
|
+
try:
|
|
1413
|
+
decoded = raw_body.decode("utf-8")
|
|
1414
|
+
print(f"\nDecoded (UTF-8): {decoded}", log=False)
|
|
1415
|
+
|
|
1416
|
+
# Try to parse as JSON
|
|
1417
|
+
try:
|
|
1418
|
+
if HAS_ORJSON:
|
|
1419
|
+
parsed = orjson.loads(decoded)
|
|
1420
|
+
else:
|
|
1421
|
+
parsed = json.loads(decoded)
|
|
1422
|
+
print(f"\nParsed JSON (type: {type(parsed).__name__}):", log=False)
|
|
1423
|
+
if HAS_ORJSON:
|
|
1424
|
+
parsed = print(orjson.dumps(parsed, indent=2), log=False)
|
|
1425
|
+
else:
|
|
1426
|
+
parsed = print(json.dumps(parsed, indent=2), log=False)
|
|
1427
|
+
except json.JSONDecodeError as json_err:
|
|
1428
|
+
print(f"\nFailed to parse as JSON: {json_err}", log=False)
|
|
1429
|
+
except UnicodeDecodeError as decode_err:
|
|
1430
|
+
print(f"\nFailed to decode as UTF-8: {decode_err}", log=False)
|
|
1431
|
+
else:
|
|
1432
|
+
print(f"Body type: {type(raw_body).__name__}", log=False)
|
|
1433
|
+
print(f"Body: {raw_body!r}", log=False)
|
|
1434
|
+
|
|
1435
|
+
print("\n" + "=" * 80, log=False)
|
|
1436
|
+
print("[[/STRAWBERRY_DEBUG]]", log=False)
|
|
1437
|
+
print("=" * 80 + "\n", log=False)
|
|
1438
|
+
|
|
1439
|
+
|
|
1440
|
+
def patch_strawberry():
|
|
1441
|
+
"""
|
|
1442
|
+
Main entry point for patching Strawberry GraphQL.
|
|
1443
|
+
Applies both schema extensions and error debugging patches.
|
|
1444
|
+
"""
|
|
1445
|
+
patch_schema()
|
|
1446
|
+
patch_views()
|