sf-veritas 0.10.3__cp39-cp39-manylinux_2_28_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sf-veritas might be problematic. Click here for more details.

Files changed (132) hide show
  1. sf_veritas/__init__.py +20 -0
  2. sf_veritas/_sffastlog.c +889 -0
  3. sf_veritas/_sffastlog.cpython-39-x86_64-linux-gnu.so +0 -0
  4. sf_veritas/_sffastnet.c +924 -0
  5. sf_veritas/_sffastnet.cpython-39-x86_64-linux-gnu.so +0 -0
  6. sf_veritas/_sffastnetworkrequest.c +730 -0
  7. sf_veritas/_sffastnetworkrequest.cpython-39-x86_64-linux-gnu.so +0 -0
  8. sf_veritas/_sffuncspan.c +2155 -0
  9. sf_veritas/_sffuncspan.cpython-39-x86_64-linux-gnu.so +0 -0
  10. sf_veritas/_sffuncspan_config.c +617 -0
  11. sf_veritas/_sffuncspan_config.cpython-39-x86_64-linux-gnu.so +0 -0
  12. sf_veritas/_sfheadercheck.c +341 -0
  13. sf_veritas/_sfheadercheck.cpython-39-x86_64-linux-gnu.so +0 -0
  14. sf_veritas/_sfnetworkhop.c +1451 -0
  15. sf_veritas/_sfnetworkhop.cpython-39-x86_64-linux-gnu.so +0 -0
  16. sf_veritas/_sfservice.c +1175 -0
  17. sf_veritas/_sfservice.cpython-39-x86_64-linux-gnu.so +0 -0
  18. sf_veritas/_sfteepreload.c +5167 -0
  19. sf_veritas/app_config.py +49 -0
  20. sf_veritas/cli.py +336 -0
  21. sf_veritas/constants.py +10 -0
  22. sf_veritas/custom_excepthook.py +304 -0
  23. sf_veritas/custom_log_handler.py +129 -0
  24. sf_veritas/custom_output_wrapper.py +144 -0
  25. sf_veritas/custom_print.py +146 -0
  26. sf_veritas/django_app.py +5 -0
  27. sf_veritas/env_vars.py +186 -0
  28. sf_veritas/exception_handling_middleware.py +18 -0
  29. sf_veritas/exception_metaclass.py +69 -0
  30. sf_veritas/fast_frame_info.py +116 -0
  31. sf_veritas/fast_network_hop.py +293 -0
  32. sf_veritas/frame_tools.py +112 -0
  33. sf_veritas/funcspan_config_loader.py +556 -0
  34. sf_veritas/function_span_profiler.py +1174 -0
  35. sf_veritas/import_hook.py +62 -0
  36. sf_veritas/infra_details/__init__.py +3 -0
  37. sf_veritas/infra_details/get_infra_details.py +24 -0
  38. sf_veritas/infra_details/kubernetes/__init__.py +3 -0
  39. sf_veritas/infra_details/kubernetes/get_cluster_name.py +147 -0
  40. sf_veritas/infra_details/kubernetes/get_details.py +7 -0
  41. sf_veritas/infra_details/running_on/__init__.py +17 -0
  42. sf_veritas/infra_details/running_on/kubernetes.py +11 -0
  43. sf_veritas/interceptors.py +497 -0
  44. sf_veritas/libsfnettee.so +0 -0
  45. sf_veritas/local_env_detect.py +118 -0
  46. sf_veritas/package_metadata.py +6 -0
  47. sf_veritas/patches/__init__.py +0 -0
  48. sf_veritas/patches/concurrent_futures.py +19 -0
  49. sf_veritas/patches/constants.py +1 -0
  50. sf_veritas/patches/exceptions.py +82 -0
  51. sf_veritas/patches/multiprocessing.py +32 -0
  52. sf_veritas/patches/network_libraries/__init__.py +76 -0
  53. sf_veritas/patches/network_libraries/aiohttp.py +281 -0
  54. sf_veritas/patches/network_libraries/curl_cffi.py +363 -0
  55. sf_veritas/patches/network_libraries/http_client.py +419 -0
  56. sf_veritas/patches/network_libraries/httpcore.py +515 -0
  57. sf_veritas/patches/network_libraries/httplib2.py +204 -0
  58. sf_veritas/patches/network_libraries/httpx.py +515 -0
  59. sf_veritas/patches/network_libraries/niquests.py +211 -0
  60. sf_veritas/patches/network_libraries/pycurl.py +385 -0
  61. sf_veritas/patches/network_libraries/requests.py +633 -0
  62. sf_veritas/patches/network_libraries/tornado.py +341 -0
  63. sf_veritas/patches/network_libraries/treq.py +270 -0
  64. sf_veritas/patches/network_libraries/urllib_request.py +468 -0
  65. sf_veritas/patches/network_libraries/utils.py +398 -0
  66. sf_veritas/patches/os.py +17 -0
  67. sf_veritas/patches/threading.py +218 -0
  68. sf_veritas/patches/web_frameworks/__init__.py +54 -0
  69. sf_veritas/patches/web_frameworks/aiohttp.py +793 -0
  70. sf_veritas/patches/web_frameworks/async_websocket_consumer.py +317 -0
  71. sf_veritas/patches/web_frameworks/blacksheep.py +527 -0
  72. sf_veritas/patches/web_frameworks/bottle.py +502 -0
  73. sf_veritas/patches/web_frameworks/cherrypy.py +678 -0
  74. sf_veritas/patches/web_frameworks/cors_utils.py +122 -0
  75. sf_veritas/patches/web_frameworks/django.py +944 -0
  76. sf_veritas/patches/web_frameworks/eve.py +395 -0
  77. sf_veritas/patches/web_frameworks/falcon.py +926 -0
  78. sf_veritas/patches/web_frameworks/fastapi.py +724 -0
  79. sf_veritas/patches/web_frameworks/flask.py +520 -0
  80. sf_veritas/patches/web_frameworks/klein.py +501 -0
  81. sf_veritas/patches/web_frameworks/litestar.py +551 -0
  82. sf_veritas/patches/web_frameworks/pyramid.py +428 -0
  83. sf_veritas/patches/web_frameworks/quart.py +824 -0
  84. sf_veritas/patches/web_frameworks/robyn.py +697 -0
  85. sf_veritas/patches/web_frameworks/sanic.py +857 -0
  86. sf_veritas/patches/web_frameworks/starlette.py +723 -0
  87. sf_veritas/patches/web_frameworks/strawberry.py +813 -0
  88. sf_veritas/patches/web_frameworks/tornado.py +481 -0
  89. sf_veritas/patches/web_frameworks/utils.py +91 -0
  90. sf_veritas/print_override.py +13 -0
  91. sf_veritas/regular_data_transmitter.py +409 -0
  92. sf_veritas/request_interceptor.py +401 -0
  93. sf_veritas/request_utils.py +550 -0
  94. sf_veritas/server_status.py +1 -0
  95. sf_veritas/shutdown_flag.py +11 -0
  96. sf_veritas/subprocess_startup.py +3 -0
  97. sf_veritas/test_cli.py +145 -0
  98. sf_veritas/thread_local.py +970 -0
  99. sf_veritas/timeutil.py +114 -0
  100. sf_veritas/transmit_exception_to_sailfish.py +28 -0
  101. sf_veritas/transmitter.py +132 -0
  102. sf_veritas/types.py +47 -0
  103. sf_veritas/unified_interceptor.py +1580 -0
  104. sf_veritas/utils.py +39 -0
  105. sf_veritas-0.10.3.dist-info/METADATA +97 -0
  106. sf_veritas-0.10.3.dist-info/RECORD +132 -0
  107. sf_veritas-0.10.3.dist-info/WHEEL +5 -0
  108. sf_veritas-0.10.3.dist-info/entry_points.txt +2 -0
  109. sf_veritas-0.10.3.dist-info/top_level.txt +1 -0
  110. sf_veritas.libs/libbrotlicommon-6ce2a53c.so.1.0.6 +0 -0
  111. sf_veritas.libs/libbrotlidec-811d1be3.so.1.0.6 +0 -0
  112. sf_veritas.libs/libcom_err-730ca923.so.2.1 +0 -0
  113. sf_veritas.libs/libcrypt-52aca757.so.1.1.0 +0 -0
  114. sf_veritas.libs/libcrypto-bdaed0ea.so.1.1.1k +0 -0
  115. sf_veritas.libs/libcurl-eaa3cf66.so.4.5.0 +0 -0
  116. sf_veritas.libs/libgssapi_krb5-323bbd21.so.2.2 +0 -0
  117. sf_veritas.libs/libidn2-2f4a5893.so.0.3.6 +0 -0
  118. sf_veritas.libs/libk5crypto-9a74ff38.so.3.1 +0 -0
  119. sf_veritas.libs/libkeyutils-2777d33d.so.1.6 +0 -0
  120. sf_veritas.libs/libkrb5-a55300e8.so.3.3 +0 -0
  121. sf_veritas.libs/libkrb5support-e6594cfc.so.0.1 +0 -0
  122. sf_veritas.libs/liblber-2-d20824ef.4.so.2.10.9 +0 -0
  123. sf_veritas.libs/libldap-2-cea2a960.4.so.2.10.9 +0 -0
  124. sf_veritas.libs/libnghttp2-39367a22.so.14.17.0 +0 -0
  125. sf_veritas.libs/libpcre2-8-516f4c9d.so.0.7.1 +0 -0
  126. sf_veritas.libs/libpsl-99becdd3.so.5.3.1 +0 -0
  127. sf_veritas.libs/libsasl2-7de4d792.so.3.0.0 +0 -0
  128. sf_veritas.libs/libselinux-d0805dcb.so.1 +0 -0
  129. sf_veritas.libs/libssh-c11d285b.so.4.8.7 +0 -0
  130. sf_veritas.libs/libssl-60250281.so.1.1.1k +0 -0
  131. sf_veritas.libs/libunistring-05abdd40.so.2.1.0 +0 -0
  132. 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
+ }