sf-veritas 0.10.3__cp314-cp314-manylinux_2_28_x86_64.whl

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

Potentially problematic release.


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

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