sf-veritas 0.10.3__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.
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-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnet.c +924 -0
- sf_veritas/_sffastnet.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffastnetworkrequest.c +730 -0
- sf_veritas/_sffastnetworkrequest.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan.c +2155 -0
- sf_veritas/_sffuncspan.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sffuncspan_config.c +617 -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 +1451 -0
- sf_veritas/_sfnetworkhop.cpython-314-x86_64-linux-gnu.so +0 -0
- sf_veritas/_sfservice.c +1175 -0
- sf_veritas/_sfservice.cpython-314-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,556 @@
|
|
|
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
|
+
# 1. Walk directory trees for .sailfish files
|
|
82
|
+
for root in self.root_paths:
|
|
83
|
+
if not os.path.exists(root):
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
for dirpath, _, filenames in os.walk(root):
|
|
87
|
+
if '.sailfish' in filenames:
|
|
88
|
+
config_path = os.path.join(dirpath, '.sailfish')
|
|
89
|
+
self._load_config_file(config_path, dirpath)
|
|
90
|
+
|
|
91
|
+
# 2. Scan all Python files for pragmas (only first 50 lines, ONE per file!)
|
|
92
|
+
for root in self.root_paths:
|
|
93
|
+
if not os.path.exists(root):
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
for py_file in Path(root).rglob('*.py'):
|
|
97
|
+
self._scan_pragma(str(py_file))
|
|
98
|
+
|
|
99
|
+
# 3. Resolve inheritance and build C tables
|
|
100
|
+
self._resolve_inheritance()
|
|
101
|
+
self._build_c_tables()
|
|
102
|
+
|
|
103
|
+
if SF_DEBUG:
|
|
104
|
+
print(
|
|
105
|
+
f"[[DEBUG]] Config loader: Loaded {len(self.resolved_configs)} configs",
|
|
106
|
+
log=False,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def _load_config_file(self, path: str, dirpath: str):
|
|
110
|
+
"""Load .sailfish file (auto-detect YAML/TOML/JSON format)"""
|
|
111
|
+
try:
|
|
112
|
+
with open(path, 'r') as f:
|
|
113
|
+
content = f.read()
|
|
114
|
+
except Exception as e:
|
|
115
|
+
if SF_DEBUG:
|
|
116
|
+
print(
|
|
117
|
+
f"[[DEBUG]] Config loader: Failed to read {path}: {e}",
|
|
118
|
+
log=False,
|
|
119
|
+
)
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
# Try to parse in order: YAML, TOML, JSON
|
|
123
|
+
config = None
|
|
124
|
+
|
|
125
|
+
# Try YAML first (most common)
|
|
126
|
+
try:
|
|
127
|
+
import yaml
|
|
128
|
+
config = yaml.safe_load(content)
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
# Try TOML if YAML failed
|
|
133
|
+
if config is None:
|
|
134
|
+
try:
|
|
135
|
+
try:
|
|
136
|
+
import tomllib # Python 3.11+
|
|
137
|
+
except ImportError:
|
|
138
|
+
import tomli as tomllib # fallback for older Python
|
|
139
|
+
config = tomllib.loads(content)
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
# Try JSON if both YAML and TOML failed
|
|
144
|
+
if config is None:
|
|
145
|
+
try:
|
|
146
|
+
config = json.loads(content)
|
|
147
|
+
except Exception as e:
|
|
148
|
+
if SF_DEBUG:
|
|
149
|
+
print(
|
|
150
|
+
f"[[DEBUG]] Config loader: Failed to parse {path} as YAML/TOML/JSON: {e}",
|
|
151
|
+
log=False,
|
|
152
|
+
)
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
# Extract funcspan section (optional wrapper)
|
|
156
|
+
# Support both wrapped (funcspan: {...}) and unwrapped formats
|
|
157
|
+
funcspan_config = config.get('funcspan', config)
|
|
158
|
+
|
|
159
|
+
# Check if this looks like a funcspan config (has expected keys)
|
|
160
|
+
expected_keys = {'default', 'files', 'functions'}
|
|
161
|
+
if not any(key in funcspan_config for key in expected_keys):
|
|
162
|
+
if SF_DEBUG:
|
|
163
|
+
print(
|
|
164
|
+
f"[[DEBUG]] Config loader: No funcspan config in {path}",
|
|
165
|
+
log=False,
|
|
166
|
+
)
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
# Store with directory context
|
|
170
|
+
self.configs[f"DIR:{dirpath}"] = funcspan_config
|
|
171
|
+
|
|
172
|
+
if SF_DEBUG:
|
|
173
|
+
print(
|
|
174
|
+
f"[[DEBUG]] Config loader: Loaded config from {path}",
|
|
175
|
+
log=False,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def _scan_pragma(self, file_path: str):
|
|
179
|
+
"""Scan file for inline pragma (first 50 lines, ONE per file!)"""
|
|
180
|
+
try:
|
|
181
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
182
|
+
for i, line in enumerate(f):
|
|
183
|
+
if i > 50: # Only check first 50 lines
|
|
184
|
+
break
|
|
185
|
+
|
|
186
|
+
# Match: # sailfish-funcspan: include_arguments=false, sample_rate=0.1
|
|
187
|
+
if match := re.match(r'#\s*sailfish-funcspan:\s*(.+)', line):
|
|
188
|
+
config = self._parse_pragma(match.group(1))
|
|
189
|
+
if config:
|
|
190
|
+
# Store file-level config
|
|
191
|
+
self.configs[f"FILE:{file_path}"] = config
|
|
192
|
+
if SF_DEBUG:
|
|
193
|
+
print(
|
|
194
|
+
f"[[DEBUG]] Config loader: Found pragma in {file_path}: {config}",
|
|
195
|
+
log=False,
|
|
196
|
+
)
|
|
197
|
+
return # Only ONE pragma per file!
|
|
198
|
+
except Exception as e:
|
|
199
|
+
if SF_DEBUG:
|
|
200
|
+
print(
|
|
201
|
+
f"[[DEBUG]] Config loader: Error scanning {file_path}: {e}",
|
|
202
|
+
log=False,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def _parse_pragma(self, pragma_str: str) -> Optional[Dict]:
|
|
206
|
+
"""Parse pragma: include_arguments=false, sample_rate=0.1"""
|
|
207
|
+
config = {}
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
for part in pragma_str.split(','):
|
|
211
|
+
if '=' not in part:
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
key, value = part.strip().split('=', 1)
|
|
215
|
+
key = key.strip()
|
|
216
|
+
value = value.strip()
|
|
217
|
+
|
|
218
|
+
# Type conversion
|
|
219
|
+
if value.lower() in ('true', 'false'):
|
|
220
|
+
config[key] = value.lower() == 'true'
|
|
221
|
+
elif '.' in value:
|
|
222
|
+
config[key] = float(value)
|
|
223
|
+
else:
|
|
224
|
+
config[key] = int(value)
|
|
225
|
+
|
|
226
|
+
return config if config else None
|
|
227
|
+
except Exception as e:
|
|
228
|
+
if SF_DEBUG:
|
|
229
|
+
print(
|
|
230
|
+
f"[[DEBUG]] Config loader: Failed to parse pragma '{pragma_str}': {e}",
|
|
231
|
+
log=False,
|
|
232
|
+
)
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
def _resolve_inheritance(self):
|
|
236
|
+
"""Resolve directory hierarchy and glob patterns"""
|
|
237
|
+
resolved = {}
|
|
238
|
+
|
|
239
|
+
# Determine default config based on whether user has .sailfish files (opt-in)
|
|
240
|
+
if len(self.configs) == 0:
|
|
241
|
+
# No .sailfish files = no opt-in = capture NOTHING by default
|
|
242
|
+
# Headers, decorators, and pragmas can still enable capture
|
|
243
|
+
default_config = {
|
|
244
|
+
'include_arguments': False,
|
|
245
|
+
'include_return_value': False,
|
|
246
|
+
'autocapture_all_children': False,
|
|
247
|
+
'arg_limit_mb': 1,
|
|
248
|
+
'return_limit_mb': 1,
|
|
249
|
+
'sample_rate': 1.0,
|
|
250
|
+
}
|
|
251
|
+
if SF_DEBUG:
|
|
252
|
+
print(
|
|
253
|
+
"[[DEBUG]] Config loader: No .sailfish files found, default capture disabled",
|
|
254
|
+
log=False,
|
|
255
|
+
)
|
|
256
|
+
else:
|
|
257
|
+
# Has .sailfish files = user opted in = use env var defaults
|
|
258
|
+
default_config = {
|
|
259
|
+
'include_arguments': SF_FUNCSPAN_CAPTURE_ARGUMENTS,
|
|
260
|
+
'include_return_value': SF_FUNCSPAN_CAPTURE_RETURN_VALUE,
|
|
261
|
+
'autocapture_all_children': SF_FUNCSPAN_AUTOCAPTURE_ALL_CHILD_FUNCTIONS,
|
|
262
|
+
'arg_limit_mb': SF_FUNCSPAN_ARG_LIMIT_MB,
|
|
263
|
+
'return_limit_mb': SF_FUNCSPAN_RETURN_LIMIT_MB,
|
|
264
|
+
'sample_rate': 1.0,
|
|
265
|
+
}
|
|
266
|
+
if SF_DEBUG:
|
|
267
|
+
print(
|
|
268
|
+
f"[[DEBUG]] Config loader: Found {len(self.configs)} .sailfish configs, using env var defaults",
|
|
269
|
+
log=False,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Process directory configs (cascade down)
|
|
273
|
+
dir_configs = sorted([k for k in self.configs.keys() if k.startswith('DIR:')])
|
|
274
|
+
|
|
275
|
+
for key in dir_configs:
|
|
276
|
+
dirpath = key[4:] # Remove 'DIR:' prefix
|
|
277
|
+
parent_config = self._get_parent_config(dirpath, resolved)
|
|
278
|
+
config = {**default_config, **parent_config, **self.configs[key]}
|
|
279
|
+
resolved[key] = config
|
|
280
|
+
|
|
281
|
+
# Expand glob patterns in files: section
|
|
282
|
+
if 'files' in self.configs[key]:
|
|
283
|
+
self._expand_file_globs(dirpath, self.configs[key]['files'], resolved, config)
|
|
284
|
+
|
|
285
|
+
# Add file-level configs (pragmas)
|
|
286
|
+
for key in self.configs.keys():
|
|
287
|
+
if key.startswith('FILE:'):
|
|
288
|
+
# Get directory config as base
|
|
289
|
+
file_path = key[5:] # Remove 'FILE:' prefix
|
|
290
|
+
dir_config = self._get_directory_config_for_file(file_path, resolved)
|
|
291
|
+
resolved[key] = {**default_config, **dir_config, **self.configs[key]}
|
|
292
|
+
|
|
293
|
+
self.resolved_configs = resolved
|
|
294
|
+
|
|
295
|
+
def _get_parent_config(self, dirpath: str, resolved: Dict) -> Dict:
|
|
296
|
+
"""Get parent directory's config for inheritance"""
|
|
297
|
+
parent = os.path.dirname(dirpath)
|
|
298
|
+
|
|
299
|
+
# Keep walking up until we find a config or reach root
|
|
300
|
+
while parent and parent != dirpath:
|
|
301
|
+
parent_key = f"DIR:{parent}"
|
|
302
|
+
if parent_key in resolved:
|
|
303
|
+
return resolved[parent_key]
|
|
304
|
+
dirpath = parent
|
|
305
|
+
parent = os.path.dirname(parent)
|
|
306
|
+
|
|
307
|
+
return {}
|
|
308
|
+
|
|
309
|
+
def _get_directory_config_for_file(self, file_path: str, resolved: Dict) -> Dict:
|
|
310
|
+
"""Get the directory config that applies to this file"""
|
|
311
|
+
dirpath = os.path.dirname(file_path)
|
|
312
|
+
|
|
313
|
+
# Walk up directory tree until we find a config
|
|
314
|
+
while dirpath:
|
|
315
|
+
dir_key = f"DIR:{dirpath}"
|
|
316
|
+
if dir_key in resolved:
|
|
317
|
+
return resolved[dir_key]
|
|
318
|
+
|
|
319
|
+
parent = os.path.dirname(dirpath)
|
|
320
|
+
if parent == dirpath: # Reached root
|
|
321
|
+
break
|
|
322
|
+
dirpath = parent
|
|
323
|
+
|
|
324
|
+
return {}
|
|
325
|
+
|
|
326
|
+
def _expand_file_globs(self, dirpath: str, file_patterns: Dict, resolved: Dict, base_config: Dict):
|
|
327
|
+
"""Expand glob patterns to actual file paths"""
|
|
328
|
+
for pattern, config in file_patterns.items():
|
|
329
|
+
full_pattern = os.path.join(dirpath, pattern)
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
matched_files = glob(full_pattern, recursive=True)
|
|
333
|
+
for matched_file in matched_files:
|
|
334
|
+
# Normalize path
|
|
335
|
+
matched_file = os.path.normpath(matched_file)
|
|
336
|
+
resolved[f"FILE:{matched_file}"] = {**base_config, **config}
|
|
337
|
+
|
|
338
|
+
if SF_DEBUG:
|
|
339
|
+
print(
|
|
340
|
+
f"[[DEBUG]] Config loader: Matched file {matched_file} with pattern {pattern}",
|
|
341
|
+
log=False,
|
|
342
|
+
)
|
|
343
|
+
except Exception as e:
|
|
344
|
+
if SF_DEBUG:
|
|
345
|
+
print(
|
|
346
|
+
f"[[DEBUG]] Config loader: Failed to expand glob {full_pattern}: {e}",
|
|
347
|
+
log=False,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
def _build_c_tables(self):
|
|
351
|
+
"""Build C hash tables for ultra-fast lookups"""
|
|
352
|
+
if not self._c_config:
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
# 1. Initialize C config system with defaults
|
|
356
|
+
# Use same logic as _resolve_inheritance: no .sailfish = no default capture
|
|
357
|
+
if len(self.configs) == 0:
|
|
358
|
+
default_config = {
|
|
359
|
+
'include_arguments': False,
|
|
360
|
+
'include_return_value': False,
|
|
361
|
+
'autocapture_all_children': False,
|
|
362
|
+
'arg_limit_mb': 1,
|
|
363
|
+
'return_limit_mb': 1,
|
|
364
|
+
'sample_rate': 1.0,
|
|
365
|
+
}
|
|
366
|
+
else:
|
|
367
|
+
default_config = {
|
|
368
|
+
'include_arguments': SF_FUNCSPAN_CAPTURE_ARGUMENTS,
|
|
369
|
+
'include_return_value': SF_FUNCSPAN_CAPTURE_RETURN_VALUE,
|
|
370
|
+
'arg_limit_mb': SF_FUNCSPAN_ARG_LIMIT_MB,
|
|
371
|
+
'return_limit_mb': SF_FUNCSPAN_RETURN_LIMIT_MB,
|
|
372
|
+
'autocapture_all_children': SF_FUNCSPAN_AUTOCAPTURE_ALL_CHILD_FUNCTIONS,
|
|
373
|
+
'sample_rate': 1.0,
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
self._c_config.init(default_config)
|
|
378
|
+
except Exception as e:
|
|
379
|
+
if SF_DEBUG:
|
|
380
|
+
print(
|
|
381
|
+
f"[[DEBUG]] Config loader: Failed to initialize C config: {e}",
|
|
382
|
+
log=False,
|
|
383
|
+
)
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
# 2. Add all file configs
|
|
387
|
+
file_count = 0
|
|
388
|
+
for key, config in self.resolved_configs.items():
|
|
389
|
+
if key.startswith('FILE:'):
|
|
390
|
+
file_path = key[5:] # Remove 'FILE:' prefix
|
|
391
|
+
try:
|
|
392
|
+
self._c_config.add_file(file_path, config)
|
|
393
|
+
file_count += 1
|
|
394
|
+
except Exception as e:
|
|
395
|
+
if SF_DEBUG:
|
|
396
|
+
print(
|
|
397
|
+
f"[[DEBUG]] Config loader: Failed to add file config for {file_path}: {e}",
|
|
398
|
+
log=False,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# 3. Add all function configs
|
|
402
|
+
func_count = 0
|
|
403
|
+
for key in self.configs.keys():
|
|
404
|
+
if key.startswith('DIR:'):
|
|
405
|
+
dirpath = key[4:]
|
|
406
|
+
config = self.resolved_configs[key]
|
|
407
|
+
|
|
408
|
+
if 'functions' in self.configs[key]:
|
|
409
|
+
for func_pattern, func_config in self.configs[key]['functions'].items():
|
|
410
|
+
# Merge with directory base config
|
|
411
|
+
merged_config = {**config, **func_config}
|
|
412
|
+
self._add_function_configs(dirpath, func_pattern, merged_config)
|
|
413
|
+
func_count += 1
|
|
414
|
+
|
|
415
|
+
if SF_DEBUG:
|
|
416
|
+
print(
|
|
417
|
+
f"[[DEBUG]] Config loader: Built C tables with {file_count} file configs and {func_count} function configs",
|
|
418
|
+
log=False,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
# 4. Pre-populate the C profiler cache to avoid Python calls during profiling
|
|
422
|
+
self._prepopulate_profiler_cache()
|
|
423
|
+
|
|
424
|
+
def _add_function_configs(self, dirpath: str, pattern: str, config: Dict):
|
|
425
|
+
"""Add function configs (handle wildcards)"""
|
|
426
|
+
if not self._c_config:
|
|
427
|
+
return
|
|
428
|
+
|
|
429
|
+
# For exact matches (no wildcards), add directly to C table
|
|
430
|
+
if '*' not in pattern:
|
|
431
|
+
# Pattern should be: "module.function" or "file.function"
|
|
432
|
+
# We need to resolve the module to a file path
|
|
433
|
+
|
|
434
|
+
# Try to find Python files that match
|
|
435
|
+
parts = pattern.rsplit('.', 1)
|
|
436
|
+
if len(parts) == 2:
|
|
437
|
+
module_pattern, func_name = parts
|
|
438
|
+
|
|
439
|
+
# Convert module pattern to file pattern
|
|
440
|
+
# e.g., "api.handlers" -> "api/handlers.py"
|
|
441
|
+
file_pattern = module_pattern.replace('.', os.sep) + '.py'
|
|
442
|
+
full_pattern = os.path.join(dirpath, file_pattern)
|
|
443
|
+
|
|
444
|
+
try:
|
|
445
|
+
matched_files = glob(full_pattern)
|
|
446
|
+
for matched_file in matched_files:
|
|
447
|
+
matched_file = os.path.normpath(matched_file)
|
|
448
|
+
try:
|
|
449
|
+
self._c_config.add_function(matched_file, func_name, config)
|
|
450
|
+
|
|
451
|
+
if SF_DEBUG:
|
|
452
|
+
print(
|
|
453
|
+
f"[[DEBUG]] Config loader: Added function config {matched_file}:{func_name}",
|
|
454
|
+
log=False,
|
|
455
|
+
)
|
|
456
|
+
except Exception as e:
|
|
457
|
+
if SF_DEBUG:
|
|
458
|
+
print(
|
|
459
|
+
f"[[DEBUG]] Config loader: Failed to add function config {matched_file}:{func_name}: {e}",
|
|
460
|
+
log=False,
|
|
461
|
+
)
|
|
462
|
+
except Exception as e:
|
|
463
|
+
if SF_DEBUG:
|
|
464
|
+
print(
|
|
465
|
+
f"[[DEBUG]] Config loader: Failed to resolve function pattern {pattern}: {e}",
|
|
466
|
+
log=False,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
else:
|
|
470
|
+
# Wildcard patterns - we'll need to compile regex and match at runtime
|
|
471
|
+
# For now, we'll skip wildcards and just log
|
|
472
|
+
if SF_DEBUG:
|
|
473
|
+
print(
|
|
474
|
+
f"[[DEBUG]] Config loader: Skipping wildcard function pattern {pattern} (not yet supported)",
|
|
475
|
+
log=False,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
def _prepopulate_profiler_cache(self):
|
|
479
|
+
"""Pre-populate the C profiler cache to avoid Python calls during profiling"""
|
|
480
|
+
if not self._c_profiler:
|
|
481
|
+
if SF_DEBUG:
|
|
482
|
+
print(
|
|
483
|
+
"[[DEBUG]] Config loader: C profiler not available, skipping cache pre-population",
|
|
484
|
+
log=False,
|
|
485
|
+
)
|
|
486
|
+
return
|
|
487
|
+
|
|
488
|
+
if SF_DEBUG:
|
|
489
|
+
print(
|
|
490
|
+
f"[[DEBUG]] Config loader: Starting cache pre-population with {len(self.resolved_configs)} resolved configs",
|
|
491
|
+
log=False,
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
cache_count = 0
|
|
495
|
+
|
|
496
|
+
# Pre-populate cache for all file-level configs (pragmas and .sailfish file patterns)
|
|
497
|
+
for key, config in self.resolved_configs.items():
|
|
498
|
+
if key.startswith('FILE:'):
|
|
499
|
+
file_path = key[5:] # Remove 'FILE:' prefix
|
|
500
|
+
|
|
501
|
+
# Extract config values
|
|
502
|
+
include_arguments = int(config.get('include_arguments', True))
|
|
503
|
+
include_return_value = int(config.get('include_return_value', True))
|
|
504
|
+
autocapture_all_children = int(config.get('autocapture_all_children', True))
|
|
505
|
+
arg_limit_mb = int(config.get('arg_limit_mb', 1))
|
|
506
|
+
return_limit_mb = int(config.get('return_limit_mb', 1))
|
|
507
|
+
sample_rate = float(config.get('sample_rate', 1.0))
|
|
508
|
+
|
|
509
|
+
if SF_DEBUG:
|
|
510
|
+
print(
|
|
511
|
+
f"[[DEBUG]] Config loader: Caching config for {file_path}: args={include_arguments} ret={include_return_value}",
|
|
512
|
+
log=False,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# Cache with "<MODULE>" as function name to indicate file-level config
|
|
516
|
+
try:
|
|
517
|
+
self._c_profiler.cache_config(
|
|
518
|
+
file_path,
|
|
519
|
+
"<MODULE>",
|
|
520
|
+
include_arguments,
|
|
521
|
+
include_return_value,
|
|
522
|
+
autocapture_all_children,
|
|
523
|
+
arg_limit_mb,
|
|
524
|
+
return_limit_mb,
|
|
525
|
+
sample_rate
|
|
526
|
+
)
|
|
527
|
+
cache_count += 1
|
|
528
|
+
if SF_DEBUG:
|
|
529
|
+
print(
|
|
530
|
+
f"[[DEBUG]] Config loader: Successfully cached config for {file_path}",
|
|
531
|
+
log=False,
|
|
532
|
+
)
|
|
533
|
+
except Exception as e:
|
|
534
|
+
if SF_DEBUG:
|
|
535
|
+
print(
|
|
536
|
+
f"[[DEBUG]] Config loader: Failed to cache config for {file_path}: {e}",
|
|
537
|
+
log=False,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
if SF_DEBUG:
|
|
541
|
+
print(
|
|
542
|
+
f"[[DEBUG]] Config loader: Pre-populated profiler cache with {cache_count} file-level configs",
|
|
543
|
+
log=False,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def get_default_config() -> Dict:
|
|
548
|
+
"""Get default config from environment variables"""
|
|
549
|
+
return {
|
|
550
|
+
'include_arguments': SF_FUNCSPAN_CAPTURE_ARGUMENTS,
|
|
551
|
+
'include_return_value': SF_FUNCSPAN_CAPTURE_RETURN_VALUE,
|
|
552
|
+
'arg_limit_mb': SF_FUNCSPAN_ARG_LIMIT_MB,
|
|
553
|
+
'return_limit_mb': SF_FUNCSPAN_RETURN_LIMIT_MB,
|
|
554
|
+
'autocapture_all_children': SF_FUNCSPAN_AUTOCAPTURE_ALL_CHILD_FUNCTIONS,
|
|
555
|
+
'sample_rate': 1.0,
|
|
556
|
+
}
|