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.
Files changed (141) hide show
  1. sf_veritas/__init__.py +46 -0
  2. sf_veritas/_auto_preload.py +73 -0
  3. sf_veritas/_sfconfig.c +162 -0
  4. sf_veritas/_sfconfig.cpython-314-x86_64-linux-gnu.so +0 -0
  5. sf_veritas/_sfcrashhandler.c +267 -0
  6. sf_veritas/_sfcrashhandler.cpython-314-x86_64-linux-gnu.so +0 -0
  7. sf_veritas/_sffastlog.c +953 -0
  8. sf_veritas/_sffastlog.cpython-314-x86_64-linux-gnu.so +0 -0
  9. sf_veritas/_sffastnet.c +994 -0
  10. sf_veritas/_sffastnet.cpython-314-x86_64-linux-gnu.so +0 -0
  11. sf_veritas/_sffastnetworkrequest.c +727 -0
  12. sf_veritas/_sffastnetworkrequest.cpython-314-x86_64-linux-gnu.so +0 -0
  13. sf_veritas/_sffuncspan.c +2791 -0
  14. sf_veritas/_sffuncspan.cpython-314-x86_64-linux-gnu.so +0 -0
  15. sf_veritas/_sffuncspan_config.c +730 -0
  16. sf_veritas/_sffuncspan_config.cpython-314-x86_64-linux-gnu.so +0 -0
  17. sf_veritas/_sfheadercheck.c +341 -0
  18. sf_veritas/_sfheadercheck.cpython-314-x86_64-linux-gnu.so +0 -0
  19. sf_veritas/_sfnetworkhop.c +1454 -0
  20. sf_veritas/_sfnetworkhop.cpython-314-x86_64-linux-gnu.so +0 -0
  21. sf_veritas/_sfservice.c +1223 -0
  22. sf_veritas/_sfservice.cpython-314-x86_64-linux-gnu.so +0 -0
  23. sf_veritas/_sfteepreload.c +6227 -0
  24. sf_veritas/app_config.py +57 -0
  25. sf_veritas/cli.py +336 -0
  26. sf_veritas/constants.py +10 -0
  27. sf_veritas/custom_excepthook.py +304 -0
  28. sf_veritas/custom_log_handler.py +146 -0
  29. sf_veritas/custom_output_wrapper.py +153 -0
  30. sf_veritas/custom_print.py +153 -0
  31. sf_veritas/django_app.py +5 -0
  32. sf_veritas/env_vars.py +186 -0
  33. sf_veritas/exception_handling_middleware.py +18 -0
  34. sf_veritas/exception_metaclass.py +69 -0
  35. sf_veritas/fast_frame_info.py +116 -0
  36. sf_veritas/fast_network_hop.py +293 -0
  37. sf_veritas/frame_tools.py +112 -0
  38. sf_veritas/funcspan_config_loader.py +693 -0
  39. sf_veritas/function_span_profiler.py +1313 -0
  40. sf_veritas/get_preload_path.py +34 -0
  41. sf_veritas/import_hook.py +62 -0
  42. sf_veritas/infra_details/__init__.py +3 -0
  43. sf_veritas/infra_details/get_infra_details.py +24 -0
  44. sf_veritas/infra_details/kubernetes/__init__.py +3 -0
  45. sf_veritas/infra_details/kubernetes/get_cluster_name.py +147 -0
  46. sf_veritas/infra_details/kubernetes/get_details.py +7 -0
  47. sf_veritas/infra_details/running_on/__init__.py +17 -0
  48. sf_veritas/infra_details/running_on/kubernetes.py +11 -0
  49. sf_veritas/interceptors.py +543 -0
  50. sf_veritas/libsfnettee.so +0 -0
  51. sf_veritas/local_env_detect.py +118 -0
  52. sf_veritas/package_metadata.py +6 -0
  53. sf_veritas/patches/__init__.py +0 -0
  54. sf_veritas/patches/_patch_tracker.py +74 -0
  55. sf_veritas/patches/concurrent_futures.py +19 -0
  56. sf_veritas/patches/constants.py +1 -0
  57. sf_veritas/patches/exceptions.py +82 -0
  58. sf_veritas/patches/multiprocessing.py +32 -0
  59. sf_veritas/patches/network_libraries/__init__.py +99 -0
  60. sf_veritas/patches/network_libraries/aiohttp.py +294 -0
  61. sf_veritas/patches/network_libraries/curl_cffi.py +363 -0
  62. sf_veritas/patches/network_libraries/http_client.py +670 -0
  63. sf_veritas/patches/network_libraries/httpcore.py +580 -0
  64. sf_veritas/patches/network_libraries/httplib2.py +315 -0
  65. sf_veritas/patches/network_libraries/httpx.py +557 -0
  66. sf_veritas/patches/network_libraries/niquests.py +218 -0
  67. sf_veritas/patches/network_libraries/pycurl.py +399 -0
  68. sf_veritas/patches/network_libraries/requests.py +595 -0
  69. sf_veritas/patches/network_libraries/ssl_socket.py +822 -0
  70. sf_veritas/patches/network_libraries/tornado.py +360 -0
  71. sf_veritas/patches/network_libraries/treq.py +270 -0
  72. sf_veritas/patches/network_libraries/urllib_request.py +483 -0
  73. sf_veritas/patches/network_libraries/utils.py +598 -0
  74. sf_veritas/patches/os.py +17 -0
  75. sf_veritas/patches/threading.py +231 -0
  76. sf_veritas/patches/web_frameworks/__init__.py +54 -0
  77. sf_veritas/patches/web_frameworks/aiohttp.py +798 -0
  78. sf_veritas/patches/web_frameworks/async_websocket_consumer.py +337 -0
  79. sf_veritas/patches/web_frameworks/blacksheep.py +532 -0
  80. sf_veritas/patches/web_frameworks/bottle.py +513 -0
  81. sf_veritas/patches/web_frameworks/cherrypy.py +683 -0
  82. sf_veritas/patches/web_frameworks/cors_utils.py +122 -0
  83. sf_veritas/patches/web_frameworks/django.py +963 -0
  84. sf_veritas/patches/web_frameworks/eve.py +401 -0
  85. sf_veritas/patches/web_frameworks/falcon.py +931 -0
  86. sf_veritas/patches/web_frameworks/fastapi.py +738 -0
  87. sf_veritas/patches/web_frameworks/flask.py +526 -0
  88. sf_veritas/patches/web_frameworks/klein.py +501 -0
  89. sf_veritas/patches/web_frameworks/litestar.py +616 -0
  90. sf_veritas/patches/web_frameworks/pyramid.py +440 -0
  91. sf_veritas/patches/web_frameworks/quart.py +841 -0
  92. sf_veritas/patches/web_frameworks/robyn.py +708 -0
  93. sf_veritas/patches/web_frameworks/sanic.py +874 -0
  94. sf_veritas/patches/web_frameworks/starlette.py +742 -0
  95. sf_veritas/patches/web_frameworks/strawberry.py +1446 -0
  96. sf_veritas/patches/web_frameworks/tornado.py +485 -0
  97. sf_veritas/patches/web_frameworks/utils.py +170 -0
  98. sf_veritas/print_override.py +13 -0
  99. sf_veritas/regular_data_transmitter.py +444 -0
  100. sf_veritas/request_interceptor.py +401 -0
  101. sf_veritas/request_utils.py +550 -0
  102. sf_veritas/segfault_handler.py +116 -0
  103. sf_veritas/server_status.py +1 -0
  104. sf_veritas/shutdown_flag.py +11 -0
  105. sf_veritas/subprocess_startup.py +3 -0
  106. sf_veritas/test_cli.py +145 -0
  107. sf_veritas/thread_local.py +1319 -0
  108. sf_veritas/timeutil.py +114 -0
  109. sf_veritas/transmit_exception_to_sailfish.py +28 -0
  110. sf_veritas/transmitter.py +132 -0
  111. sf_veritas/types.py +47 -0
  112. sf_veritas/unified_interceptor.py +1678 -0
  113. sf_veritas/utils.py +39 -0
  114. sf_veritas-0.11.10.dist-info/METADATA +97 -0
  115. sf_veritas-0.11.10.dist-info/RECORD +141 -0
  116. sf_veritas-0.11.10.dist-info/WHEEL +5 -0
  117. sf_veritas-0.11.10.dist-info/entry_points.txt +2 -0
  118. sf_veritas-0.11.10.dist-info/top_level.txt +1 -0
  119. sf_veritas.libs/libbrotlicommon-6ce2a53c.so.1.0.6 +0 -0
  120. sf_veritas.libs/libbrotlidec-811d1be3.so.1.0.6 +0 -0
  121. sf_veritas.libs/libcom_err-730ca923.so.2.1 +0 -0
  122. sf_veritas.libs/libcrypt-52aca757.so.1.1.0 +0 -0
  123. sf_veritas.libs/libcrypto-bdaed0ea.so.1.1.1k +0 -0
  124. sf_veritas.libs/libcurl-eaa3cf66.so.4.5.0 +0 -0
  125. sf_veritas.libs/libgssapi_krb5-323bbd21.so.2.2 +0 -0
  126. sf_veritas.libs/libidn2-2f4a5893.so.0.3.6 +0 -0
  127. sf_veritas.libs/libk5crypto-9a74ff38.so.3.1 +0 -0
  128. sf_veritas.libs/libkeyutils-2777d33d.so.1.6 +0 -0
  129. sf_veritas.libs/libkrb5-a55300e8.so.3.3 +0 -0
  130. sf_veritas.libs/libkrb5support-e6594cfc.so.0.1 +0 -0
  131. sf_veritas.libs/liblber-2-d20824ef.4.so.2.10.9 +0 -0
  132. sf_veritas.libs/libldap-2-cea2a960.4.so.2.10.9 +0 -0
  133. sf_veritas.libs/libnghttp2-39367a22.so.14.17.0 +0 -0
  134. sf_veritas.libs/libpcre2-8-516f4c9d.so.0.7.1 +0 -0
  135. sf_veritas.libs/libpsl-99becdd3.so.5.3.1 +0 -0
  136. sf_veritas.libs/libsasl2-7de4d792.so.3.0.0 +0 -0
  137. sf_veritas.libs/libselinux-d0805dcb.so.1 +0 -0
  138. sf_veritas.libs/libssh-c11d285b.so.4.8.7 +0 -0
  139. sf_veritas.libs/libssl-60250281.so.1.1.1k +0 -0
  140. sf_veritas.libs/libunistring-05abdd40.so.2.1.0 +0 -0
  141. 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
+ }