sf-veritas 0.10.3__cp39-cp39-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-39-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnet.c +924 -0
- sf_veritas/_sffastnet.cpython-39-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnetworkrequest.c +730 -0
- sf_veritas/_sffastnetworkrequest.cpython-39-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan.c +2155 -0
- sf_veritas/_sffuncspan.cpython-39-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan_config.c +617 -0
- sf_veritas/_sffuncspan_config.cpython-39-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfheadercheck.c +341 -0
- sf_veritas/_sfheadercheck.cpython-39-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfnetworkhop.c +1451 -0
- sf_veritas/_sfnetworkhop.cpython-39-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfservice.c +1175 -0
- sf_veritas/_sfservice.cpython-39-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,1174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Function span profiler for collecting function call telemetry.
|
|
3
|
+
|
|
4
|
+
This module provides a high-performance profiler that captures function calls,
|
|
5
|
+
arguments, return values, and execution timing using a C extension.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import functools
|
|
9
|
+
import inspect
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Any, Callable, Dict, List, Optional, Set
|
|
13
|
+
|
|
14
|
+
from . import app_config
|
|
15
|
+
from .env_vars import SF_DEBUG
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from . import _sffuncspan
|
|
19
|
+
|
|
20
|
+
_HAS_NATIVE = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
_HAS_NATIVE = False
|
|
23
|
+
|
|
24
|
+
from .thread_local import get_or_set_sf_trace_id
|
|
25
|
+
|
|
26
|
+
# Marker attribute for skipping function tracing
|
|
27
|
+
_SKIP_FUNCTION_TRACING_ATTR = "_sf_skip_function_tracing"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def skip_function_tracing(func: Callable) -> Callable:
|
|
31
|
+
"""
|
|
32
|
+
Decorator to skip function span tracing for a specific function.
|
|
33
|
+
|
|
34
|
+
When using automatic profiling (sys.setprofile), this decorator marks
|
|
35
|
+
a function to be completely skipped by the function span profiler.
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
@skip_function_tracing
|
|
39
|
+
def internal_helper():
|
|
40
|
+
# This function won't have function spans traced
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
Note: This has no effect when using manual @profile_function decoration.
|
|
44
|
+
"""
|
|
45
|
+
setattr(func, _SKIP_FUNCTION_TRACING_ATTR, True)
|
|
46
|
+
return func
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Backward compatibility alias
|
|
50
|
+
skip_tracing = skip_function_tracing
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def skip_network_tracing(func: Callable) -> Callable:
|
|
54
|
+
"""
|
|
55
|
+
Decorator to skip network request/response tracing for a specific endpoint.
|
|
56
|
+
|
|
57
|
+
This decorator wraps the function with the suppress_network_recording context,
|
|
58
|
+
preventing all outbound HTTP/HTTPS requests made during the function execution
|
|
59
|
+
from being captured and sent to the Sailfish backend.
|
|
60
|
+
|
|
61
|
+
The actual network requests still go out normally - they're just not observed
|
|
62
|
+
by the telemetry system.
|
|
63
|
+
|
|
64
|
+
Usage:
|
|
65
|
+
@skip_network_tracing
|
|
66
|
+
@app.get("/healthz")
|
|
67
|
+
def healthz():
|
|
68
|
+
# Network requests in here won't be traced
|
|
69
|
+
return {"ok": True}
|
|
70
|
+
|
|
71
|
+
# Or for FastAPI with path parameters:
|
|
72
|
+
@app.get("/admin/stats")
|
|
73
|
+
@skip_network_tracing
|
|
74
|
+
async def admin_stats():
|
|
75
|
+
# Admin endpoint - don't trace network calls
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
Note: For async functions, this works correctly with async/await.
|
|
79
|
+
"""
|
|
80
|
+
from .thread_local import suppress_network_recording
|
|
81
|
+
|
|
82
|
+
if inspect.iscoroutinefunction(func):
|
|
83
|
+
# Async function
|
|
84
|
+
@functools.wraps(func)
|
|
85
|
+
async def async_wrapper(*args, **kwargs):
|
|
86
|
+
with suppress_network_recording():
|
|
87
|
+
return await func(*args, **kwargs)
|
|
88
|
+
|
|
89
|
+
# Mark function so web frameworks can skip registration
|
|
90
|
+
async_wrapper._sf_skip_tracing = True
|
|
91
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
92
|
+
print(
|
|
93
|
+
"[[skip_network_tracing]] skipping tracing for async endpoint",
|
|
94
|
+
log=False,
|
|
95
|
+
)
|
|
96
|
+
return async_wrapper
|
|
97
|
+
else:
|
|
98
|
+
# Sync function
|
|
99
|
+
@functools.wraps(func)
|
|
100
|
+
def sync_wrapper(*args, **kwargs):
|
|
101
|
+
with suppress_network_recording():
|
|
102
|
+
return func(*args, **kwargs)
|
|
103
|
+
|
|
104
|
+
# Mark function so web frameworks can skip registration
|
|
105
|
+
sync_wrapper._sf_skip_tracing = True
|
|
106
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
107
|
+
print("[[skip_network_tracing]] skipping tracing for endpoint", log=False)
|
|
108
|
+
return sync_wrapper
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def profile_function(
|
|
112
|
+
func: Optional[Callable] = None,
|
|
113
|
+
*,
|
|
114
|
+
capture_args: bool = False,
|
|
115
|
+
capture_return: bool = True,
|
|
116
|
+
):
|
|
117
|
+
"""
|
|
118
|
+
Decorator for manual function profiling with ultra-low overhead (~0.001-0.01%).
|
|
119
|
+
|
|
120
|
+
This provides an alternative to automatic profiling via sys.setprofile,
|
|
121
|
+
offering much lower overhead at the cost of requiring explicit decoration.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
func: Function to decorate (when used without arguments)
|
|
125
|
+
capture_args: Capture function arguments (adds ~10µs overhead)
|
|
126
|
+
capture_return: Capture return value (adds ~5µs overhead)
|
|
127
|
+
|
|
128
|
+
Usage:
|
|
129
|
+
# Simple usage (fastest)
|
|
130
|
+
@profile_function
|
|
131
|
+
def my_function():
|
|
132
|
+
...
|
|
133
|
+
|
|
134
|
+
# With argument capture
|
|
135
|
+
@profile_function(capture_args=True)
|
|
136
|
+
def my_function(x, y):
|
|
137
|
+
...
|
|
138
|
+
|
|
139
|
+
# Without return capture (faster)
|
|
140
|
+
@profile_function(capture_return=False)
|
|
141
|
+
def my_function():
|
|
142
|
+
...
|
|
143
|
+
|
|
144
|
+
Overhead:
|
|
145
|
+
- No capture: ~1-2µs per call
|
|
146
|
+
- With args: ~10-15µs per call
|
|
147
|
+
- With return: ~5-10µs per call
|
|
148
|
+
- With both: ~15-25µs per call
|
|
149
|
+
|
|
150
|
+
This is 2000-5000x faster than sys.setprofile-based profiling!
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def decorator(fn: Callable) -> Callable:
|
|
154
|
+
@functools.wraps(fn)
|
|
155
|
+
def wrapper(*args, **kwargs):
|
|
156
|
+
if not _HAS_NATIVE:
|
|
157
|
+
return fn(*args, **kwargs)
|
|
158
|
+
|
|
159
|
+
# Generate span ID (fast: ~50ns)
|
|
160
|
+
span_id = _sffuncspan.generate_span_id()
|
|
161
|
+
parent_span_id = _sffuncspan.peek_parent_span_id()
|
|
162
|
+
_sffuncspan.push_span(span_id)
|
|
163
|
+
|
|
164
|
+
# Capture arguments if requested
|
|
165
|
+
arguments_json = "{}"
|
|
166
|
+
if capture_args:
|
|
167
|
+
try:
|
|
168
|
+
# Build arguments dict
|
|
169
|
+
arg_names = fn.__code__.co_varnames[: fn.__code__.co_argcount]
|
|
170
|
+
args_dict = {}
|
|
171
|
+
for i, name in enumerate(arg_names):
|
|
172
|
+
if i < len(args):
|
|
173
|
+
args_dict[name] = str(args[i])[:100] # Limit size
|
|
174
|
+
for key, value in kwargs.items():
|
|
175
|
+
args_dict[key] = str(value)[:100]
|
|
176
|
+
arguments_json = json.dumps(args_dict)
|
|
177
|
+
except:
|
|
178
|
+
arguments_json = '{"_error": "failed to capture args"}'
|
|
179
|
+
|
|
180
|
+
# Record start time (fast: ~100ns)
|
|
181
|
+
start_ns = _sffuncspan.get_epoch_ns()
|
|
182
|
+
|
|
183
|
+
# Execute function
|
|
184
|
+
try:
|
|
185
|
+
result = fn(*args, **kwargs)
|
|
186
|
+
exception_occurred = False
|
|
187
|
+
except Exception as e:
|
|
188
|
+
exception_occurred = True
|
|
189
|
+
result = None
|
|
190
|
+
raise
|
|
191
|
+
finally:
|
|
192
|
+
# Record end time
|
|
193
|
+
end_ns = _sffuncspan.get_epoch_ns()
|
|
194
|
+
duration_ns = end_ns - start_ns
|
|
195
|
+
|
|
196
|
+
# Pop span
|
|
197
|
+
_sffuncspan.pop_span()
|
|
198
|
+
|
|
199
|
+
# Capture return value if requested and no exception
|
|
200
|
+
return_value_json = None
|
|
201
|
+
if capture_return and not exception_occurred and result is not None:
|
|
202
|
+
try:
|
|
203
|
+
return_value_json = json.dumps(str(result)[:100])
|
|
204
|
+
except:
|
|
205
|
+
return_value_json = None
|
|
206
|
+
|
|
207
|
+
# Record span (includes sampling check in C)
|
|
208
|
+
try:
|
|
209
|
+
_, session_id = get_or_set_sf_trace_id()
|
|
210
|
+
_sffuncspan.record_span(
|
|
211
|
+
session_id=str(session_id),
|
|
212
|
+
span_id=span_id,
|
|
213
|
+
parent_span_id=parent_span_id,
|
|
214
|
+
file_path=fn.__code__.co_filename,
|
|
215
|
+
line_number=fn.__code__.co_firstlineno,
|
|
216
|
+
column_number=0,
|
|
217
|
+
function_name=fn.__name__,
|
|
218
|
+
arguments_json=arguments_json,
|
|
219
|
+
return_value_json=return_value_json,
|
|
220
|
+
start_time_ns=start_ns,
|
|
221
|
+
duration_ns=duration_ns,
|
|
222
|
+
)
|
|
223
|
+
except:
|
|
224
|
+
pass # Fail silently to not impact application
|
|
225
|
+
|
|
226
|
+
return result
|
|
227
|
+
|
|
228
|
+
return wrapper
|
|
229
|
+
|
|
230
|
+
# Support both @profile_function and @profile_function()
|
|
231
|
+
if func is None:
|
|
232
|
+
return decorator
|
|
233
|
+
else:
|
|
234
|
+
return decorator(func)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def capture_function_spans(
|
|
238
|
+
func: Optional[Callable] = None,
|
|
239
|
+
*,
|
|
240
|
+
include_arguments: Optional[bool] = None,
|
|
241
|
+
include_return_value: Optional[bool] = None,
|
|
242
|
+
arg_limit_mb: Optional[int] = None,
|
|
243
|
+
return_limit_mb: Optional[int] = None,
|
|
244
|
+
autocapture_all_children: Optional[bool] = None,
|
|
245
|
+
sample_rate: Optional[float] = None,
|
|
246
|
+
):
|
|
247
|
+
"""
|
|
248
|
+
Decorator to override function span capture settings for a specific function.
|
|
249
|
+
|
|
250
|
+
This decorator has second-highest priority (only HTTP headers override it).
|
|
251
|
+
When applied, it registers the function's config with the C extension for
|
|
252
|
+
ultra-fast runtime lookups (<5ns).
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
func: Function to decorate (when used without arguments)
|
|
256
|
+
include_arguments: Capture function arguments (default: from env SF_FUNCSPAN_CAPTURE_ARGUMENTS)
|
|
257
|
+
include_return_value: Capture return value (default: from env SF_FUNCSPAN_CAPTURE_RETURN_VALUE)
|
|
258
|
+
arg_limit_mb: Max size for arguments in MB (default: from env SF_FUNCSPAN_ARG_LIMIT_MB)
|
|
259
|
+
return_limit_mb: Max size for return value in MB (default: from env SF_FUNCSPAN_RETURN_LIMIT_MB)
|
|
260
|
+
autocapture_all_children: Capture all child functions (default: from env SF_FUNCSPAN_AUTOCAPTURE_ALL_CHILD_FUNCTIONS)
|
|
261
|
+
sample_rate: Sampling rate 0.0-1.0 (0.0=disabled, 1.0=all) (default: 1.0)
|
|
262
|
+
|
|
263
|
+
Usage:
|
|
264
|
+
# Use defaults from environment
|
|
265
|
+
@capture_function_spans
|
|
266
|
+
def my_function():
|
|
267
|
+
...
|
|
268
|
+
|
|
269
|
+
# Override specific settings
|
|
270
|
+
@capture_function_spans(include_arguments=False, sample_rate=0.5)
|
|
271
|
+
def sensitive_function(api_key, password):
|
|
272
|
+
...
|
|
273
|
+
|
|
274
|
+
# Capture only this function, not children
|
|
275
|
+
@capture_function_spans(autocapture_all_children=False)
|
|
276
|
+
def top_level_handler():
|
|
277
|
+
...
|
|
278
|
+
|
|
279
|
+
Note:
|
|
280
|
+
- No runtime overhead (no wrapper!) - config registered at decoration time
|
|
281
|
+
- Works with automatic profiling (sys.setprofile) - decorator is not a wrapper
|
|
282
|
+
- Override via HTTP header X-Sf3-FunctionSpanCaptureOverride for per-request control
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
def decorator(fn: Callable) -> Callable:
|
|
286
|
+
try:
|
|
287
|
+
from . import _sffuncspan_config
|
|
288
|
+
except ImportError:
|
|
289
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
290
|
+
print(
|
|
291
|
+
"[[DEBUG]] capture_function_spans: Config extension not available, decorator has no effect",
|
|
292
|
+
log=False,
|
|
293
|
+
)
|
|
294
|
+
return fn # No-op if config extension not available
|
|
295
|
+
|
|
296
|
+
# Build config dict (None values mean "use default")
|
|
297
|
+
config = {}
|
|
298
|
+
|
|
299
|
+
if include_arguments is not None:
|
|
300
|
+
config["include_arguments"] = include_arguments
|
|
301
|
+
|
|
302
|
+
if include_return_value is not None:
|
|
303
|
+
config["include_return_value"] = include_return_value
|
|
304
|
+
|
|
305
|
+
if arg_limit_mb is not None:
|
|
306
|
+
config["arg_limit_mb"] = arg_limit_mb
|
|
307
|
+
|
|
308
|
+
if return_limit_mb is not None:
|
|
309
|
+
config["return_limit_mb"] = return_limit_mb
|
|
310
|
+
|
|
311
|
+
if autocapture_all_children is not None:
|
|
312
|
+
config["autocapture_all_children"] = autocapture_all_children
|
|
313
|
+
|
|
314
|
+
if sample_rate is not None:
|
|
315
|
+
config["sample_rate"] = sample_rate
|
|
316
|
+
|
|
317
|
+
# Register with C extension (only if we have config to set)
|
|
318
|
+
if config:
|
|
319
|
+
try:
|
|
320
|
+
file_path = fn.__code__.co_filename
|
|
321
|
+
func_name = fn.__name__
|
|
322
|
+
_sffuncspan_config.add_function(file_path, func_name, config)
|
|
323
|
+
|
|
324
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
325
|
+
print(
|
|
326
|
+
f"[[DEBUG]] capture_function_spans: Registered {file_path}:{func_name} with config {config}",
|
|
327
|
+
log=False,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Pre-populate the profiler cache to avoid Python callbacks during profiling
|
|
331
|
+
try:
|
|
332
|
+
from . import _sffuncspan
|
|
333
|
+
|
|
334
|
+
# Get config values with defaults
|
|
335
|
+
from .env_vars import (
|
|
336
|
+
SF_FUNCSPAN_ARG_LIMIT_MB,
|
|
337
|
+
SF_FUNCSPAN_AUTOCAPTURE_ALL_CHILD_FUNCTIONS,
|
|
338
|
+
SF_FUNCSPAN_CAPTURE_ARGUMENTS,
|
|
339
|
+
SF_FUNCSPAN_CAPTURE_RETURN_VALUE,
|
|
340
|
+
SF_FUNCSPAN_RETURN_LIMIT_MB,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
inc_args = int(
|
|
344
|
+
config.get("include_arguments", SF_FUNCSPAN_CAPTURE_ARGUMENTS)
|
|
345
|
+
)
|
|
346
|
+
inc_ret = int(
|
|
347
|
+
config.get(
|
|
348
|
+
"include_return_value", SF_FUNCSPAN_CAPTURE_RETURN_VALUE
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
autocap = int(
|
|
352
|
+
config.get(
|
|
353
|
+
"autocapture_all_children",
|
|
354
|
+
SF_FUNCSPAN_AUTOCAPTURE_ALL_CHILD_FUNCTIONS,
|
|
355
|
+
)
|
|
356
|
+
)
|
|
357
|
+
arg_lim = int(config.get("arg_limit_mb", SF_FUNCSPAN_ARG_LIMIT_MB))
|
|
358
|
+
ret_lim = int(
|
|
359
|
+
config.get("return_limit_mb", SF_FUNCSPAN_RETURN_LIMIT_MB)
|
|
360
|
+
)
|
|
361
|
+
samp_rate = float(config.get("sample_rate", 1.0))
|
|
362
|
+
|
|
363
|
+
_sffuncspan.cache_config(
|
|
364
|
+
file_path,
|
|
365
|
+
func_name,
|
|
366
|
+
inc_args,
|
|
367
|
+
inc_ret,
|
|
368
|
+
autocap,
|
|
369
|
+
arg_lim,
|
|
370
|
+
ret_lim,
|
|
371
|
+
samp_rate,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
375
|
+
print(
|
|
376
|
+
f"[[DEBUG]] capture_function_spans: Cached config for {file_path}:{func_name}",
|
|
377
|
+
log=False,
|
|
378
|
+
)
|
|
379
|
+
except Exception as e:
|
|
380
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
381
|
+
print(
|
|
382
|
+
f"[[DEBUG]] capture_function_spans: Failed to cache config: {e}",
|
|
383
|
+
log=False,
|
|
384
|
+
)
|
|
385
|
+
except Exception as e:
|
|
386
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
387
|
+
print(
|
|
388
|
+
f"[[DEBUG]] capture_function_spans: Failed to register function config: {e}",
|
|
389
|
+
log=False,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
return fn # Return original function (no wrapper!)
|
|
393
|
+
|
|
394
|
+
# Support both @capture_function_spans and @capture_function_spans()
|
|
395
|
+
if func is None:
|
|
396
|
+
return decorator
|
|
397
|
+
else:
|
|
398
|
+
return decorator(func)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class FunctionSpanProfiler:
|
|
402
|
+
"""
|
|
403
|
+
High-performance function span profiler using sys.setprofile.
|
|
404
|
+
|
|
405
|
+
Captures:
|
|
406
|
+
- Function call location (file, line, column)
|
|
407
|
+
- Function arguments (names and values)
|
|
408
|
+
- Return values
|
|
409
|
+
- Execution timing (start time and duration in nanoseconds)
|
|
410
|
+
- Hierarchical span relationships (parent_span_id)
|
|
411
|
+
"""
|
|
412
|
+
|
|
413
|
+
def __init__(
|
|
414
|
+
self,
|
|
415
|
+
url: str,
|
|
416
|
+
query: str,
|
|
417
|
+
api_key: str,
|
|
418
|
+
service_uuid: str,
|
|
419
|
+
library: str = "sf_veritas",
|
|
420
|
+
version: str = "1.0.0",
|
|
421
|
+
http2: bool = True,
|
|
422
|
+
variable_capture_size_limit_mb: int = 1,
|
|
423
|
+
capture_from_installed_libraries: Optional[List[str]] = None,
|
|
424
|
+
sample_rate: float = 1.0,
|
|
425
|
+
enable_sampling: bool = False,
|
|
426
|
+
include_django_view_functions: bool = False,
|
|
427
|
+
):
|
|
428
|
+
"""
|
|
429
|
+
Initialize the function span profiler.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
url: GraphQL endpoint URL
|
|
433
|
+
query: GraphQL mutation query for function spans
|
|
434
|
+
api_key: API key for authentication
|
|
435
|
+
service_uuid: Service UUID
|
|
436
|
+
library: Library name (default: "sf_veritas")
|
|
437
|
+
version: Library version (default: "1.0.0")
|
|
438
|
+
http2: Use HTTP/2 (default: True)
|
|
439
|
+
variable_capture_size_limit_mb: Max size to capture per variable (default: 1MB)
|
|
440
|
+
capture_from_installed_libraries: List of library prefixes to capture from
|
|
441
|
+
sample_rate: Sampling probability 0.0-1.0 (default: 1.0 = capture all, 0.1 = 10%)
|
|
442
|
+
enable_sampling: Enable sampling (default: False)
|
|
443
|
+
"""
|
|
444
|
+
if not _HAS_NATIVE:
|
|
445
|
+
raise RuntimeError("Native _sffuncspan extension not available")
|
|
446
|
+
|
|
447
|
+
self._initialized = False
|
|
448
|
+
self._active = False
|
|
449
|
+
self._previous_profiler = None # Store previous profiler for chaining
|
|
450
|
+
self._capture_from_installed_libraries: Set[str] = set(
|
|
451
|
+
capture_from_installed_libraries or []
|
|
452
|
+
)
|
|
453
|
+
self._variable_capture_size_limit_mb = variable_capture_size_limit_mb
|
|
454
|
+
|
|
455
|
+
# Track active function calls with their start times and span IDs
|
|
456
|
+
self._active_calls: Dict[int, Dict[str, Any]] = {}
|
|
457
|
+
|
|
458
|
+
# Initialize the C extension
|
|
459
|
+
success = _sffuncspan.init(
|
|
460
|
+
url=url,
|
|
461
|
+
query=query,
|
|
462
|
+
api_key=api_key,
|
|
463
|
+
service_uuid=service_uuid,
|
|
464
|
+
library=library,
|
|
465
|
+
version=version,
|
|
466
|
+
http2=1 if http2 else 0,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
if not success:
|
|
470
|
+
raise RuntimeError("Failed to initialize _sffuncspan")
|
|
471
|
+
|
|
472
|
+
# Get configuration from environment variables
|
|
473
|
+
from .env_vars import (
|
|
474
|
+
SF_FUNCSPAN_ARG_LIMIT_MB,
|
|
475
|
+
SF_FUNCSPAN_AUTOCAPTURE_ALL_CHILD_FUNCTIONS,
|
|
476
|
+
SF_FUNCSPAN_CAPTURE_ARGUMENTS,
|
|
477
|
+
SF_FUNCSPAN_CAPTURE_INSTALLED_PACKAGES,
|
|
478
|
+
SF_FUNCSPAN_CAPTURE_RETURN_VALUE,
|
|
479
|
+
SF_FUNCSPAN_CAPTURE_SF_VERITAS,
|
|
480
|
+
SF_FUNCSPAN_PARSE_JSON_STRINGS,
|
|
481
|
+
SF_FUNCSPAN_RETURN_LIMIT_MB,
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
# Store DEFAULT capture flags (used as fallback if config lookup fails)
|
|
485
|
+
self._default_capture_arguments = SF_FUNCSPAN_CAPTURE_ARGUMENTS
|
|
486
|
+
self._default_capture_return_value = SF_FUNCSPAN_CAPTURE_RETURN_VALUE
|
|
487
|
+
self._default_arg_limit_mb = SF_FUNCSPAN_ARG_LIMIT_MB
|
|
488
|
+
self._default_return_limit_mb = SF_FUNCSPAN_RETURN_LIMIT_MB
|
|
489
|
+
self._default_autocapture_all_children = (
|
|
490
|
+
SF_FUNCSPAN_AUTOCAPTURE_ALL_CHILD_FUNCTIONS
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
# Try to import config system
|
|
494
|
+
try:
|
|
495
|
+
from . import _sffuncspan_config
|
|
496
|
+
|
|
497
|
+
self._config_system = _sffuncspan_config
|
|
498
|
+
except ImportError:
|
|
499
|
+
self._config_system = None
|
|
500
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
501
|
+
print(
|
|
502
|
+
"[[DEBUG]] FunctionSpanProfiler: Config system not available, using defaults",
|
|
503
|
+
log=False,
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
# Track call depth for top-level-only mode
|
|
507
|
+
self._call_depth = 0
|
|
508
|
+
|
|
509
|
+
# Configure the profiler settings
|
|
510
|
+
print("[FUNCSPAN_INIT] About to call _sffuncspan.configure()", flush=True)
|
|
511
|
+
_sffuncspan.configure(
|
|
512
|
+
variable_capture_size_limit_mb=variable_capture_size_limit_mb,
|
|
513
|
+
capture_from_installed_libraries=list(
|
|
514
|
+
self._capture_from_installed_libraries
|
|
515
|
+
),
|
|
516
|
+
sample_rate=sample_rate,
|
|
517
|
+
enable_sampling=enable_sampling,
|
|
518
|
+
parse_json_strings=SF_FUNCSPAN_PARSE_JSON_STRINGS,
|
|
519
|
+
capture_arguments=SF_FUNCSPAN_CAPTURE_ARGUMENTS,
|
|
520
|
+
capture_return_value=SF_FUNCSPAN_CAPTURE_RETURN_VALUE,
|
|
521
|
+
arg_limit_mb=SF_FUNCSPAN_ARG_LIMIT_MB,
|
|
522
|
+
return_limit_mb=SF_FUNCSPAN_RETURN_LIMIT_MB,
|
|
523
|
+
include_django_view_functions=include_django_view_functions,
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
print(f"[FUNCSPAN_INIT] About to call set_capture_installed_packages({SF_FUNCSPAN_CAPTURE_INSTALLED_PACKAGES})", flush=True)
|
|
527
|
+
# Set capture_installed_packages flag in C extension
|
|
528
|
+
_sffuncspan.set_capture_installed_packages(SF_FUNCSPAN_CAPTURE_INSTALLED_PACKAGES)
|
|
529
|
+
print("[FUNCSPAN_INIT] Successfully called set_capture_installed_packages", flush=True)
|
|
530
|
+
|
|
531
|
+
print(f"[FUNCSPAN_INIT] About to call set_capture_sf_veritas({SF_FUNCSPAN_CAPTURE_SF_VERITAS})", flush=True)
|
|
532
|
+
# Set capture_sf_veritas flag in C extension
|
|
533
|
+
_sffuncspan.set_capture_sf_veritas(SF_FUNCSPAN_CAPTURE_SF_VERITAS)
|
|
534
|
+
print("[FUNCSPAN_INIT] Successfully called set_capture_sf_veritas", flush=True)
|
|
535
|
+
|
|
536
|
+
self._initialized = True
|
|
537
|
+
|
|
538
|
+
def _get_config_for_function(self, file_path: str, func_name: str) -> Dict:
|
|
539
|
+
"""
|
|
540
|
+
Get configuration for a specific function.
|
|
541
|
+
|
|
542
|
+
Looks up config from the C extension's config system, which includes:
|
|
543
|
+
- HTTP header overrides (highest priority)
|
|
544
|
+
- Decorator configs
|
|
545
|
+
- Function-level configs from .sailfish files
|
|
546
|
+
- File pragmas
|
|
547
|
+
- File-level configs
|
|
548
|
+
- Directory configs
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
file_path: Path to the file containing the function
|
|
552
|
+
func_name: Name of the function
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
Dict with config keys: include_arguments, include_return_value,
|
|
556
|
+
arg_limit_mb, return_limit_mb, autocapture_all_children, sample_rate
|
|
557
|
+
"""
|
|
558
|
+
if self._config_system:
|
|
559
|
+
try:
|
|
560
|
+
return self._config_system.get(file_path, func_name)
|
|
561
|
+
except Exception as e:
|
|
562
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
563
|
+
print(
|
|
564
|
+
f"[[DEBUG]] Failed to get config for {file_path}::{func_name}: {e}",
|
|
565
|
+
log=False,
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
# Fallback to defaults if config system not available or lookup fails
|
|
569
|
+
return {
|
|
570
|
+
"include_arguments": self._default_capture_arguments,
|
|
571
|
+
"include_return_value": self._default_capture_return_value,
|
|
572
|
+
"arg_limit_mb": self._default_arg_limit_mb,
|
|
573
|
+
"return_limit_mb": self._default_return_limit_mb,
|
|
574
|
+
"autocapture_all_children": self._default_autocapture_all_children,
|
|
575
|
+
"sample_rate": 1.0,
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
579
|
+
"""Get performance statistics."""
|
|
580
|
+
if not self._initialized:
|
|
581
|
+
return {}
|
|
582
|
+
return _sffuncspan.get_stats()
|
|
583
|
+
|
|
584
|
+
def reset_stats(self):
|
|
585
|
+
"""Reset performance statistics."""
|
|
586
|
+
if self._initialized:
|
|
587
|
+
_sffuncspan.reset_stats()
|
|
588
|
+
|
|
589
|
+
def start(self):
|
|
590
|
+
"""Start profiling function calls using ultra-fast C profiler."""
|
|
591
|
+
print("[FUNCSPAN_START] Entering start() method", flush=True)
|
|
592
|
+
if not self._initialized:
|
|
593
|
+
raise RuntimeError("Profiler not initialized")
|
|
594
|
+
|
|
595
|
+
if self._active:
|
|
596
|
+
return # Already active
|
|
597
|
+
|
|
598
|
+
print("[FUNCSPAN_START] About to import threading", flush=True)
|
|
599
|
+
|
|
600
|
+
# Use ultra-fast C profiler instead of Python sys.setprofile!
|
|
601
|
+
# This is 100-1000x faster because:
|
|
602
|
+
# 1. No Python callback overhead
|
|
603
|
+
# 2. No frame attribute lookups in Python
|
|
604
|
+
# 3. Direct C string operations
|
|
605
|
+
# 4. Pre-built JSON in C
|
|
606
|
+
# 5. Lock-free ring buffer
|
|
607
|
+
try:
|
|
608
|
+
import threading
|
|
609
|
+
|
|
610
|
+
print("[FUNCSPAN_START] About to call _sffuncspan.start_c_profiler()", flush=True)
|
|
611
|
+
# Start C profiler for current thread
|
|
612
|
+
_sffuncspan.start_c_profiler()
|
|
613
|
+
|
|
614
|
+
print("[FUNCSPAN_START] C profiler started, setting up threading.setprofile", flush=True)
|
|
615
|
+
# For new threads, we need to set profiler via threading.setprofile
|
|
616
|
+
# This is a lightweight wrapper that just calls the C profiler
|
|
617
|
+
def _thread_profiler_wrapper():
|
|
618
|
+
"""Lightweight wrapper to enable C profiler on new threads."""
|
|
619
|
+
_sffuncspan.start_c_profiler()
|
|
620
|
+
|
|
621
|
+
# Set for all future threads (FastAPI workers, etc.)
|
|
622
|
+
threading.setprofile(
|
|
623
|
+
lambda *args: _sffuncspan.start_c_profiler() if args else None
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
print("[FUNCSPAN_START] Setting _active=True", flush=True)
|
|
627
|
+
self._active = True
|
|
628
|
+
print("[FUNCSPAN_START] Successfully started profiler!", flush=True)
|
|
629
|
+
except Exception as e:
|
|
630
|
+
print(f"[[FUNCSPAN_ERROR]] Failed to start C profiler: {e}", log=False)
|
|
631
|
+
raise
|
|
632
|
+
|
|
633
|
+
def stop(self):
|
|
634
|
+
"""Stop profiling function calls."""
|
|
635
|
+
if not self._active:
|
|
636
|
+
return
|
|
637
|
+
|
|
638
|
+
if SF_DEBUG and app_config._interceptors_initialized:
|
|
639
|
+
print("[[FUNCSPAN_DEBUG]] Stopping C profiler", log=False)
|
|
640
|
+
|
|
641
|
+
import threading
|
|
642
|
+
|
|
643
|
+
_sffuncspan.stop_c_profiler()
|
|
644
|
+
threading.setprofile(None)
|
|
645
|
+
self._active = False
|
|
646
|
+
|
|
647
|
+
def shutdown(self):
|
|
648
|
+
"""Shutdown the profiler and cleanup resources."""
|
|
649
|
+
self.stop()
|
|
650
|
+
if self._initialized:
|
|
651
|
+
_sffuncspan.shutdown()
|
|
652
|
+
self._initialized = False
|
|
653
|
+
|
|
654
|
+
def _should_capture_frame(self, frame) -> bool:
|
|
655
|
+
"""
|
|
656
|
+
Determine if we should capture this frame.
|
|
657
|
+
|
|
658
|
+
Args:
|
|
659
|
+
frame: Python frame object
|
|
660
|
+
|
|
661
|
+
Returns:
|
|
662
|
+
True if we should capture, False otherwise
|
|
663
|
+
"""
|
|
664
|
+
# Get the function name and file path WHERE THE FUNCTION IS DEFINED
|
|
665
|
+
# This is the key - frame.f_code.co_filename tells us where the function's
|
|
666
|
+
# code actually lives, not where it's being called from
|
|
667
|
+
func_name = frame.f_code.co_name
|
|
668
|
+
filename = frame.f_code.co_filename
|
|
669
|
+
|
|
670
|
+
# Exclude private/dunder methods (__enter__, __exit__, __init__, etc.)
|
|
671
|
+
if func_name.startswith("__") and func_name.endswith("__"):
|
|
672
|
+
return False
|
|
673
|
+
|
|
674
|
+
# ALWAYS exclude these paths (Python internals, site-packages, etc.)
|
|
675
|
+
# This is THE critical check - if the function is DEFINED in site-packages,
|
|
676
|
+
# we don't capture it, regardless of where it's called from
|
|
677
|
+
exclude_patterns = [
|
|
678
|
+
"site-packages", # Functions defined in site-packages
|
|
679
|
+
"dist-packages", # Functions defined in dist-packages
|
|
680
|
+
"/lib/python", # Python stdlib
|
|
681
|
+
"\\lib\\python", # Python stdlib (Windows)
|
|
682
|
+
"<frozen", # Frozen modules
|
|
683
|
+
"<string>", # exec() and eval() generated code
|
|
684
|
+
"importlib", # Import machinery
|
|
685
|
+
"_bootstrap", # Bootstrap code
|
|
686
|
+
"sf_veritas", # Don't capture our own telemetry code!
|
|
687
|
+
]
|
|
688
|
+
|
|
689
|
+
for pattern in exclude_patterns:
|
|
690
|
+
if pattern in filename:
|
|
691
|
+
return False
|
|
692
|
+
|
|
693
|
+
# If library filter is set, only capture those libraries
|
|
694
|
+
if self._capture_from_installed_libraries:
|
|
695
|
+
# Check if it matches any of the capture prefixes
|
|
696
|
+
for lib_prefix in self._capture_from_installed_libraries:
|
|
697
|
+
if lib_prefix in filename:
|
|
698
|
+
return True
|
|
699
|
+
return False
|
|
700
|
+
|
|
701
|
+
# No filter set - capture everything except excluded patterns
|
|
702
|
+
return True
|
|
703
|
+
|
|
704
|
+
def _serialize_value(self, value: Any, max_size: int) -> str:
|
|
705
|
+
"""
|
|
706
|
+
Serialize a value to JSON, respecting size limits.
|
|
707
|
+
|
|
708
|
+
Uses ultra-fast C implementation when available (<1µs), falls back to Python.
|
|
709
|
+
|
|
710
|
+
Args:
|
|
711
|
+
value: Value to serialize
|
|
712
|
+
max_size: Maximum size in bytes
|
|
713
|
+
|
|
714
|
+
Returns:
|
|
715
|
+
JSON string representation
|
|
716
|
+
"""
|
|
717
|
+
# Use ultra-fast C serialization when available
|
|
718
|
+
if _HAS_NATIVE:
|
|
719
|
+
try:
|
|
720
|
+
return _sffuncspan.serialize_value(value, max_size)
|
|
721
|
+
except Exception:
|
|
722
|
+
pass # Fall back to Python implementation
|
|
723
|
+
|
|
724
|
+
# Python fallback implementation (slower but always works)
|
|
725
|
+
try:
|
|
726
|
+
# First try: direct JSON serialization for primitives and built-ins
|
|
727
|
+
serialized = json.dumps(value)
|
|
728
|
+
if len(serialized) > max_size:
|
|
729
|
+
return json.dumps({"_truncated": True, "_size": len(serialized)})
|
|
730
|
+
return serialized
|
|
731
|
+
except (TypeError, ValueError):
|
|
732
|
+
pass # Try introspection
|
|
733
|
+
|
|
734
|
+
# Second try: Introspect the object to extract meaningful data
|
|
735
|
+
type_name = type(value).__name__
|
|
736
|
+
module_name = type(value).__module__
|
|
737
|
+
|
|
738
|
+
try:
|
|
739
|
+
result = {
|
|
740
|
+
"_type": (
|
|
741
|
+
f"{module_name}.{type_name}"
|
|
742
|
+
if module_name != "builtins"
|
|
743
|
+
else type_name
|
|
744
|
+
)
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
# Try __dict__ first (works for most custom objects)
|
|
748
|
+
if hasattr(value, "__dict__"):
|
|
749
|
+
try:
|
|
750
|
+
obj_dict = {}
|
|
751
|
+
for key, val in value.__dict__.items():
|
|
752
|
+
# Skip private/dunder attributes and callables (methods, functions)
|
|
753
|
+
if not key.startswith("_") and not callable(val):
|
|
754
|
+
try:
|
|
755
|
+
# Recursively serialize nested values (with size limit)
|
|
756
|
+
obj_dict[key] = json.loads(
|
|
757
|
+
self._serialize_value(val, max_size // 10)
|
|
758
|
+
)
|
|
759
|
+
except:
|
|
760
|
+
obj_dict[key] = str(val)[:100]
|
|
761
|
+
if obj_dict:
|
|
762
|
+
result["attributes"] = obj_dict
|
|
763
|
+
except:
|
|
764
|
+
pass
|
|
765
|
+
|
|
766
|
+
# Try __slots__ if available
|
|
767
|
+
if hasattr(value, "__slots__"):
|
|
768
|
+
try:
|
|
769
|
+
slots_dict = {}
|
|
770
|
+
for slot in value.__slots__:
|
|
771
|
+
if hasattr(value, slot) and not slot.startswith("_"):
|
|
772
|
+
try:
|
|
773
|
+
slot_val = getattr(value, slot)
|
|
774
|
+
slots_dict[slot] = json.loads(
|
|
775
|
+
self._serialize_value(slot_val, max_size // 10)
|
|
776
|
+
)
|
|
777
|
+
except:
|
|
778
|
+
slots_dict[slot] = str(getattr(value, slot, None))[:100]
|
|
779
|
+
if slots_dict:
|
|
780
|
+
result["slots"] = slots_dict
|
|
781
|
+
except:
|
|
782
|
+
pass
|
|
783
|
+
|
|
784
|
+
# Try to get useful properties/methods that might reveal data
|
|
785
|
+
# Look for common patterns like .data, .value, .content, .body, .result
|
|
786
|
+
for attr_name in [
|
|
787
|
+
"data",
|
|
788
|
+
"value",
|
|
789
|
+
"content",
|
|
790
|
+
"body",
|
|
791
|
+
"result",
|
|
792
|
+
"message",
|
|
793
|
+
"text",
|
|
794
|
+
]:
|
|
795
|
+
if hasattr(value, attr_name):
|
|
796
|
+
try:
|
|
797
|
+
attr_val = getattr(value, attr_name)
|
|
798
|
+
if not callable(attr_val):
|
|
799
|
+
result[attr_name] = json.loads(
|
|
800
|
+
self._serialize_value(attr_val, max_size // 10)
|
|
801
|
+
)
|
|
802
|
+
except:
|
|
803
|
+
pass
|
|
804
|
+
|
|
805
|
+
# Add a safe repr as fallback
|
|
806
|
+
try:
|
|
807
|
+
result["_repr"] = str(value)[:200]
|
|
808
|
+
except:
|
|
809
|
+
result["_repr"] = f"<{type_name} object>"
|
|
810
|
+
|
|
811
|
+
serialized = json.dumps(result)
|
|
812
|
+
if len(serialized) > max_size:
|
|
813
|
+
return json.dumps(
|
|
814
|
+
{
|
|
815
|
+
"_truncated": True,
|
|
816
|
+
"_size": len(serialized),
|
|
817
|
+
"_type": result["_type"],
|
|
818
|
+
}
|
|
819
|
+
)
|
|
820
|
+
return serialized
|
|
821
|
+
|
|
822
|
+
except Exception as e:
|
|
823
|
+
# Ultimate fallback
|
|
824
|
+
try:
|
|
825
|
+
return json.dumps(
|
|
826
|
+
{
|
|
827
|
+
"_error": "serialization failed",
|
|
828
|
+
"_type": (
|
|
829
|
+
f"{module_name}.{type_name}"
|
|
830
|
+
if module_name != "builtins"
|
|
831
|
+
else type_name
|
|
832
|
+
),
|
|
833
|
+
"_repr": str(value)[:100] if str(value) else "<no repr>",
|
|
834
|
+
}
|
|
835
|
+
)
|
|
836
|
+
except:
|
|
837
|
+
return json.dumps({"_error": "complete serialization failure"})
|
|
838
|
+
|
|
839
|
+
def _capture_arguments(self, frame, arg_limit_mb: Optional[int] = None) -> str:
|
|
840
|
+
"""
|
|
841
|
+
Capture function arguments as JSON.
|
|
842
|
+
|
|
843
|
+
Args:
|
|
844
|
+
frame: Python frame object
|
|
845
|
+
arg_limit_mb: Max size for arguments in MB (default: from config)
|
|
846
|
+
|
|
847
|
+
Returns:
|
|
848
|
+
JSON string with argument names and values
|
|
849
|
+
"""
|
|
850
|
+
code = frame.f_code
|
|
851
|
+
arg_count = code.co_argcount + code.co_kwonlyargcount
|
|
852
|
+
|
|
853
|
+
# Handle methods (skip 'self' or 'cls' if present)
|
|
854
|
+
var_names = code.co_varnames[:arg_count]
|
|
855
|
+
|
|
856
|
+
arguments = {}
|
|
857
|
+
# Use arg-specific limit instead of general variable limit
|
|
858
|
+
if arg_limit_mb is None:
|
|
859
|
+
arg_limit_mb = self._default_arg_limit_mb
|
|
860
|
+
max_size = arg_limit_mb * 1048576
|
|
861
|
+
|
|
862
|
+
for var_name in var_names:
|
|
863
|
+
if var_name in frame.f_locals:
|
|
864
|
+
value = frame.f_locals[var_name]
|
|
865
|
+
arguments[var_name] = self._serialize_value(
|
|
866
|
+
value, max_size // len(var_names) if var_names else max_size
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
return json.dumps(arguments)
|
|
870
|
+
|
|
871
|
+
def _profile_callback(self, frame, event: str, arg):
|
|
872
|
+
"""
|
|
873
|
+
Profile callback function called by sys.setprofile.
|
|
874
|
+
|
|
875
|
+
Args:
|
|
876
|
+
frame: Current frame
|
|
877
|
+
event: Event type ('call', 'return', 'exception', etc.)
|
|
878
|
+
arg: Event-specific argument
|
|
879
|
+
"""
|
|
880
|
+
# DEBUG: Check if we're even being called
|
|
881
|
+
code = frame.f_code
|
|
882
|
+
func_name = code.co_name
|
|
883
|
+
if SF_DEBUG and func_name == "simple_calculation":
|
|
884
|
+
print(
|
|
885
|
+
f"[[FUNCSPAN_DEBUG]] *** OUR CALLBACK WAS CALLED FOR {func_name}! event={event}",
|
|
886
|
+
log=False,
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
# Chain to previous profiler first (if any)
|
|
890
|
+
if self._previous_profiler is not None:
|
|
891
|
+
try:
|
|
892
|
+
self._previous_profiler(frame, event, arg)
|
|
893
|
+
except Exception:
|
|
894
|
+
pass # Ignore errors in chained profiler
|
|
895
|
+
|
|
896
|
+
# Fast path: Check if function has @skip_tracing decorator
|
|
897
|
+
filename = code.co_filename
|
|
898
|
+
|
|
899
|
+
# DEBUG: Log when we see simple_calculation
|
|
900
|
+
if SF_DEBUG and func_name == "simple_calculation" and event == "call":
|
|
901
|
+
print(
|
|
902
|
+
f"[[FUNCSPAN_DEBUG]] Callback triggered: {func_name} in {filename}, event={event}",
|
|
903
|
+
log=False,
|
|
904
|
+
)
|
|
905
|
+
print(
|
|
906
|
+
f"[[FUNCSPAN_DEBUG]] Current profiler is: {sys.getprofile()}", log=False
|
|
907
|
+
)
|
|
908
|
+
print(f"[[FUNCSPAN_DEBUG]] We are: {self._profile_callback}", log=False)
|
|
909
|
+
|
|
910
|
+
# Check if this function should be skipped
|
|
911
|
+
# Look for the function in globals to check for skip attribute
|
|
912
|
+
if "self" in frame.f_locals:
|
|
913
|
+
# Method call
|
|
914
|
+
obj = frame.f_locals["self"]
|
|
915
|
+
if hasattr(obj, func_name):
|
|
916
|
+
func = getattr(obj, func_name)
|
|
917
|
+
if hasattr(func, _SKIP_FUNCTION_TRACING_ATTR):
|
|
918
|
+
return
|
|
919
|
+
elif func_name in frame.f_globals:
|
|
920
|
+
func = frame.f_globals[func_name]
|
|
921
|
+
if callable(func) and hasattr(func, _SKIP_FUNCTION_TRACING_ATTR):
|
|
922
|
+
return
|
|
923
|
+
|
|
924
|
+
# Fast path: Check if we should even process this frame
|
|
925
|
+
should_capture = self._should_capture_frame(frame)
|
|
926
|
+
|
|
927
|
+
# DEBUG: Log capture decision for simple_calculation
|
|
928
|
+
if SF_DEBUG and func_name == "simple_calculation" and event == "call":
|
|
929
|
+
print(f"[[FUNCSPAN_DEBUG]] Should capture? {should_capture}", log=False)
|
|
930
|
+
|
|
931
|
+
if not should_capture:
|
|
932
|
+
return
|
|
933
|
+
|
|
934
|
+
if event == "call":
|
|
935
|
+
self._handle_call(frame)
|
|
936
|
+
elif event == "return":
|
|
937
|
+
self._handle_return(frame, arg)
|
|
938
|
+
elif event == "exception":
|
|
939
|
+
# We can choose to handle exceptions differently if needed
|
|
940
|
+
pass
|
|
941
|
+
|
|
942
|
+
def _handle_call(self, frame):
|
|
943
|
+
"""
|
|
944
|
+
Handle a function call event.
|
|
945
|
+
|
|
946
|
+
Args:
|
|
947
|
+
frame: Python frame object
|
|
948
|
+
"""
|
|
949
|
+
if not self._should_capture_frame(frame):
|
|
950
|
+
return
|
|
951
|
+
|
|
952
|
+
# Get frame info early for config lookup
|
|
953
|
+
code = frame.f_code
|
|
954
|
+
file_path = code.co_filename
|
|
955
|
+
function_name = code.co_name
|
|
956
|
+
|
|
957
|
+
# Look up config for this function to check autocapture setting
|
|
958
|
+
config = self._get_config_for_function(file_path, function_name)
|
|
959
|
+
|
|
960
|
+
# Check if we should skip child functions
|
|
961
|
+
should_skip_child = False
|
|
962
|
+
if (
|
|
963
|
+
not config.get(
|
|
964
|
+
"autocapture_all_children", self._default_autocapture_all_children
|
|
965
|
+
)
|
|
966
|
+
and self._call_depth > 0
|
|
967
|
+
):
|
|
968
|
+
# We're inside a captured function and child capture is disabled
|
|
969
|
+
should_skip_child = True
|
|
970
|
+
|
|
971
|
+
# Increment call depth (even for skipped calls, so we track nesting)
|
|
972
|
+
self._call_depth += 1
|
|
973
|
+
|
|
974
|
+
# If we're skipping this child function, mark it but don't capture
|
|
975
|
+
if should_skip_child:
|
|
976
|
+
frame_id = id(frame)
|
|
977
|
+
self._active_calls[frame_id] = {
|
|
978
|
+
"captured": False
|
|
979
|
+
} # Not captured, just tracking
|
|
980
|
+
return
|
|
981
|
+
|
|
982
|
+
# Generate span ID
|
|
983
|
+
span_id = _sffuncspan.generate_span_id()
|
|
984
|
+
|
|
985
|
+
# Get parent span ID from the stack
|
|
986
|
+
parent_span_id = _sffuncspan.peek_parent_span_id()
|
|
987
|
+
|
|
988
|
+
# Push current span onto stack
|
|
989
|
+
_sffuncspan.push_span(span_id)
|
|
990
|
+
|
|
991
|
+
# Record start time
|
|
992
|
+
start_time_ns = _sffuncspan.get_epoch_ns()
|
|
993
|
+
|
|
994
|
+
# Get remaining frame info (file_path, function_name already extracted above)
|
|
995
|
+
line_number = frame.f_lineno
|
|
996
|
+
column_number = 0 # Python doesn't provide column info easily
|
|
997
|
+
|
|
998
|
+
# Config was already looked up above for autocapture check, reuse it
|
|
999
|
+
|
|
1000
|
+
# Capture arguments (or skip if disabled)
|
|
1001
|
+
if config["include_arguments"]:
|
|
1002
|
+
arg_limit_mb = config.get("arg_limit_mb", self._default_arg_limit_mb)
|
|
1003
|
+
arguments_json = self._capture_arguments(frame, arg_limit_mb=arg_limit_mb)
|
|
1004
|
+
else:
|
|
1005
|
+
arguments_json = "{}" # Empty object if arguments capture is disabled
|
|
1006
|
+
|
|
1007
|
+
# Store call info for when it returns (including the config!)
|
|
1008
|
+
frame_id = id(frame)
|
|
1009
|
+
self._active_calls[frame_id] = {
|
|
1010
|
+
"span_id": span_id,
|
|
1011
|
+
"parent_span_id": parent_span_id,
|
|
1012
|
+
"file_path": file_path,
|
|
1013
|
+
"line_number": line_number,
|
|
1014
|
+
"column_number": column_number,
|
|
1015
|
+
"function_name": function_name,
|
|
1016
|
+
"arguments_json": arguments_json,
|
|
1017
|
+
"start_time_ns": start_time_ns,
|
|
1018
|
+
"captured": True, # Mark that we actually captured this call
|
|
1019
|
+
"config": config, # Store config for use in _handle_return
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
def _handle_return(self, frame, return_value):
|
|
1023
|
+
"""
|
|
1024
|
+
Handle a function return event.
|
|
1025
|
+
|
|
1026
|
+
Args:
|
|
1027
|
+
frame: Python frame object
|
|
1028
|
+
return_value: The value being returned
|
|
1029
|
+
"""
|
|
1030
|
+
frame_id = id(frame)
|
|
1031
|
+
|
|
1032
|
+
# Check if we have a record of this call
|
|
1033
|
+
if frame_id not in self._active_calls:
|
|
1034
|
+
return
|
|
1035
|
+
|
|
1036
|
+
call_info = self._active_calls.pop(frame_id)
|
|
1037
|
+
|
|
1038
|
+
# Decrement call depth
|
|
1039
|
+
if self._call_depth > 0:
|
|
1040
|
+
self._call_depth -= 1
|
|
1041
|
+
|
|
1042
|
+
# If this was a skipped child function, we're done
|
|
1043
|
+
if not call_info.get("captured", False):
|
|
1044
|
+
return
|
|
1045
|
+
|
|
1046
|
+
# Pop span from stack
|
|
1047
|
+
_sffuncspan.pop_span()
|
|
1048
|
+
|
|
1049
|
+
# Calculate duration
|
|
1050
|
+
end_time_ns = _sffuncspan.get_epoch_ns()
|
|
1051
|
+
duration_ns = end_time_ns - call_info["start_time_ns"]
|
|
1052
|
+
|
|
1053
|
+
# Get config for this function (from stored call info)
|
|
1054
|
+
config = call_info.get("config", {})
|
|
1055
|
+
if not config:
|
|
1056
|
+
# Fallback if config wasn't stored (shouldn't happen)
|
|
1057
|
+
config = self._get_config_for_function(
|
|
1058
|
+
call_info["file_path"], call_info["function_name"]
|
|
1059
|
+
)
|
|
1060
|
+
|
|
1061
|
+
# Serialize return value (or skip if disabled by config)
|
|
1062
|
+
if config.get("include_return_value", self._default_capture_return_value):
|
|
1063
|
+
max_size = (
|
|
1064
|
+
config.get("return_limit_mb", self._default_return_limit_mb) * 1048576
|
|
1065
|
+
)
|
|
1066
|
+
return_value_json = self._serialize_value(return_value, max_size)
|
|
1067
|
+
else:
|
|
1068
|
+
return_value_json = None # No return value if disabled
|
|
1069
|
+
|
|
1070
|
+
# Get session ID (trace ID)
|
|
1071
|
+
_, session_id = get_or_set_sf_trace_id()
|
|
1072
|
+
|
|
1073
|
+
# Record the span
|
|
1074
|
+
_sffuncspan.record_span(
|
|
1075
|
+
session_id=str(session_id),
|
|
1076
|
+
span_id=call_info["span_id"],
|
|
1077
|
+
parent_span_id=call_info["parent_span_id"],
|
|
1078
|
+
file_path=call_info["file_path"],
|
|
1079
|
+
line_number=call_info["line_number"],
|
|
1080
|
+
column_number=call_info["column_number"],
|
|
1081
|
+
function_name=call_info["function_name"],
|
|
1082
|
+
arguments_json=call_info["arguments_json"],
|
|
1083
|
+
return_value_json=return_value_json,
|
|
1084
|
+
start_time_ns=call_info["start_time_ns"],
|
|
1085
|
+
duration_ns=duration_ns,
|
|
1086
|
+
)
|
|
1087
|
+
|
|
1088
|
+
def __enter__(self):
|
|
1089
|
+
"""Context manager entry."""
|
|
1090
|
+
self.start()
|
|
1091
|
+
return self
|
|
1092
|
+
|
|
1093
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
1094
|
+
"""Context manager exit."""
|
|
1095
|
+
self.stop()
|
|
1096
|
+
return False
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
# Global profiler instance
|
|
1100
|
+
_global_profiler: Optional[FunctionSpanProfiler] = None
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
def init_function_span_profiler(
|
|
1104
|
+
url: str,
|
|
1105
|
+
query: str,
|
|
1106
|
+
api_key: str,
|
|
1107
|
+
service_uuid: str,
|
|
1108
|
+
library: str = "sf_veritas",
|
|
1109
|
+
version: str = "1.0.0",
|
|
1110
|
+
http2: bool = True,
|
|
1111
|
+
variable_capture_size_limit_mb: int = 1,
|
|
1112
|
+
capture_from_installed_libraries: Optional[List[str]] = None,
|
|
1113
|
+
sample_rate: float = 1.0,
|
|
1114
|
+
enable_sampling: bool = False,
|
|
1115
|
+
include_django_view_functions: bool = False,
|
|
1116
|
+
auto_start: bool = True,
|
|
1117
|
+
) -> FunctionSpanProfiler:
|
|
1118
|
+
"""
|
|
1119
|
+
Initialize the global function span profiler.
|
|
1120
|
+
|
|
1121
|
+
Args:
|
|
1122
|
+
url: GraphQL endpoint URL
|
|
1123
|
+
query: GraphQL mutation query for function spans
|
|
1124
|
+
api_key: API key for authentication
|
|
1125
|
+
service_uuid: Service UUID
|
|
1126
|
+
library: Library name (default: "sf_veritas")
|
|
1127
|
+
version: Library version (default: "1.0.0")
|
|
1128
|
+
http2: Use HTTP/2 (default: True)
|
|
1129
|
+
variable_capture_size_limit_mb: Max size to capture per variable (default: 1MB)
|
|
1130
|
+
capture_from_installed_libraries: List of library prefixes to capture from
|
|
1131
|
+
sample_rate: Sampling probability 0.0-1.0 (default: 1.0 = capture all, 0.1 = 10%)
|
|
1132
|
+
enable_sampling: Enable sampling (default: False)
|
|
1133
|
+
auto_start: Automatically start profiling (default: True)
|
|
1134
|
+
|
|
1135
|
+
Returns:
|
|
1136
|
+
FunctionSpanProfiler instance
|
|
1137
|
+
"""
|
|
1138
|
+
global _global_profiler
|
|
1139
|
+
|
|
1140
|
+
if _global_profiler is not None:
|
|
1141
|
+
_global_profiler.shutdown()
|
|
1142
|
+
|
|
1143
|
+
_global_profiler = FunctionSpanProfiler(
|
|
1144
|
+
url=url,
|
|
1145
|
+
query=query,
|
|
1146
|
+
api_key=api_key,
|
|
1147
|
+
service_uuid=service_uuid,
|
|
1148
|
+
library=library,
|
|
1149
|
+
version=version,
|
|
1150
|
+
http2=http2,
|
|
1151
|
+
variable_capture_size_limit_mb=variable_capture_size_limit_mb,
|
|
1152
|
+
capture_from_installed_libraries=capture_from_installed_libraries,
|
|
1153
|
+
sample_rate=sample_rate,
|
|
1154
|
+
enable_sampling=enable_sampling,
|
|
1155
|
+
include_django_view_functions=include_django_view_functions,
|
|
1156
|
+
)
|
|
1157
|
+
|
|
1158
|
+
if auto_start:
|
|
1159
|
+
_global_profiler.start()
|
|
1160
|
+
|
|
1161
|
+
return _global_profiler
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
def get_function_span_profiler() -> Optional[FunctionSpanProfiler]:
|
|
1165
|
+
"""Get the global function span profiler instance."""
|
|
1166
|
+
return _global_profiler
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
def shutdown_function_span_profiler():
|
|
1170
|
+
"""Shutdown the global function span profiler."""
|
|
1171
|
+
global _global_profiler
|
|
1172
|
+
if _global_profiler is not None:
|
|
1173
|
+
_global_profiler.shutdown()
|
|
1174
|
+
_global_profiler = None
|