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,693 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Function Span Config Loader
|
|
3
|
+
|
|
4
|
+
Scans directory tree for .sailfish files and inline pragmas.
|
|
5
|
+
Builds C hash tables for ultra-fast runtime lookups (<5ns).
|
|
6
|
+
|
|
7
|
+
Configuration hierarchy (highest to lowest priority):
|
|
8
|
+
1. HTTP Header X-Sf3-FunctionSpanCaptureOverride
|
|
9
|
+
2. Decorator @capture_function_spans()
|
|
10
|
+
3. Function config in .sailfish (functions: section)
|
|
11
|
+
4. File pragma # sailfish-funcspan:
|
|
12
|
+
5. File config in .sailfish (files: section)
|
|
13
|
+
6. Directory .sailfish (cascades down)
|
|
14
|
+
7. Parent directory .sailfish (inherited)
|
|
15
|
+
8. Environment variables SF_FUNCSPAN_*
|
|
16
|
+
9. Hard-coded defaults
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
from glob import glob
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Dict, List, Optional, Set
|
|
25
|
+
|
|
26
|
+
from .env_vars import (
|
|
27
|
+
SF_FUNCSPAN_ARG_LIMIT_MB,
|
|
28
|
+
SF_FUNCSPAN_AUTOCAPTURE_ALL_CHILD_FUNCTIONS,
|
|
29
|
+
SF_FUNCSPAN_CAPTURE_ARGUMENTS,
|
|
30
|
+
SF_FUNCSPAN_CAPTURE_RETURN_VALUE,
|
|
31
|
+
SF_FUNCSPAN_RETURN_LIMIT_MB,
|
|
32
|
+
SF_DEBUG,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class FunctionSpanConfigLoader:
|
|
37
|
+
"""
|
|
38
|
+
Scans directory tree for .sailfish files at startup.
|
|
39
|
+
Parses YAML/TOML/JSON and inline pragmas.
|
|
40
|
+
Builds C hash tables for ultra-fast runtime lookups.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, root_paths: List[str]):
|
|
44
|
+
try:
|
|
45
|
+
from . import _sffuncspan_config
|
|
46
|
+
self._c_config = _sffuncspan_config
|
|
47
|
+
except ImportError as e:
|
|
48
|
+
if SF_DEBUG:
|
|
49
|
+
print(
|
|
50
|
+
f"[[DEBUG]] Failed to import _sffuncspan_config: {e}",
|
|
51
|
+
log=False,
|
|
52
|
+
)
|
|
53
|
+
self._c_config = None
|
|
54
|
+
|
|
55
|
+
# Also import _sffuncspan for cache pre-population
|
|
56
|
+
try:
|
|
57
|
+
from . import _sffuncspan
|
|
58
|
+
self._c_profiler = _sffuncspan
|
|
59
|
+
except ImportError as e:
|
|
60
|
+
if SF_DEBUG:
|
|
61
|
+
print(
|
|
62
|
+
f"[[DEBUG]] Failed to import _sffuncspan: {e}",
|
|
63
|
+
log=False,
|
|
64
|
+
)
|
|
65
|
+
self._c_profiler = None
|
|
66
|
+
|
|
67
|
+
self.root_paths = root_paths
|
|
68
|
+
self.configs: Dict[str, Dict] = {} # path -> config
|
|
69
|
+
self.resolved_configs: Dict[str, Dict] = {} # resolved with inheritance
|
|
70
|
+
|
|
71
|
+
def load_all_configs(self):
|
|
72
|
+
"""Scan and load all .sailfish files + pragmas"""
|
|
73
|
+
if not self._c_config:
|
|
74
|
+
if SF_DEBUG:
|
|
75
|
+
print(
|
|
76
|
+
"[[DEBUG]] Config loader: C extension not available, skipping",
|
|
77
|
+
log=False,
|
|
78
|
+
)
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
# Debug: Show what root paths we're scanning
|
|
82
|
+
print(
|
|
83
|
+
f"[FuncSpanDebug] Scanning for .sailfish files in root paths:",
|
|
84
|
+
log=False,
|
|
85
|
+
)
|
|
86
|
+
for root in self.root_paths:
|
|
87
|
+
print(f"[FuncSpanDebug] - {root} (exists={os.path.exists(root)})", log=False)
|
|
88
|
+
|
|
89
|
+
# 1. Walk directory trees for .sailfish files
|
|
90
|
+
sailfish_files_found = []
|
|
91
|
+
for root in self.root_paths:
|
|
92
|
+
if not os.path.exists(root):
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
for dirpath, _, filenames in os.walk(root):
|
|
96
|
+
if '.sailfish' in filenames:
|
|
97
|
+
config_path = os.path.join(dirpath, '.sailfish')
|
|
98
|
+
sailfish_files_found.append(config_path)
|
|
99
|
+
self._load_config_file(config_path, dirpath)
|
|
100
|
+
|
|
101
|
+
print(
|
|
102
|
+
f"[FuncSpanDebug] Found {len(sailfish_files_found)} .sailfish files:",
|
|
103
|
+
log=False,
|
|
104
|
+
)
|
|
105
|
+
for sf_file in sailfish_files_found:
|
|
106
|
+
print(f"[FuncSpanDebug] - {sf_file}", log=False)
|
|
107
|
+
|
|
108
|
+
# 2. Scan all Python files for pragmas (only first 50 lines, ONE per file!)
|
|
109
|
+
for root in self.root_paths:
|
|
110
|
+
if not os.path.exists(root):
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
for py_file in Path(root).rglob('*.py'):
|
|
114
|
+
self._scan_pragma(str(py_file))
|
|
115
|
+
|
|
116
|
+
# 3. Resolve inheritance and build C tables
|
|
117
|
+
self._resolve_inheritance()
|
|
118
|
+
self._build_c_tables()
|
|
119
|
+
|
|
120
|
+
print(
|
|
121
|
+
f"[FuncSpanDebug] Config loader: Loaded {len(self.resolved_configs)} resolved configs",
|
|
122
|
+
log=False,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if SF_DEBUG:
|
|
126
|
+
print(
|
|
127
|
+
f"[[DEBUG]] Config loader: Loaded {len(self.resolved_configs)} configs",
|
|
128
|
+
log=False,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def _load_config_file(self, path: str, dirpath: str):
|
|
132
|
+
"""Load .sailfish file (auto-detect YAML/TOML/JSON format)"""
|
|
133
|
+
try:
|
|
134
|
+
with open(path, 'r') as f:
|
|
135
|
+
content = f.read()
|
|
136
|
+
except Exception as e:
|
|
137
|
+
if SF_DEBUG:
|
|
138
|
+
print(
|
|
139
|
+
f"[[DEBUG]] Config loader: Failed to read {path}: {e}",
|
|
140
|
+
log=False,
|
|
141
|
+
)
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
# Try to parse in order: YAML, TOML, JSON
|
|
145
|
+
config = None
|
|
146
|
+
|
|
147
|
+
# Try YAML first (most common)
|
|
148
|
+
try:
|
|
149
|
+
import yaml
|
|
150
|
+
config = yaml.safe_load(content)
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
# Try TOML if YAML failed
|
|
155
|
+
if config is None:
|
|
156
|
+
try:
|
|
157
|
+
try:
|
|
158
|
+
import tomllib # Python 3.11+
|
|
159
|
+
except ImportError:
|
|
160
|
+
import tomli as tomllib # fallback for older Python
|
|
161
|
+
config = tomllib.loads(content)
|
|
162
|
+
except Exception:
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
# Try JSON if both YAML and TOML failed
|
|
166
|
+
if config is None:
|
|
167
|
+
try:
|
|
168
|
+
config = json.loads(content)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
if SF_DEBUG:
|
|
171
|
+
print(
|
|
172
|
+
f"[[DEBUG]] Config loader: Failed to parse {path} as YAML/TOML/JSON: {e}",
|
|
173
|
+
log=False,
|
|
174
|
+
)
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
# Extract funcspan section (optional wrapper)
|
|
178
|
+
# Support both wrapped (funcspan: {...}) and unwrapped formats
|
|
179
|
+
funcspan_config = config.get('funcspan', config)
|
|
180
|
+
|
|
181
|
+
# Check if this looks like a funcspan config (has expected keys)
|
|
182
|
+
expected_keys = {'default', 'files', 'functions'}
|
|
183
|
+
if not any(key in funcspan_config for key in expected_keys):
|
|
184
|
+
if SF_DEBUG:
|
|
185
|
+
print(
|
|
186
|
+
f"[[DEBUG]] Config loader: No funcspan config in {path}",
|
|
187
|
+
log=False,
|
|
188
|
+
)
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
# Store with directory context
|
|
192
|
+
self.configs[f"DIR:{dirpath}"] = funcspan_config
|
|
193
|
+
|
|
194
|
+
# Debug: Show what patterns/keys are in this config
|
|
195
|
+
print(
|
|
196
|
+
f"[FuncSpanDebug] Loaded config from {path}:",
|
|
197
|
+
log=False,
|
|
198
|
+
)
|
|
199
|
+
print(f"[FuncSpanDebug] Directory: {dirpath}", log=False)
|
|
200
|
+
print(f"[FuncSpanDebug] Keys in config: {list(funcspan_config.keys())}", log=False)
|
|
201
|
+
if 'default' in funcspan_config:
|
|
202
|
+
print(f"[FuncSpanDebug] Has 'default' config: {funcspan_config['default']}", log=False)
|
|
203
|
+
# Show all non-default, non-functions keys (these are file patterns)
|
|
204
|
+
file_patterns = [k for k in funcspan_config.keys() if k not in ('default', 'functions', 'files')]
|
|
205
|
+
if file_patterns:
|
|
206
|
+
print(f"[FuncSpanDebug] File patterns found: {file_patterns}", log=False)
|
|
207
|
+
for pattern in file_patterns:
|
|
208
|
+
print(f"[FuncSpanDebug] Pattern '{pattern}': {funcspan_config[pattern]}", log=False)
|
|
209
|
+
|
|
210
|
+
if SF_DEBUG:
|
|
211
|
+
print(
|
|
212
|
+
f"[[DEBUG]] Config loader: Loaded config from {path}",
|
|
213
|
+
log=False,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def _scan_pragma(self, file_path: str):
|
|
217
|
+
"""Scan file for inline pragma (first 50 lines, ONE per file!)"""
|
|
218
|
+
try:
|
|
219
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
220
|
+
for i, line in enumerate(f):
|
|
221
|
+
if i > 50: # Only check first 50 lines
|
|
222
|
+
break
|
|
223
|
+
|
|
224
|
+
# Match: # sailfish-funcspan: include_arguments=false, sample_rate=0.1
|
|
225
|
+
if match := re.match(r'#\s*sailfish-funcspan:\s*(.+)', line):
|
|
226
|
+
config = self._parse_pragma(match.group(1))
|
|
227
|
+
if config:
|
|
228
|
+
# Store file-level config
|
|
229
|
+
self.configs[f"FILE:{file_path}"] = config
|
|
230
|
+
if SF_DEBUG:
|
|
231
|
+
print(
|
|
232
|
+
f"[[DEBUG]] Config loader: Found pragma in {file_path}: {config}",
|
|
233
|
+
log=False,
|
|
234
|
+
)
|
|
235
|
+
return # Only ONE pragma per file!
|
|
236
|
+
except Exception as e:
|
|
237
|
+
if SF_DEBUG:
|
|
238
|
+
print(
|
|
239
|
+
f"[[DEBUG]] Config loader: Error scanning {file_path}: {e}",
|
|
240
|
+
log=False,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def _parse_pragma(self, pragma_str: str) -> Optional[Dict]:
|
|
244
|
+
"""Parse pragma: include_arguments=false, sample_rate=0.1"""
|
|
245
|
+
config = {}
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
for part in pragma_str.split(','):
|
|
249
|
+
if '=' not in part:
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
key, value = part.strip().split('=', 1)
|
|
253
|
+
key = key.strip()
|
|
254
|
+
value = value.strip()
|
|
255
|
+
|
|
256
|
+
# Type conversion
|
|
257
|
+
if value.lower() in ('true', 'false'):
|
|
258
|
+
config[key] = value.lower() == 'true'
|
|
259
|
+
elif '.' in value:
|
|
260
|
+
config[key] = float(value)
|
|
261
|
+
else:
|
|
262
|
+
config[key] = int(value)
|
|
263
|
+
|
|
264
|
+
return config if config else None
|
|
265
|
+
except Exception as e:
|
|
266
|
+
if SF_DEBUG:
|
|
267
|
+
print(
|
|
268
|
+
f"[[DEBUG]] Config loader: Failed to parse pragma '{pragma_str}': {e}",
|
|
269
|
+
log=False,
|
|
270
|
+
)
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
def _resolve_inheritance(self):
|
|
274
|
+
"""Resolve directory hierarchy and glob patterns"""
|
|
275
|
+
resolved = {}
|
|
276
|
+
|
|
277
|
+
# Determine default config based on whether user has .sailfish files (opt-in)
|
|
278
|
+
if len(self.configs) == 0:
|
|
279
|
+
# No .sailfish files = no opt-in = capture NOTHING by default
|
|
280
|
+
# Headers, decorators, and pragmas can still enable capture
|
|
281
|
+
default_config = {
|
|
282
|
+
'include_arguments': False,
|
|
283
|
+
'include_return_value': False,
|
|
284
|
+
'autocapture_all_children': False,
|
|
285
|
+
'arg_limit_mb': 1,
|
|
286
|
+
'return_limit_mb': 1,
|
|
287
|
+
'sample_rate': 1.0,
|
|
288
|
+
}
|
|
289
|
+
if SF_DEBUG:
|
|
290
|
+
print(
|
|
291
|
+
"[[DEBUG]] Config loader: No .sailfish files found, default capture disabled",
|
|
292
|
+
log=False,
|
|
293
|
+
)
|
|
294
|
+
else:
|
|
295
|
+
# Has .sailfish files = user opted in = use env var defaults
|
|
296
|
+
default_config = {
|
|
297
|
+
'include_arguments': SF_FUNCSPAN_CAPTURE_ARGUMENTS,
|
|
298
|
+
'include_return_value': SF_FUNCSPAN_CAPTURE_RETURN_VALUE,
|
|
299
|
+
'autocapture_all_children': SF_FUNCSPAN_AUTOCAPTURE_ALL_CHILD_FUNCTIONS,
|
|
300
|
+
'arg_limit_mb': SF_FUNCSPAN_ARG_LIMIT_MB,
|
|
301
|
+
'return_limit_mb': SF_FUNCSPAN_RETURN_LIMIT_MB,
|
|
302
|
+
'sample_rate': 1.0,
|
|
303
|
+
}
|
|
304
|
+
if SF_DEBUG:
|
|
305
|
+
print(
|
|
306
|
+
f"[[DEBUG]] Config loader: Found {len(self.configs)} .sailfish configs, using env var defaults",
|
|
307
|
+
log=False,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Process directory configs (cascade down)
|
|
311
|
+
dir_configs = sorted([k for k in self.configs.keys() if k.startswith('DIR:')])
|
|
312
|
+
|
|
313
|
+
for key in dir_configs:
|
|
314
|
+
dirpath = key[4:] # Remove 'DIR:' prefix
|
|
315
|
+
parent_config = self._get_parent_config(dirpath, resolved)
|
|
316
|
+
|
|
317
|
+
# Get the base directory config (default + inherited)
|
|
318
|
+
dir_base_config = {**default_config, **parent_config}
|
|
319
|
+
|
|
320
|
+
# Apply the 'default' section if present
|
|
321
|
+
config_data = self.configs[key]
|
|
322
|
+
if 'default' in config_data:
|
|
323
|
+
dir_base_config = {**dir_base_config, **config_data['default']}
|
|
324
|
+
|
|
325
|
+
resolved[key] = dir_base_config
|
|
326
|
+
|
|
327
|
+
# Expand glob patterns in files: section
|
|
328
|
+
if 'files' in config_data:
|
|
329
|
+
self._expand_file_globs(dirpath, config_data['files'], resolved, dir_base_config)
|
|
330
|
+
|
|
331
|
+
# IMPORTANT: Also process TOP-LEVEL patterns (not under files:)
|
|
332
|
+
# These are patterns like "*.py", "app.py", "**/*.py" at the root of the config
|
|
333
|
+
top_level_patterns = {
|
|
334
|
+
k: v for k, v in config_data.items()
|
|
335
|
+
if k not in ('default', 'files', 'functions')
|
|
336
|
+
}
|
|
337
|
+
if top_level_patterns:
|
|
338
|
+
print(
|
|
339
|
+
f"[FuncSpanDebug] Processing top-level patterns for {dirpath}: {list(top_level_patterns.keys())}",
|
|
340
|
+
log=False,
|
|
341
|
+
)
|
|
342
|
+
self._expand_file_globs(dirpath, top_level_patterns, resolved, dir_base_config)
|
|
343
|
+
|
|
344
|
+
# Add file-level configs (pragmas)
|
|
345
|
+
for key in self.configs.keys():
|
|
346
|
+
if key.startswith('FILE:'):
|
|
347
|
+
# Get directory config as base
|
|
348
|
+
file_path = key[5:] # Remove 'FILE:' prefix
|
|
349
|
+
dir_config = self._get_directory_config_for_file(file_path, resolved)
|
|
350
|
+
resolved[key] = {**default_config, **dir_config, **self.configs[key]}
|
|
351
|
+
|
|
352
|
+
self.resolved_configs = resolved
|
|
353
|
+
|
|
354
|
+
# Debug: Show summary of resolved configs
|
|
355
|
+
file_configs = [k for k in resolved.keys() if k.startswith('FILE:')]
|
|
356
|
+
print(
|
|
357
|
+
f"[FuncSpanDebug] Resolved {len(file_configs)} file-specific configs",
|
|
358
|
+
log=False,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Check if app.py got a config
|
|
362
|
+
app_py_configs = [k for k in file_configs if 'app.py' in k]
|
|
363
|
+
if app_py_configs:
|
|
364
|
+
print(
|
|
365
|
+
f"[FuncSpanDebug] Found {len(app_py_configs)} config(s) for app.py files:",
|
|
366
|
+
log=False,
|
|
367
|
+
)
|
|
368
|
+
for cfg_key in app_py_configs:
|
|
369
|
+
print(f"[FuncSpanDebug] - {cfg_key[5:]}", log=False) # Remove FILE: prefix
|
|
370
|
+
print(f"[FuncSpanDebug] Config: {resolved[cfg_key]}", log=False)
|
|
371
|
+
else:
|
|
372
|
+
print(
|
|
373
|
+
"[FuncSpanDebug] WARNING: No config found for any app.py files!",
|
|
374
|
+
log=False,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
def _get_parent_config(self, dirpath: str, resolved: Dict) -> Dict:
|
|
378
|
+
"""Get parent directory's config for inheritance"""
|
|
379
|
+
parent = os.path.dirname(dirpath)
|
|
380
|
+
|
|
381
|
+
# Keep walking up until we find a config or reach root
|
|
382
|
+
while parent and parent != dirpath:
|
|
383
|
+
parent_key = f"DIR:{parent}"
|
|
384
|
+
if parent_key in resolved:
|
|
385
|
+
return resolved[parent_key]
|
|
386
|
+
dirpath = parent
|
|
387
|
+
parent = os.path.dirname(parent)
|
|
388
|
+
|
|
389
|
+
return {}
|
|
390
|
+
|
|
391
|
+
def _get_directory_config_for_file(self, file_path: str, resolved: Dict) -> Dict:
|
|
392
|
+
"""Get the directory config that applies to this file"""
|
|
393
|
+
dirpath = os.path.dirname(file_path)
|
|
394
|
+
|
|
395
|
+
# Walk up directory tree until we find a config
|
|
396
|
+
while dirpath:
|
|
397
|
+
dir_key = f"DIR:{dirpath}"
|
|
398
|
+
if dir_key in resolved:
|
|
399
|
+
return resolved[dir_key]
|
|
400
|
+
|
|
401
|
+
parent = os.path.dirname(dirpath)
|
|
402
|
+
if parent == dirpath: # Reached root
|
|
403
|
+
break
|
|
404
|
+
dirpath = parent
|
|
405
|
+
|
|
406
|
+
return {}
|
|
407
|
+
|
|
408
|
+
def _expand_file_globs(self, dirpath: str, file_patterns: Dict, resolved: Dict, base_config: Dict):
|
|
409
|
+
"""Expand glob patterns to actual file paths"""
|
|
410
|
+
for pattern, config in file_patterns.items():
|
|
411
|
+
full_pattern = os.path.join(dirpath, pattern)
|
|
412
|
+
|
|
413
|
+
print(
|
|
414
|
+
f"[FuncSpanDebug] Expanding glob pattern: '{pattern}' in {dirpath}",
|
|
415
|
+
log=False,
|
|
416
|
+
)
|
|
417
|
+
print(f"[FuncSpanDebug] Full pattern: {full_pattern}", log=False)
|
|
418
|
+
|
|
419
|
+
try:
|
|
420
|
+
matched_files = glob(full_pattern, recursive=True)
|
|
421
|
+
print(
|
|
422
|
+
f"[FuncSpanDebug] Matched {len(matched_files)} files",
|
|
423
|
+
log=False,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
for matched_file in matched_files:
|
|
427
|
+
# Normalize path
|
|
428
|
+
matched_file = os.path.normpath(matched_file)
|
|
429
|
+
resolved[f"FILE:{matched_file}"] = {**base_config, **config}
|
|
430
|
+
|
|
431
|
+
# Show first few matches and last match
|
|
432
|
+
if len(matched_files) <= 5 or matched_files.index(matched_file) < 3 or matched_files.index(matched_file) >= len(matched_files) - 1:
|
|
433
|
+
print(f"[FuncSpanDebug] - {matched_file}", log=False)
|
|
434
|
+
elif matched_files.index(matched_file) == 3:
|
|
435
|
+
print(f"[FuncSpanDebug] ... ({len(matched_files) - 4} more files) ...", log=False)
|
|
436
|
+
|
|
437
|
+
if SF_DEBUG:
|
|
438
|
+
print(
|
|
439
|
+
f"[[DEBUG]] Config loader: Matched file {matched_file} with pattern {pattern}",
|
|
440
|
+
log=False,
|
|
441
|
+
)
|
|
442
|
+
except Exception as e:
|
|
443
|
+
print(
|
|
444
|
+
f"[FuncSpanDebug] ERROR expanding glob: {e}",
|
|
445
|
+
log=False,
|
|
446
|
+
)
|
|
447
|
+
if SF_DEBUG:
|
|
448
|
+
print(
|
|
449
|
+
f"[[DEBUG]] Config loader: Failed to expand glob {full_pattern}: {e}",
|
|
450
|
+
log=False,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
def _build_c_tables(self):
|
|
454
|
+
"""Build C hash tables for ultra-fast lookups"""
|
|
455
|
+
if not self._c_config:
|
|
456
|
+
return
|
|
457
|
+
|
|
458
|
+
# 1. Initialize C config system with defaults
|
|
459
|
+
# Use same logic as _resolve_inheritance: no .sailfish = no default capture
|
|
460
|
+
if len(self.configs) == 0:
|
|
461
|
+
default_config = {
|
|
462
|
+
'include_arguments': False,
|
|
463
|
+
'include_return_value': False,
|
|
464
|
+
'autocapture_all_children': False,
|
|
465
|
+
'arg_limit_mb': 1,
|
|
466
|
+
'return_limit_mb': 1,
|
|
467
|
+
'sample_rate': 1.0,
|
|
468
|
+
}
|
|
469
|
+
else:
|
|
470
|
+
default_config = {
|
|
471
|
+
'include_arguments': SF_FUNCSPAN_CAPTURE_ARGUMENTS,
|
|
472
|
+
'include_return_value': SF_FUNCSPAN_CAPTURE_RETURN_VALUE,
|
|
473
|
+
'arg_limit_mb': SF_FUNCSPAN_ARG_LIMIT_MB,
|
|
474
|
+
'return_limit_mb': SF_FUNCSPAN_RETURN_LIMIT_MB,
|
|
475
|
+
'autocapture_all_children': SF_FUNCSPAN_AUTOCAPTURE_ALL_CHILD_FUNCTIONS,
|
|
476
|
+
'sample_rate': 1.0,
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
try:
|
|
480
|
+
self._c_config.init(default_config)
|
|
481
|
+
except Exception as e:
|
|
482
|
+
if SF_DEBUG:
|
|
483
|
+
print(
|
|
484
|
+
f"[[DEBUG]] Config loader: Failed to initialize C config: {e}",
|
|
485
|
+
log=False,
|
|
486
|
+
)
|
|
487
|
+
return
|
|
488
|
+
|
|
489
|
+
# 2. Add all file configs
|
|
490
|
+
file_count = 0
|
|
491
|
+
print(
|
|
492
|
+
f"[FuncSpanDebug] Building C config tables from {len(self.resolved_configs)} resolved configs",
|
|
493
|
+
log=False,
|
|
494
|
+
)
|
|
495
|
+
for key, config in self.resolved_configs.items():
|
|
496
|
+
if key.startswith('FILE:'):
|
|
497
|
+
file_path = key[5:] # Remove 'FILE:' prefix
|
|
498
|
+
print(
|
|
499
|
+
f"[FuncSpanDebug] Adding file config to C: {file_path}",
|
|
500
|
+
log=False,
|
|
501
|
+
)
|
|
502
|
+
print(
|
|
503
|
+
f"[FuncSpanDebug] Config: {config}",
|
|
504
|
+
log=False,
|
|
505
|
+
)
|
|
506
|
+
try:
|
|
507
|
+
# Determine priority based on source
|
|
508
|
+
# Priority 4 = File pragma (# sailfish-funcspan:)
|
|
509
|
+
# Priority 5 = File config from .sailfish glob patterns
|
|
510
|
+
# Check if this config came from a pragma (has FILE: prefix and was scanned from Python file)
|
|
511
|
+
is_pragma = key in self.configs # Pragmas are stored directly with FILE: prefix
|
|
512
|
+
priority = 4 if is_pragma else 5 # 4=pragma (higher), 5=sailfish file (lower)
|
|
513
|
+
|
|
514
|
+
self._c_config.add_file(file_path, config, priority=priority)
|
|
515
|
+
file_count += 1
|
|
516
|
+
print(
|
|
517
|
+
f"[FuncSpanDebug] ✓ Successfully added to C config system (priority={priority})",
|
|
518
|
+
log=False,
|
|
519
|
+
)
|
|
520
|
+
except Exception as e:
|
|
521
|
+
print(
|
|
522
|
+
f"[FuncSpanDebug] ✗ FAILED to add to C config: {e}",
|
|
523
|
+
log=False,
|
|
524
|
+
)
|
|
525
|
+
if SF_DEBUG:
|
|
526
|
+
print(
|
|
527
|
+
f"[[DEBUG]] Config loader: Failed to add file config for {file_path}: {e}",
|
|
528
|
+
log=False,
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
# 3. Add all function configs
|
|
532
|
+
func_count = 0
|
|
533
|
+
for key in self.configs.keys():
|
|
534
|
+
if key.startswith('DIR:'):
|
|
535
|
+
dirpath = key[4:]
|
|
536
|
+
config = self.resolved_configs[key]
|
|
537
|
+
|
|
538
|
+
if 'functions' in self.configs[key]:
|
|
539
|
+
for func_pattern, func_config in self.configs[key]['functions'].items():
|
|
540
|
+
# Merge with directory base config
|
|
541
|
+
merged_config = {**config, **func_config}
|
|
542
|
+
self._add_function_configs(dirpath, func_pattern, merged_config)
|
|
543
|
+
func_count += 1
|
|
544
|
+
|
|
545
|
+
print(
|
|
546
|
+
f"[FuncSpanDebug] Built C tables with {file_count} file configs and {func_count} function configs",
|
|
547
|
+
log=False,
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
if SF_DEBUG:
|
|
551
|
+
print(
|
|
552
|
+
f"[[DEBUG]] Config loader: Built C tables with {file_count} file configs and {func_count} function configs",
|
|
553
|
+
log=False,
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
# 4. Pre-populate the C profiler cache to avoid Python calls during profiling
|
|
557
|
+
self._prepopulate_profiler_cache()
|
|
558
|
+
|
|
559
|
+
def _add_function_configs(self, dirpath: str, pattern: str, config: Dict):
|
|
560
|
+
"""Add function configs (handle wildcards)"""
|
|
561
|
+
if not self._c_config:
|
|
562
|
+
return
|
|
563
|
+
|
|
564
|
+
# For exact matches (no wildcards), add directly to C table
|
|
565
|
+
if '*' not in pattern:
|
|
566
|
+
# Pattern should be: "module.function" or "file.function"
|
|
567
|
+
# We need to resolve the module to a file path
|
|
568
|
+
|
|
569
|
+
# Try to find Python files that match
|
|
570
|
+
parts = pattern.rsplit('.', 1)
|
|
571
|
+
if len(parts) == 2:
|
|
572
|
+
module_pattern, func_name = parts
|
|
573
|
+
|
|
574
|
+
# Convert module pattern to file pattern
|
|
575
|
+
# e.g., "api.handlers" -> "api/handlers.py"
|
|
576
|
+
file_pattern = module_pattern.replace('.', os.sep) + '.py'
|
|
577
|
+
full_pattern = os.path.join(dirpath, file_pattern)
|
|
578
|
+
|
|
579
|
+
try:
|
|
580
|
+
matched_files = glob(full_pattern)
|
|
581
|
+
for matched_file in matched_files:
|
|
582
|
+
matched_file = os.path.normpath(matched_file)
|
|
583
|
+
try:
|
|
584
|
+
# PRIORITY: .sailfish function config has priority=3
|
|
585
|
+
# (lower than decorator which is 2)
|
|
586
|
+
self._c_config.add_function(matched_file, func_name, config, priority=3)
|
|
587
|
+
|
|
588
|
+
if SF_DEBUG:
|
|
589
|
+
print(
|
|
590
|
+
f"[[DEBUG]] Config loader: Added function config {matched_file}:{func_name} (priority=3/SAILFISH_FUNCTION)",
|
|
591
|
+
log=False,
|
|
592
|
+
)
|
|
593
|
+
except Exception as e:
|
|
594
|
+
if SF_DEBUG:
|
|
595
|
+
print(
|
|
596
|
+
f"[[DEBUG]] Config loader: Failed to add function config {matched_file}:{func_name}: {e}",
|
|
597
|
+
log=False,
|
|
598
|
+
)
|
|
599
|
+
except Exception as e:
|
|
600
|
+
if SF_DEBUG:
|
|
601
|
+
print(
|
|
602
|
+
f"[[DEBUG]] Config loader: Failed to resolve function pattern {pattern}: {e}",
|
|
603
|
+
log=False,
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
else:
|
|
607
|
+
# Wildcard patterns - we'll need to compile regex and match at runtime
|
|
608
|
+
# For now, we'll skip wildcards and just log
|
|
609
|
+
if SF_DEBUG:
|
|
610
|
+
print(
|
|
611
|
+
f"[[DEBUG]] Config loader: Skipping wildcard function pattern {pattern} (not yet supported)",
|
|
612
|
+
log=False,
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
def _prepopulate_profiler_cache(self):
|
|
616
|
+
"""Pre-populate the C profiler cache to avoid Python calls during profiling"""
|
|
617
|
+
if not self._c_profiler:
|
|
618
|
+
if SF_DEBUG:
|
|
619
|
+
print(
|
|
620
|
+
"[[DEBUG]] Config loader: C profiler not available, skipping cache pre-population",
|
|
621
|
+
log=False,
|
|
622
|
+
)
|
|
623
|
+
return
|
|
624
|
+
|
|
625
|
+
if SF_DEBUG:
|
|
626
|
+
print(
|
|
627
|
+
f"[[DEBUG]] Config loader: Starting cache pre-population with {len(self.resolved_configs)} resolved configs",
|
|
628
|
+
log=False,
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
cache_count = 0
|
|
632
|
+
|
|
633
|
+
# Pre-populate cache for all file-level configs (pragmas and .sailfish file patterns)
|
|
634
|
+
for key, config in self.resolved_configs.items():
|
|
635
|
+
if key.startswith('FILE:'):
|
|
636
|
+
file_path = key[5:] # Remove 'FILE:' prefix
|
|
637
|
+
|
|
638
|
+
# Extract config values
|
|
639
|
+
include_arguments = int(config.get('include_arguments', True))
|
|
640
|
+
include_return_value = int(config.get('include_return_value', True))
|
|
641
|
+
autocapture_all_children = int(config.get('autocapture_all_children', True))
|
|
642
|
+
arg_limit_mb = int(config.get('arg_limit_mb', 1))
|
|
643
|
+
return_limit_mb = int(config.get('return_limit_mb', 1))
|
|
644
|
+
sample_rate = float(config.get('sample_rate', 1.0))
|
|
645
|
+
|
|
646
|
+
if SF_DEBUG:
|
|
647
|
+
print(
|
|
648
|
+
f"[[DEBUG]] Config loader: Caching config for {file_path}: args={include_arguments} ret={include_return_value}",
|
|
649
|
+
log=False,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
# Cache with "<MODULE>" as function name to indicate file-level config
|
|
653
|
+
try:
|
|
654
|
+
self._c_profiler.cache_config(
|
|
655
|
+
file_path,
|
|
656
|
+
"<MODULE>",
|
|
657
|
+
include_arguments,
|
|
658
|
+
include_return_value,
|
|
659
|
+
autocapture_all_children,
|
|
660
|
+
arg_limit_mb,
|
|
661
|
+
return_limit_mb,
|
|
662
|
+
sample_rate
|
|
663
|
+
)
|
|
664
|
+
cache_count += 1
|
|
665
|
+
if SF_DEBUG:
|
|
666
|
+
print(
|
|
667
|
+
f"[[DEBUG]] Config loader: Successfully cached config for {file_path}",
|
|
668
|
+
log=False,
|
|
669
|
+
)
|
|
670
|
+
except Exception as e:
|
|
671
|
+
if SF_DEBUG:
|
|
672
|
+
print(
|
|
673
|
+
f"[[DEBUG]] Config loader: Failed to cache config for {file_path}: {e}",
|
|
674
|
+
log=False,
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
if SF_DEBUG:
|
|
678
|
+
print(
|
|
679
|
+
f"[[DEBUG]] Config loader: Pre-populated profiler cache with {cache_count} file-level configs",
|
|
680
|
+
log=False,
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def get_default_config() -> Dict:
|
|
685
|
+
"""Get default config from environment variables"""
|
|
686
|
+
return {
|
|
687
|
+
'include_arguments': SF_FUNCSPAN_CAPTURE_ARGUMENTS,
|
|
688
|
+
'include_return_value': SF_FUNCSPAN_CAPTURE_RETURN_VALUE,
|
|
689
|
+
'arg_limit_mb': SF_FUNCSPAN_ARG_LIMIT_MB,
|
|
690
|
+
'return_limit_mb': SF_FUNCSPAN_RETURN_LIMIT_MB,
|
|
691
|
+
'autocapture_all_children': SF_FUNCSPAN_AUTOCAPTURE_ALL_CHILD_FUNCTIONS,
|
|
692
|
+
'sample_rate': 1.0,
|
|
693
|
+
}
|