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