sf-veritas 0.10.3__cp312-cp312-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-312-x86_64-linux-gnu.so +0 -0
  4. sf_veritas/_sffastnet.c +924 -0
  5. sf_veritas/_sffastnet.cpython-312-x86_64-linux-gnu.so +0 -0
  6. sf_veritas/_sffastnetworkrequest.c +730 -0
  7. sf_veritas/_sffastnetworkrequest.cpython-312-x86_64-linux-gnu.so +0 -0
  8. sf_veritas/_sffuncspan.c +2155 -0
  9. sf_veritas/_sffuncspan.cpython-312-x86_64-linux-gnu.so +0 -0
  10. sf_veritas/_sffuncspan_config.c +617 -0
  11. sf_veritas/_sffuncspan_config.cpython-312-x86_64-linux-gnu.so +0 -0
  12. sf_veritas/_sfheadercheck.c +341 -0
  13. sf_veritas/_sfheadercheck.cpython-312-x86_64-linux-gnu.so +0 -0
  14. sf_veritas/_sfnetworkhop.c +1451 -0
  15. sf_veritas/_sfnetworkhop.cpython-312-x86_64-linux-gnu.so +0 -0
  16. sf_veritas/_sfservice.c +1175 -0
  17. sf_veritas/_sfservice.cpython-312-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,2155 @@
1
+ // sf_veritas/_sffuncspan.c
2
+ #define PY_SSIZE_T_CLEAN
3
+ #include <Python.h>
4
+ #include <frameobject.h>
5
+ #include <pthread.h>
6
+ #include <curl/curl.h>
7
+ #include <stdatomic.h>
8
+ #include <stdint.h>
9
+ #include <stdlib.h>
10
+ #include <string.h>
11
+ #include <time.h>
12
+ #include <sys/time.h>
13
+
14
+ // ---------- Thread-local guard flag to prevent recursive telemetry capture ----------
15
+ __attribute__((visibility("default")))
16
+ __thread int g_in_telemetry_send = 0;
17
+
18
+ // ---------- External Config System Integration ----------
19
+ // Config structure matches _sffuncspan_config.c
20
+ typedef struct {
21
+ uint8_t include_arguments;
22
+ uint8_t include_return_value;
23
+ uint8_t autocapture_all_children;
24
+ uint8_t _padding;
25
+ float sample_rate;
26
+ uint32_t arg_limit_mb;
27
+ uint32_t return_limit_mb;
28
+ uint64_t hash;
29
+ } sf_funcspan_config_t;
30
+
31
+ // Python module reference for config system
32
+ static PyObject *g_config_module = NULL;
33
+ static PyObject *g_config_get_func = NULL;
34
+
35
+ // Default config to use if config system is not available
36
+ static sf_funcspan_config_t g_fallback_config = {
37
+ .include_arguments = 1,
38
+ .include_return_value = 1,
39
+ .autocapture_all_children = 1,
40
+ ._padding = 0,
41
+ .sample_rate = 1.0f,
42
+ .arg_limit_mb = 1,
43
+ .return_limit_mb = 1,
44
+ .hash = 0
45
+ };
46
+
47
+ // Initialize config system integration (called once at init)
48
+ static void init_config_system(void) {
49
+ // Try to import _sffuncspan_config module
50
+ g_config_module = PyImport_ImportModule("sf_veritas._sffuncspan_config");
51
+ if (g_config_module) {
52
+ // Get the 'get' function
53
+ g_config_get_func = PyObject_GetAttrString(g_config_module, "get");
54
+ if (!g_config_get_func || !PyCallable_Check(g_config_get_func)) {
55
+ Py_XDECREF(g_config_get_func);
56
+ g_config_get_func = NULL;
57
+ Py_DECREF(g_config_module);
58
+ g_config_module = NULL;
59
+ fprintf(stderr, "[_sffuncspan] WARNING: Config module imported but 'get' function not found\n");
60
+ } else {
61
+ fprintf(stderr, "[_sffuncspan] Config system initialized successfully\n");
62
+ }
63
+ } else {
64
+ PyErr_Clear(); // Config module not available, use defaults
65
+ fprintf(stderr, "[_sffuncspan] WARNING: Config module not available, using defaults\n");
66
+ }
67
+ }
68
+
69
+ // Thread-local recursion guard to prevent calling Python from within config lookup
70
+ static _Thread_local int g_in_config_lookup = 0;
71
+
72
+ // Thread-local recursion guard to prevent profiling the profiler itself
73
+ static _Thread_local int g_in_profiler = 0;
74
+
75
+ // Debug counter (only log first few lookups)
76
+ static _Atomic int g_debug_lookup_count = 0;
77
+
78
+ // Simple cache for config lookups (to avoid calling Python during argument capture)
79
+ #define CONFIG_CACHE_SIZE 256
80
+ typedef struct {
81
+ uint64_t hash; // Hash of file_path:func_name
82
+ sf_funcspan_config_t config;
83
+ } config_cache_entry_t;
84
+
85
+ static config_cache_entry_t g_config_cache[CONFIG_CACHE_SIZE];
86
+ static pthread_mutex_t g_config_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
87
+
88
+ // Simple string hash function
89
+ static inline uint64_t simple_hash(const char *str1, const char *str2) {
90
+ uint64_t hash = 5381;
91
+ const unsigned char *s = (const unsigned char *)str1;
92
+ while (*s) {
93
+ hash = ((hash << 5) + hash) + *s++;
94
+ }
95
+ s = (const unsigned char *)str2;
96
+ while (*s) {
97
+ hash = ((hash << 5) + hash) + *s++;
98
+ }
99
+ return hash;
100
+ }
101
+
102
+ // Get config for a function by calling Python config system (with cache)
103
+ static inline sf_funcspan_config_t get_function_config(const char *file_path, const char *func_name) {
104
+ uint64_t func_hash, file_hash;
105
+ uint32_t func_cache_idx, file_cache_idx;
106
+ int count;
107
+ PyObject *args = NULL;
108
+ PyObject *result = NULL;
109
+ PyObject *val = NULL;
110
+ sf_funcspan_config_t config;
111
+
112
+ // First, check cache for function-specific config (exact match)
113
+ func_hash = simple_hash(file_path, func_name);
114
+ func_cache_idx = func_hash % CONFIG_CACHE_SIZE;
115
+
116
+ // Check cache first (no lock for read - this is a simple cache, not perfect but fast)
117
+ if (g_config_cache[func_cache_idx].hash == func_hash) {
118
+ count = atomic_fetch_add(&g_debug_lookup_count, 1);
119
+ if (count < 5) {
120
+ fprintf(stderr, "[_sffuncspan] CACHE HIT (func): %s::%s -> args=%d ret=%d\n",
121
+ func_name, file_path,
122
+ g_config_cache[func_cache_idx].config.include_arguments,
123
+ g_config_cache[func_cache_idx].config.include_return_value);
124
+ }
125
+ return g_config_cache[func_cache_idx].config;
126
+ }
127
+
128
+ // Second, check cache for file-level config (using "<MODULE>" as function name)
129
+ file_hash = simple_hash(file_path, "<MODULE>");
130
+ file_cache_idx = file_hash % CONFIG_CACHE_SIZE;
131
+
132
+ if (g_config_cache[file_cache_idx].hash == file_hash) {
133
+ count = atomic_fetch_add(&g_debug_lookup_count, 1);
134
+ if (count < 5) {
135
+ fprintf(stderr, "[_sffuncspan] CACHE HIT (file): %s::%s -> args=%d ret=%d\n",
136
+ func_name, file_path,
137
+ g_config_cache[file_cache_idx].config.include_arguments,
138
+ g_config_cache[file_cache_idx].config.include_return_value);
139
+ }
140
+ return g_config_cache[file_cache_idx].config;
141
+ }
142
+
143
+ // CACHE MISS - try config module (includes HTTP header overrides!)
144
+ if (g_config_get_func && !g_in_config_lookup) {
145
+ // Prevent recursion
146
+ g_in_config_lookup = 1;
147
+
148
+ args = Py_BuildValue("(ss)", file_path, func_name);
149
+ if (args) {
150
+ result = PyObject_CallObject(g_config_get_func, args);
151
+ Py_DECREF(args);
152
+
153
+ if (result && PyDict_Check(result)) {
154
+ config = g_fallback_config;
155
+
156
+ val = PyDict_GetItemString(result, "include_arguments");
157
+ if (val && PyBool_Check(val)) config.include_arguments = (val == Py_True) ? 1 : 0;
158
+
159
+ val = PyDict_GetItemString(result, "include_return_value");
160
+ if (val && PyBool_Check(val)) config.include_return_value = (val == Py_True) ? 1 : 0;
161
+
162
+ val = PyDict_GetItemString(result, "autocapture_all_children");
163
+ if (val && PyBool_Check(val)) config.autocapture_all_children = (val == Py_True) ? 1 : 0;
164
+
165
+ val = PyDict_GetItemString(result, "arg_limit_mb");
166
+ if (val && PyLong_Check(val)) config.arg_limit_mb = (uint32_t)PyLong_AsLong(val);
167
+
168
+ val = PyDict_GetItemString(result, "return_limit_mb");
169
+ if (val && PyLong_Check(val)) config.return_limit_mb = (uint32_t)PyLong_AsLong(val);
170
+
171
+ val = PyDict_GetItemString(result, "sample_rate");
172
+ if (val && PyFloat_Check(val)) config.sample_rate = (float)PyFloat_AsDouble(val);
173
+
174
+ // DON'T cache configs from _sffuncspan_config.get() because they include
175
+ // thread-local HTTP header overrides that change per-request.
176
+ // Only cache configs that were pre-populated via cache_config().
177
+
178
+ Py_DECREF(result);
179
+ g_in_config_lookup = 0;
180
+ return config;
181
+ }
182
+
183
+ Py_XDECREF(result);
184
+ if (PyErr_Occurred()) PyErr_Clear();
185
+ }
186
+ g_in_config_lookup = 0;
187
+ }
188
+
189
+ // Fallback to defaults
190
+ count = atomic_fetch_add(&g_debug_lookup_count, 1);
191
+ if (count < 5) {
192
+ fprintf(stderr, "[_sffuncspan] CACHE MISS: %s::%s - using fallback config\n", func_name, file_path);
193
+ }
194
+ return g_fallback_config;
195
+ }
196
+
197
+ // Compatibility for Python 3.8
198
+ #if PY_VERSION_HEX < 0x03090000 // Python < 3.9
199
+ static inline PyCodeObject* PyFrame_GetCode(PyFrameObject *frame) {
200
+ PyCodeObject *code = frame->f_code;
201
+ Py_INCREF(code);
202
+ return code;
203
+ }
204
+ #endif
205
+
206
+ // ---------- Ring buffer ----------
207
+ #ifndef SFFS_RING_CAP
208
+ #define SFFS_RING_CAP 524288 // 512K slots for high-throughput (was 64K)
209
+ #endif
210
+
211
+ typedef struct {
212
+ char *body; // malloc'd HTTP JSON body
213
+ size_t len;
214
+ } sffs_msg_t;
215
+
216
+ static sffs_msg_t *g_ring = NULL;
217
+ static size_t g_cap = 0;
218
+ static _Atomic size_t g_head = 0; // consumer
219
+ static _Atomic size_t g_tail = 0; // producer
220
+
221
+ // tiny spinlock to make push MPMC-safe enough for Python producers
222
+ static atomic_flag g_push_lock = ATOMIC_FLAG_INIT;
223
+
224
+ // wake/sleep
225
+ static pthread_mutex_t g_cv_mtx = PTHREAD_MUTEX_INITIALIZER;
226
+ static pthread_cond_t g_cv = PTHREAD_COND_INITIALIZER;
227
+ static _Atomic int g_running = 0;
228
+
229
+ // Thread pool for concurrent senders (configurable via SF_FUNCSPAN_SENDER_THREADS)
230
+ #define MAX_SENDER_THREADS 16
231
+ static pthread_t g_sender_threads[MAX_SENDER_THREADS];
232
+ static int g_num_sender_threads = 0;
233
+
234
+ // curl state - per-thread handles for concurrent HTTP requests
235
+ __thread CURL *g_telem_curl = NULL;
236
+ static struct curl_slist *g_hdrs = NULL;
237
+
238
+ // config (owned strings)
239
+ static char *g_url = NULL;
240
+ static char *g_func_span_query_escaped = NULL;
241
+ static char *g_json_prefix_func_span = NULL;
242
+ static char *g_api_key = NULL;
243
+ static char *g_service_uuid = NULL;
244
+ static char *g_library = NULL;
245
+ static char *g_version = NULL;
246
+ static int g_http2 = 0;
247
+
248
+ // Function span configuration
249
+ static size_t g_variable_capture_size_limit_bytes = 1048576; // 1MB default
250
+ static PyObject *g_capture_from_installed_libraries = NULL; // list of strings or NULL
251
+
252
+ // Sampling configuration for ultra-low overhead
253
+ static _Atomic uint64_t g_sample_counter = 0;
254
+ static uint64_t g_sample_rate = 1; // 1 = capture all, 100 = capture 1/100, 10000 = capture 1/10000
255
+ static int g_enable_sampling = 0; // 0 = disabled (capture all by default), 1 = enabled
256
+
257
+ // Master kill switch from SF_ENABLE_FUNCTION_SPANS env var (default: TRUE)
258
+ // When disabled, profiler hooks run but skip ALL expensive work (config, capture, transmission)
259
+ // NOTHING can override this (not headers, not decorators, nothing)
260
+ static int g_enable_function_spans = 1; // Default: enabled
261
+
262
+ // Debug flag from environment (set in py_init)
263
+ static int SF_DEBUG = 0;
264
+
265
+ // Serialization configuration
266
+ static int g_parse_json_strings = 1; // 1 = auto-parse JSON strings, 0 = keep as strings
267
+
268
+ // Capture control - granular configuration
269
+ static int g_capture_arguments = 1; // 1 = capture arguments, 0 = skip
270
+ static int g_capture_return_value = 1; // 1 = capture return value, 0 = skip
271
+ static size_t g_arg_limit_bytes = 1048576; // 1MB default for arguments
272
+ static size_t g_return_limit_bytes = 1048576; // 1MB default for return values
273
+
274
+ // Django view function filtering
275
+ static int g_include_django_view_functions = 0; // 0 = skip Django view functions (default), 1 = include them
276
+
277
+ // Installed packages filtering - controlled by SF_FUNCSPAN_CAPTURE_INSTALLED_PACKAGES
278
+ static int g_capture_installed_packages = 0; // 0 = skip site-packages/stdlib (default), 1 = capture them
279
+
280
+ // SF Veritas self-capture - controlled by SF_FUNCSPAN_CAPTURE_SF_VERITAS
281
+ static int g_capture_sf_veritas = 0; // 0 = skip sf_veritas (default), 1 = capture our own telemetry code
282
+
283
+ // Performance monitoring
284
+ static _Atomic uint64_t g_spans_recorded = 0;
285
+ static _Atomic uint64_t g_spans_sampled_out = 0;
286
+ static _Atomic uint64_t g_spans_dropped = 0;
287
+
288
+ static const char *JSON_SUFFIX = "}}";
289
+
290
+ // Span ID management - thread-local storage for span stack
291
+ static pthread_key_t g_span_stack_key;
292
+ static pthread_once_t g_span_stack_key_once = PTHREAD_ONCE_INIT;
293
+
294
+ // Span ID counter
295
+ static _Atomic uint64_t g_span_id_counter = 0;
296
+
297
+ // Span stack entry
298
+ typedef struct span_entry {
299
+ char *span_id;
300
+ struct span_entry *next;
301
+ } span_entry_t;
302
+
303
+ // ---------- Helpers for epoch nanoseconds ----------
304
+ static inline uint64_t now_epoch_ns(void) {
305
+ struct timespec ts;
306
+ clock_gettime(CLOCK_REALTIME, &ts);
307
+ return ((uint64_t)ts.tv_sec) * 1000000000ULL + (uint64_t)ts.tv_nsec;
308
+ }
309
+
310
+ static inline uint64_t now_ms(void) {
311
+ #if defined(CLOCK_REALTIME_COARSE)
312
+ struct timespec ts;
313
+ clock_gettime(CLOCK_REALTIME_COARSE, &ts);
314
+ return ((uint64_t)ts.tv_sec) * 1000ULL + (uint64_t)(ts.tv_nsec / 1000000ULL);
315
+ #else
316
+ struct timeval tv;
317
+ gettimeofday(&tv, NULL);
318
+ return ((uint64_t)tv.tv_sec) * 1000ULL + (uint64_t)(tv.tv_usec / 1000ULL);
319
+ #endif
320
+ }
321
+
322
+ static char *str_dup(const char *s) {
323
+ size_t n = strlen(s);
324
+ char *p = (char*)malloc(n + 1);
325
+ if (!p) return NULL;
326
+ memcpy(p, s, n);
327
+ p[n] = 0;
328
+ return p;
329
+ }
330
+
331
+ // escape for generic JSON string fields
332
+ static char *json_escape(const char *s) {
333
+ const unsigned char *in = (const unsigned char*)s;
334
+ size_t extra = 0;
335
+ for (const unsigned char *p = in; *p; ++p) {
336
+ switch (*p) {
337
+ case '\\': case '"': extra++; break;
338
+ default:
339
+ if (*p < 0x20) extra += 5; // \u00XX
340
+ }
341
+ }
342
+ size_t inlen = strlen(s);
343
+ char *out = (char*)malloc(inlen + extra + 1);
344
+ if (!out) return NULL;
345
+
346
+ char *o = out;
347
+ for (const unsigned char *p = in; *p; ++p) {
348
+ switch (*p) {
349
+ case '\\': *o++='\\'; *o++='\\'; break;
350
+ case '"': *o++='\\'; *o++='"'; break;
351
+ default:
352
+ if (*p < 0x20) {
353
+ static const char hex[] = "0123456789abcdef";
354
+ *o++='\\'; *o++='u'; *o++='0'; *o++='0';
355
+ *o++=hex[(*p)>>4]; *o++=hex[(*p)&0xF];
356
+ } else {
357
+ *o++ = (char)*p;
358
+ }
359
+ }
360
+ }
361
+ *o = 0;
362
+ return out;
363
+ }
364
+
365
+ // escape for the GraphQL "query" string (handle \n, \r, \t too)
366
+ static char *json_escape_query(const char *s) {
367
+ const unsigned char *in = (const unsigned char*)s;
368
+ size_t extra = 0;
369
+ for (const unsigned char *p = in; *p; ++p) {
370
+ switch (*p) {
371
+ case '\\': case '"': case '\n': case '\r': case '\t': extra++; break;
372
+ default: break;
373
+ }
374
+ }
375
+ size_t inlen = strlen(s);
376
+ char *out = (char*)malloc(inlen + extra + 1);
377
+ if (!out) return NULL;
378
+ char *o = out;
379
+ for (const unsigned char *p = in; *p; ++p) {
380
+ switch (*p) {
381
+ case '\\': *o++='\\'; *o++='\\'; break;
382
+ case '"': *o++='\\'; *o++='"'; break;
383
+ case '\n': *o++='\\'; *o++='n'; break;
384
+ case '\r': *o++='\\'; *o++='r'; break;
385
+ case '\t': *o++='\\'; *o++='t'; break;
386
+ default: *o++=(char)*p;
387
+ }
388
+ }
389
+ *o=0;
390
+ return out;
391
+ }
392
+
393
+ // generic prefix builder for a given escaped query
394
+ static int build_prefix_for_query(const char *query_escaped, char **out_prefix) {
395
+ const char *p1 = "{\"query\":\"";
396
+ const char *p2 = "\",\"variables\":{";
397
+ const char *k1 = "\"apiKey\":\"";
398
+ const char *k2 = "\",\"serviceUuid\":\"";
399
+ const char *k3 = "\",\"library\":\"";
400
+ const char *k4 = "\",\"version\":\"";
401
+
402
+ size_t n = strlen(p1) + strlen(query_escaped) + strlen(p2)
403
+ + strlen(k1) + strlen(g_api_key)
404
+ + strlen(k2) + strlen(g_service_uuid)
405
+ + strlen(k3) + strlen(g_library)
406
+ + strlen(k4) + strlen(g_version) + 5;
407
+
408
+ char *prefix = (char*)malloc(n);
409
+ if (!prefix) return 0;
410
+
411
+ char *o = prefix;
412
+ o += sprintf(o, "%s%s%s", p1, query_escaped, p2);
413
+ o += sprintf(o, "%s%s", k1, g_api_key);
414
+ o += sprintf(o, "%s%s", k2, g_service_uuid);
415
+ o += sprintf(o, "%s%s", k3, g_library);
416
+ o += sprintf(o, "%s%s\"", k4, g_version);
417
+ *o = '\0';
418
+
419
+ *out_prefix = prefix;
420
+ return 1;
421
+ }
422
+
423
+ // ---------- Span stack management ----------
424
+ static void init_span_stack_key(void) {
425
+ pthread_key_create(&g_span_stack_key, NULL);
426
+ }
427
+
428
+ static span_entry_t* get_span_stack(void) {
429
+ pthread_once(&g_span_stack_key_once, init_span_stack_key);
430
+ return (span_entry_t*)pthread_getspecific(g_span_stack_key);
431
+ }
432
+
433
+ static void set_span_stack(span_entry_t *stack) {
434
+ pthread_once(&g_span_stack_key_once, init_span_stack_key);
435
+ pthread_setspecific(g_span_stack_key, stack);
436
+ }
437
+
438
+ static char* generate_span_id(void) {
439
+ uint64_t id = atomic_fetch_add(&g_span_id_counter, 1);
440
+ char *span_id = (char*)malloc(32);
441
+ if (!span_id) return NULL;
442
+ snprintf(span_id, 32, "span-%llu", (unsigned long long)id);
443
+ return span_id;
444
+ }
445
+
446
+ static void push_span(const char *span_id) {
447
+ span_entry_t *entry = (span_entry_t*)malloc(sizeof(span_entry_t));
448
+ if (!entry) return;
449
+ entry->span_id = str_dup(span_id);
450
+ entry->next = get_span_stack();
451
+ set_span_stack(entry);
452
+ }
453
+
454
+ static char* pop_span(void) {
455
+ span_entry_t *stack = get_span_stack();
456
+ if (!stack) return NULL;
457
+ char *span_id = stack->span_id;
458
+ span_entry_t *next = stack->next;
459
+ free(stack);
460
+ set_span_stack(next);
461
+ return span_id;
462
+ }
463
+
464
+ static char* peek_parent_span_id(void) {
465
+ span_entry_t *stack = get_span_stack();
466
+ if (!stack) return NULL;
467
+ return stack->span_id ? str_dup(stack->span_id) : NULL;
468
+ }
469
+
470
+ // ---------- Build function span body ----------
471
+ static int build_body_func_span(
472
+ const char *session_id,
473
+ const char *span_id,
474
+ const char *parent_span_id,
475
+ const char *file_path,
476
+ int line_number,
477
+ int column_number,
478
+ const char *function_name,
479
+ const char *arguments_json,
480
+ const char *return_value_json,
481
+ uint64_t start_time_ns,
482
+ uint64_t duration_ns,
483
+ char **out_body,
484
+ size_t *out_len
485
+ ) {
486
+ // Escape all string fields
487
+ char *sid_esc = json_escape(session_id ? session_id : "");
488
+ char *spanid_esc = json_escape(span_id ? span_id : "");
489
+ char *pspanid_esc = parent_span_id ? json_escape(parent_span_id) : NULL;
490
+ char *file_esc = json_escape(file_path ? file_path : "");
491
+ char *func_esc = json_escape(function_name ? function_name : "");
492
+ char *args_esc = json_escape(arguments_json ? arguments_json : "{}");
493
+ char *ret_esc = return_value_json ? json_escape(return_value_json) : NULL;
494
+
495
+ if (!sid_esc || !spanid_esc || !file_esc || !func_esc || !args_esc) {
496
+ free(sid_esc); free(spanid_esc); free(pspanid_esc); free(file_esc);
497
+ free(func_esc); free(args_esc); free(ret_esc);
498
+ return 0;
499
+ }
500
+
501
+ uint64_t tms = now_ms();
502
+ const char *k_sid = ",\"sessionId\":\"";
503
+ const char *k_spanid = ",\"spanId\":\"";
504
+ const char *k_pspanid = ",\"parentSpanId\":\"";
505
+ const char *k_pspanid_null = ",\"parentSpanId\":null";
506
+ const char *k_file = ",\"filePath\":\"";
507
+ const char *k_line = ",\"lineNumber\":";
508
+ const char *k_col = ",\"columnNumber\":";
509
+ const char *k_func = ",\"functionName\":\"";
510
+ const char *k_args = ",\"arguments\":\"";
511
+ const char *k_ret = ",\"returnValue\":\"";
512
+ const char *k_ret_null = ",\"returnValue\":null";
513
+ const char *k_start = ",\"startTimeNs\":";
514
+ const char *k_dur = ",\"durationNs\":";
515
+ const char *k_ts = ",\"timestampMs\":\"";
516
+
517
+ char ts_buf[32], line_buf[16], col_buf[16], start_buf[32], dur_buf[32];
518
+ snprintf(ts_buf, sizeof(ts_buf), "%llu", (unsigned long long)tms);
519
+ snprintf(line_buf, sizeof(line_buf), "%d", line_number);
520
+ snprintf(col_buf, sizeof(col_buf), "%d", column_number);
521
+ snprintf(start_buf, sizeof(start_buf), "%llu", (unsigned long long)start_time_ns);
522
+ snprintf(dur_buf, sizeof(dur_buf), "%llu", (unsigned long long)duration_ns);
523
+
524
+ if (!g_json_prefix_func_span) {
525
+ free(sid_esc); free(spanid_esc); free(pspanid_esc); free(file_esc);
526
+ free(func_esc); free(args_esc); free(ret_esc);
527
+ return 0;
528
+ }
529
+
530
+ size_t len = strlen(g_json_prefix_func_span)
531
+ + strlen(k_sid) + strlen(sid_esc)
532
+ + strlen(k_spanid) + strlen(spanid_esc)
533
+ + (pspanid_esc ? (strlen(k_pspanid) + strlen(pspanid_esc)) : strlen(k_pspanid_null))
534
+ + strlen(k_file) + strlen(file_esc)
535
+ + strlen(k_line) + strlen(line_buf)
536
+ + strlen(k_col) + strlen(col_buf)
537
+ + strlen(k_func) + strlen(func_esc)
538
+ + strlen(k_args) + strlen(args_esc)
539
+ + (ret_esc ? (strlen(k_ret) + strlen(ret_esc)) : strlen(k_ret_null))
540
+ + strlen(k_start) + strlen(start_buf)
541
+ + strlen(k_dur) + strlen(dur_buf)
542
+ + strlen(k_ts) + strlen(ts_buf) + 1 // +1 for closing quote
543
+ + strlen(JSON_SUFFIX) + 10;
544
+
545
+ char *body = (char*)malloc(len);
546
+ if (!body) {
547
+ free(sid_esc); free(spanid_esc); free(pspanid_esc); free(file_esc);
548
+ free(func_esc); free(args_esc); free(ret_esc);
549
+ return 0;
550
+ }
551
+
552
+ char *o = body;
553
+ o += sprintf(o, "%s", g_json_prefix_func_span);
554
+ o += sprintf(o, "%s%s\"", k_sid, sid_esc);
555
+ o += sprintf(o, "%s%s\"", k_spanid, spanid_esc);
556
+ if (pspanid_esc) {
557
+ o += sprintf(o, "%s%s\"", k_pspanid, pspanid_esc);
558
+ } else {
559
+ o += sprintf(o, "%s", k_pspanid_null);
560
+ }
561
+ o += sprintf(o, "%s%s\"", k_file, file_esc);
562
+ o += sprintf(o, "%s%s", k_line, line_buf);
563
+ o += sprintf(o, "%s%s", k_col, col_buf);
564
+ o += sprintf(o, "%s%s\"", k_func, func_esc);
565
+ o += sprintf(o, "%s%s\"", k_args, args_esc);
566
+ if (ret_esc) {
567
+ o += sprintf(o, "%s%s\"", k_ret, ret_esc);
568
+ } else {
569
+ o += sprintf(o, "%s", k_ret_null);
570
+ }
571
+ o += sprintf(o, "%s%s", k_start, start_buf);
572
+ o += sprintf(o, "%s%s", k_dur, dur_buf);
573
+ o += sprintf(o, "%s%s\"", k_ts, ts_buf);
574
+ o += sprintf(o, "%s", JSON_SUFFIX);
575
+ *o = '\0';
576
+
577
+ *out_body = body;
578
+ *out_len = (size_t)(o - body);
579
+
580
+ free(sid_esc); free(spanid_esc); free(pspanid_esc); free(file_esc);
581
+ free(func_esc); free(args_esc); free(ret_esc);
582
+ return 1;
583
+ }
584
+
585
+ // ---------- Sampling ----------
586
+ static inline int should_sample(void) {
587
+ if (!g_enable_sampling) {
588
+ return 1; // Sampling disabled, capture all
589
+ }
590
+
591
+ // Fast path: atomic increment and modulo check
592
+ uint64_t counter = atomic_fetch_add(&g_sample_counter, 1);
593
+ if (counter % g_sample_rate == 0) {
594
+ return 1; // This one gets sampled
595
+ }
596
+
597
+ atomic_fetch_add(&g_spans_sampled_out, 1);
598
+ return 0; // Skip this one
599
+ }
600
+
601
+ // ---------- ring ops ----------
602
+ static inline size_t ring_count(void) {
603
+ size_t h = atomic_load_explicit(&g_head, memory_order_acquire);
604
+ size_t t = atomic_load_explicit(&g_tail, memory_order_acquire);
605
+ return t - h;
606
+ }
607
+ static inline int ring_empty(void) { return ring_count() == 0; }
608
+
609
+ static int ring_push(char *body, size_t len) {
610
+ while (atomic_flag_test_and_set_explicit(&g_push_lock, memory_order_acquire)) {
611
+ // brief spin
612
+ }
613
+ size_t t = atomic_load_explicit(&g_tail, memory_order_relaxed);
614
+ size_t h = atomic_load_explicit(&g_head, memory_order_acquire);
615
+ if ((t - h) >= g_cap) {
616
+ atomic_flag_clear_explicit(&g_push_lock, memory_order_release);
617
+ atomic_fetch_add(&g_spans_dropped, 1); // Track dropped spans
618
+ return 0; // full (drop)
619
+ }
620
+ size_t idx = t % g_cap;
621
+ g_ring[idx].body = body;
622
+ g_ring[idx].len = len;
623
+ atomic_store_explicit(&g_tail, t + 1, memory_order_release);
624
+ atomic_flag_clear_explicit(&g_push_lock, memory_order_release);
625
+
626
+ atomic_fetch_add(&g_spans_recorded, 1); // Track recorded spans
627
+
628
+ pthread_mutex_lock(&g_cv_mtx);
629
+ pthread_cond_signal(&g_cv);
630
+ pthread_mutex_unlock(&g_cv_mtx);
631
+ return 1;
632
+ }
633
+
634
+ static int ring_pop(char **body, size_t *len) {
635
+ size_t h = atomic_load_explicit(&g_head, memory_order_relaxed);
636
+ size_t t = atomic_load_explicit(&g_tail, memory_order_acquire);
637
+ if (h == t) return 0;
638
+ size_t idx = h % g_cap;
639
+ *body = g_ring[idx].body;
640
+ *len = g_ring[idx].len;
641
+ g_ring[idx].body = NULL;
642
+ g_ring[idx].len = 0;
643
+ atomic_store_explicit(&g_head, h + 1, memory_order_release);
644
+ return 1;
645
+ }
646
+
647
+ // ---------- curl sink callbacks ----------
648
+ static size_t _sink_write(char *ptr, size_t size, size_t nmemb, void *userdata) {
649
+ (void)ptr; (void)userdata;
650
+ return size * nmemb;
651
+ }
652
+ static size_t _sink_header(char *ptr, size_t size, size_t nmemb, void *userdata) {
653
+ (void)ptr; (void)userdata;
654
+ return size * nmemb;
655
+ }
656
+
657
+ // ---------- pthread cleanup handler for sender threads ----------
658
+ static void sender_cleanup(void *arg) {
659
+ (void)arg;
660
+ if (g_telem_curl) {
661
+ curl_easy_cleanup(g_telem_curl);
662
+ g_telem_curl = NULL;
663
+ }
664
+ }
665
+
666
+ // ---------- sender thread ----------
667
+ static void *sender_main(void *arg) {
668
+ (void)arg;
669
+
670
+ // CRITICAL: Set thread-local guard flag to prevent recursive capture
671
+ g_in_telemetry_send = 1;
672
+
673
+ // Initialize per-thread curl handle
674
+ g_telem_curl = curl_easy_init();
675
+ if (!g_telem_curl) return NULL;
676
+
677
+ // Configure per-thread curl handle
678
+ curl_easy_setopt(g_telem_curl, CURLOPT_URL, g_url);
679
+ curl_easy_setopt(g_telem_curl, CURLOPT_TCP_KEEPALIVE, 1L);
680
+ curl_easy_setopt(g_telem_curl, CURLOPT_TCP_NODELAY, 1L); // NEW: Disable Nagle for immediate sends
681
+ curl_easy_setopt(g_telem_curl, CURLOPT_HTTPHEADER, g_hdrs);
682
+ #ifdef CURL_HTTP_VERSION_2TLS
683
+ if (g_http2) {
684
+ curl_easy_setopt(g_telem_curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS);
685
+ }
686
+ #endif
687
+ curl_easy_setopt(g_telem_curl, CURLOPT_WRITEFUNCTION, _sink_write);
688
+ curl_easy_setopt(g_telem_curl, CURLOPT_HEADERFUNCTION, _sink_header);
689
+
690
+ // Register cleanup handler
691
+ pthread_cleanup_push(sender_cleanup, NULL);
692
+
693
+ while (atomic_load(&g_running)) {
694
+ if (ring_empty()) {
695
+ pthread_mutex_lock(&g_cv_mtx);
696
+ if (ring_empty() && atomic_load(&g_running))
697
+ pthread_cond_wait(&g_cv, &g_cv_mtx);
698
+ pthread_mutex_unlock(&g_cv_mtx);
699
+ if (!atomic_load(&g_running)) break;
700
+ }
701
+ char *body = NULL; size_t len = 0;
702
+ while (ring_pop(&body, &len)) {
703
+ if (!body) continue;
704
+ curl_easy_setopt(g_telem_curl, CURLOPT_POSTFIELDS, body);
705
+ curl_easy_setopt(g_telem_curl, CURLOPT_POSTFIELDSIZE, (long)len);
706
+ (void)curl_easy_perform(g_telem_curl); // fire-and-forget
707
+ free(body);
708
+ if (!atomic_load(&g_running)) break;
709
+ }
710
+ }
711
+
712
+ pthread_cleanup_pop(1);
713
+ return NULL;
714
+ }
715
+
716
+ // Forward declaration of serialization function (defined later)
717
+ static char* serialize_python_object_to_json(PyObject *value, size_t max_size);
718
+
719
+ // ---------- Ultra-fast C profiler hook ----------
720
+ // Thread-local storage for call stack (minimal overhead)
721
+ typedef struct call_frame {
722
+ uint64_t start_ns;
723
+ char *span_id;
724
+ char *function_name; // OWNED (str_dup'd from PyUnicode_AsUTF8), must free
725
+ char *file_path; // OWNED (str_dup'd from PyUnicode_AsUTF8), must free
726
+ int line_number;
727
+ char *arguments_json; // Owned, must free if not NULL
728
+ PyFrameObject *frame; // Borrowed reference for argument capture
729
+ sf_funcspan_config_t config; // Config looked up during CALL, reused during RETURN
730
+ struct call_frame *parent;
731
+ } call_frame_t;
732
+
733
+ static pthread_key_t g_call_stack_key;
734
+ static pthread_once_t g_call_stack_key_once = PTHREAD_ONCE_INIT;
735
+
736
+ static void init_call_stack_key(void) {
737
+ pthread_key_create(&g_call_stack_key, NULL);
738
+ }
739
+
740
+ static call_frame_t* get_call_stack(void) {
741
+ pthread_once(&g_call_stack_key_once, init_call_stack_key);
742
+ return (call_frame_t*)pthread_getspecific(g_call_stack_key);
743
+ }
744
+
745
+ static void set_call_stack(call_frame_t *stack) {
746
+ pthread_once(&g_call_stack_key_once, init_call_stack_key);
747
+ pthread_setspecific(g_call_stack_key, stack);
748
+ }
749
+
750
+ // Debug counter for argument capture
751
+ static _Atomic int g_debug_arg_capture_count = 0;
752
+
753
+ // Capture function arguments from a frame (ultra-fast C implementation)
754
+ static char* capture_arguments_from_frame(PyFrameObject *frame) {
755
+ int debug_count = atomic_fetch_add(&g_debug_arg_capture_count, 1);
756
+ int should_debug = (debug_count < 5);
757
+
758
+ if (!frame) {
759
+ if (should_debug) fprintf(stderr, "[_sffuncspan] capture_args: frame is NULL\n");
760
+ return str_dup("{}");
761
+ }
762
+
763
+ PyCodeObject *code = PyFrame_GetCode(frame);
764
+ if (!code) {
765
+ if (should_debug) fprintf(stderr, "[_sffuncspan] capture_args: code is NULL\n");
766
+ return str_dup("{}");
767
+ }
768
+
769
+ // Get argument count
770
+ int arg_count = code->co_argcount + code->co_kwonlyargcount;
771
+ if (should_debug) {
772
+ fprintf(stderr, "[_sffuncspan] capture_args: arg_count=%d\n", arg_count);
773
+ }
774
+
775
+ if (arg_count == 0) {
776
+ Py_DECREF(code);
777
+ if (should_debug) fprintf(stderr, "[_sffuncspan] capture_args: arg_count is 0, returning {}\n");
778
+ return str_dup("{}");
779
+ }
780
+
781
+ // Build arguments dict as JSON
782
+ size_t buf_size = 4096;
783
+ char *buf = (char*)malloc(buf_size);
784
+ if (!buf) {
785
+ Py_DECREF(code);
786
+ return str_dup("{}");
787
+ }
788
+
789
+ size_t pos = 0;
790
+ buf[pos++] = '{';
791
+ int added = 0;
792
+
793
+ // Get frame locals - CRITICAL: must call PyFrame_FastToLocals first to populate the dict!
794
+ #if PY_VERSION_HEX >= 0x030B0000 // Python 3.11+
795
+ PyObject *locals = PyFrame_GetLocals(frame);
796
+ #else
797
+ // For Python < 3.11, we must explicitly populate f_locals from the fast locals array
798
+ PyFrame_FastToLocals(frame);
799
+ PyObject *locals = frame->f_locals;
800
+ Py_XINCREF(locals);
801
+ #endif
802
+ if (!locals) {
803
+ Py_DECREF(code);
804
+ free(buf);
805
+ if (should_debug) fprintf(stderr, "[_sffuncspan] capture_args: locals is NULL\n");
806
+ return str_dup("{}");
807
+ }
808
+
809
+ if (should_debug) {
810
+ Py_ssize_t dict_size = PyDict_Check(locals) ? PyDict_Size(locals) : -1;
811
+ fprintf(stderr, "[_sffuncspan] capture_args: locals dict size=%zd\n", dict_size);
812
+ }
813
+
814
+ // Get variable names
815
+ #if PY_VERSION_HEX >= 0x030B0000 // Python 3.11+
816
+ PyObject *co_varnames = PyCode_GetVarnames(code);
817
+ #else
818
+ PyObject *co_varnames = code->co_varnames;
819
+ Py_XINCREF(co_varnames);
820
+ #endif
821
+ if (!co_varnames) {
822
+ Py_DECREF(locals);
823
+ Py_DECREF(code);
824
+ free(buf);
825
+ return str_dup("{}");
826
+ }
827
+
828
+ // Iterate through argument names
829
+ for (int i = 0; i < arg_count && i < PyTuple_Size(co_varnames); i++) {
830
+ PyObject *var_name_obj = PyTuple_GetItem(co_varnames, i);
831
+ if (!var_name_obj) continue;
832
+
833
+ const char *var_name = PyUnicode_AsUTF8(var_name_obj);
834
+ if (!var_name) continue;
835
+
836
+ // Get value from locals
837
+ PyObject *value = PyDict_GetItemString(locals, var_name);
838
+ if (!value) continue;
839
+
840
+ // Serialize value
841
+ char *value_json = serialize_python_object_to_json(value, g_arg_limit_bytes / (arg_count > 0 ? arg_count : 1));
842
+ if (!value_json) continue;
843
+
844
+ // Escape variable name
845
+ char *var_name_esc = json_escape(var_name);
846
+ if (!var_name_esc) {
847
+ free(value_json);
848
+ continue;
849
+ }
850
+
851
+ // Check buffer space
852
+ size_t needed = strlen(var_name_esc) + strlen(value_json) + 10;
853
+ if (pos + needed >= buf_size - 10) {
854
+ free(var_name_esc);
855
+ free(value_json);
856
+ break;
857
+ }
858
+
859
+ // Add to JSON
860
+ if (added > 0) buf[pos++] = ',';
861
+ buf[pos++] = '"';
862
+ size_t name_len = strlen(var_name_esc);
863
+ memcpy(buf + pos, var_name_esc, name_len);
864
+ pos += name_len;
865
+ buf[pos++] = '"';
866
+ buf[pos++] = ':';
867
+ size_t val_len = strlen(value_json);
868
+ memcpy(buf + pos, value_json, val_len);
869
+ pos += val_len;
870
+
871
+ free(var_name_esc);
872
+ free(value_json);
873
+ added++;
874
+ }
875
+
876
+ buf[pos++] = '}';
877
+ buf[pos] = '\0';
878
+
879
+ Py_DECREF(co_varnames);
880
+ Py_DECREF(locals);
881
+ Py_DECREF(code);
882
+
883
+ return buf;
884
+ }
885
+
886
+ // Debug counter for profiler callbacks
887
+ static _Atomic int g_profiler_call_count = 0;
888
+
889
+ // Debug counter for accepted functions
890
+ static _Atomic int g_debug_accepted_count = 0;
891
+
892
+ // Profiler ready flag - set to 1 after PyEval_SetProfile() completes successfully
893
+ // This prevents profiling during profiler installation (when Python may call our
894
+ // profiler for frames already on the stack, including sf_veritas initialization code)
895
+ static _Atomic int g_profiler_ready = 0;
896
+
897
+ // Interceptors ready flag - set to 1 after setup_interceptors() completes
898
+ // This prevents profiling during interceptor initialization (which can cause crashes)
899
+ static _Atomic int g_interceptors_ready = 0;
900
+
901
+ // Fast C profiler callback - this replaces the Python _profile_callback
902
+ static int c_profile_func(PyObject *obj, PyFrameObject *frame, int what, PyObject *arg) {
903
+ (void)obj;
904
+ (void)arg;
905
+
906
+ // CRITICAL: Recursion guard - prevent profiling the profiler itself!
907
+ // This prevents infinite recursion when capturing sf_veritas code or calling Python from C
908
+ if (g_in_profiler) {
909
+ return 0;
910
+ }
911
+ g_in_profiler = 1;
912
+
913
+ // CRITICAL: Defensive NULL checks first!
914
+ if (!frame) {
915
+ g_in_profiler = 0;
916
+ return 0;
917
+ }
918
+ if (!g_running) {
919
+ g_in_profiler = 0;
920
+ return 0;
921
+ }
922
+
923
+ // DEBUG: Log first few calls
924
+ int call_count = atomic_fetch_add(&g_profiler_call_count, 1);
925
+ if (call_count < 5) {
926
+ fprintf(stderr, "[_sffuncspan] PROFILER CALLED #%d, what=%d\n", call_count, what);
927
+ fflush(stderr);
928
+ }
929
+
930
+ // PROFILER READY CHECK: Skip all calls until profiler is fully initialized
931
+ // This prevents profiling during PyEval_SetProfile() installation, when Python may
932
+ // call our profiler for frames already on the stack (including sf_veritas code).
933
+ if (!atomic_load(&g_profiler_ready)) {
934
+ if (SF_DEBUG && call_count < 5) {
935
+ fprintf(stderr, "[_sffuncspan] PROFILER_NOT_READY: Skipping call during initialization\n");
936
+ fflush(stderr);
937
+ }
938
+ g_in_profiler = 0;
939
+ return 0;
940
+ }
941
+
942
+ // INTERCEPTORS READY CHECK: Skip all calls until interceptors are fully set up
943
+ // This prevents profiling during setup_interceptors(), which can cause crashes
944
+ // when profiling code that's in an inconsistent state during initialization.
945
+ if (!atomic_load(&g_interceptors_ready)) {
946
+ if (SF_DEBUG && call_count < 5) {
947
+ fprintf(stderr, "[_sffuncspan] INTERCEPTORS_NOT_READY: Skipping call during interceptor setup\n");
948
+ fflush(stderr);
949
+ }
950
+ g_in_profiler = 0;
951
+ return 0;
952
+ }
953
+
954
+ // Fast sampling check - exit immediately if not capturing
955
+ if (g_enable_sampling && !should_sample()) {
956
+ g_in_profiler = 0;
957
+ return 0;
958
+ }
959
+
960
+ // MASTER KILL SWITCH: If SF_ENABLE_FUNCTION_SPANS=false, hard disable
961
+ // Profiler hooks still run (lightweight), but skip ALL expensive work:
962
+ // - No config lookup
963
+ // - No argument/return serialization
964
+ // - No libcurl transmission
965
+ // NOTHING can override this (not headers, not decorators, nothing)
966
+ if (!g_enable_function_spans) {
967
+ g_in_profiler = 0;
968
+ return 0;
969
+ }
970
+
971
+ if (call_count < 5) {
972
+ fprintf(stderr, "[_sffuncspan] DEBUG: Getting code object\n");
973
+ fflush(stderr);
974
+ }
975
+
976
+ PyCodeObject *code = PyFrame_GetCode(frame);
977
+ if (!code) {
978
+ g_in_profiler = 0;
979
+ return 0;
980
+ }
981
+
982
+ if (call_count < 5) {
983
+ fprintf(stderr, "[_sffuncspan] DEBUG: Got code object\n");
984
+ fflush(stderr);
985
+ }
986
+
987
+ // DEFENSIVE: Check for NULL before calling PyUnicode_AsUTF8
988
+ if (!code->co_filename || !code->co_name) {
989
+ Py_DECREF(code);
990
+ g_in_profiler = 0;
991
+ return 0;
992
+ }
993
+
994
+ if (call_count < 5) {
995
+ fprintf(stderr, "[_sffuncspan] DEBUG: About to call PyUnicode_AsUTF8\n");
996
+ fflush(stderr);
997
+ }
998
+
999
+ const char *filename = PyUnicode_AsUTF8(code->co_filename);
1000
+ const char *funcname = PyUnicode_AsUTF8(code->co_name);
1001
+
1002
+ if (call_count < 5) {
1003
+ fprintf(stderr, "[_sffuncspan] DEBUG: Got filename=%s, funcname=%s\n",
1004
+ filename ? filename : "NULL", funcname ? funcname : "NULL");
1005
+ fflush(stderr);
1006
+ }
1007
+
1008
+ // Fast path: Skip if no filename/funcname
1009
+ if (!filename || !funcname) {
1010
+ Py_DECREF(code);
1011
+ g_in_profiler = 0;
1012
+ return 0;
1013
+ }
1014
+
1015
+ if (call_count < 5) {
1016
+ fprintf(stderr, "[_sffuncspan] DEBUG: About to check dunder methods\n");
1017
+ fflush(stderr);
1018
+ }
1019
+
1020
+ // Fast path: Skip dunder methods
1021
+ if (funcname[0] == '_' && funcname[1] == '_') {
1022
+ // if (SF_DEBUG) {
1023
+ // fprintf(stderr, "[_sffuncspan] FILTERED: %s::%s - dunder method\n", filename, funcname);
1024
+ // fflush(stderr);
1025
+ // }
1026
+ Py_DECREF(code);
1027
+ g_in_profiler = 0;
1028
+ return 0;
1029
+ }
1030
+
1031
+ if (call_count < 5) {
1032
+ fprintf(stderr, "[_sffuncspan] DEBUG: About to check sf_veritas\n");
1033
+ fflush(stderr);
1034
+ }
1035
+
1036
+ // CRITICAL: Check for sf_veritas code FIRST, before Python stdlib check!
1037
+ // sf_veritas files are often in paths like /lib/python3.9/site-packages/sf_veritas/
1038
+ // which would match the stdlib filter, so we must check this first.
1039
+ int is_sf_veritas = (strstr(filename, "sf_veritas") != NULL);
1040
+
1041
+ if (call_count < 5) {
1042
+ fprintf(stderr, "[_sffuncspan] DEBUG: is_sf_veritas=%d\n", is_sf_veritas);
1043
+ fflush(stderr);
1044
+ }
1045
+
1046
+ // If it's sf_veritas and we don't want to capture it, skip early
1047
+ if (is_sf_veritas && !g_capture_sf_veritas) {
1048
+ // if (SF_DEBUG) {
1049
+ // fprintf(stderr, "[_sffuncspan] FILTERED: %s::%s - sf_veritas code (capture_sf_veritas=false)\n", filename, funcname);
1050
+ // fflush(stderr);
1051
+ // }
1052
+ Py_DECREF(code);
1053
+ g_in_profiler = 0;
1054
+ return 0;
1055
+ }
1056
+
1057
+ // Fast path: ALWAYS skip Python stdlib, frozen modules, bootstrap code
1058
+ // These are Python internals, never capture them
1059
+ // BUT: Skip this check if it's sf_veritas (already handled above)
1060
+ if (!is_sf_veritas) {
1061
+ if (strstr(filename, "/lib/python") ||
1062
+ strstr(filename, "\\lib\\python") ||
1063
+ strstr(filename, "<frozen") ||
1064
+ strstr(filename, "<string>") ||
1065
+ strstr(filename, "importlib") ||
1066
+ strstr(filename, "_bootstrap")) {
1067
+ // if (SF_DEBUG) {
1068
+ // fprintf(stderr, "[_sffuncspan] FILTERED: %s::%s - Python stdlib/internals\n", filename, funcname);
1069
+ // fflush(stderr);
1070
+ // }
1071
+ Py_DECREF(code);
1072
+ g_in_profiler = 0;
1073
+ return 0;
1074
+ }
1075
+ }
1076
+
1077
+ // Conditionally skip installed packages (site-packages)
1078
+ // BUT: if it's sf_veritas and we want to capture it, don't skip (already handled above)
1079
+ if (!g_capture_installed_packages && !is_sf_veritas) {
1080
+ if (strstr(filename, "site-packages") ||
1081
+ strstr(filename, "dist-packages")) {
1082
+ // if (SF_DEBUG) {
1083
+ // fprintf(stderr, "[_sffuncspan] FILTERED: %s::%s - site-packages (capture_installed_packages=false)\n", filename, funcname);
1084
+ // fflush(stderr);
1085
+ // }
1086
+ Py_DECREF(code);
1087
+ g_in_profiler = 0;
1088
+ return 0;
1089
+ }
1090
+ }
1091
+
1092
+ // Skip Django view functions unless explicitly enabled
1093
+ if (!g_include_django_view_functions) {
1094
+ // Check if this is a Django view function (filename ends with views.py or views/__init__.py)
1095
+ const char *views_py = strstr(filename, "views.py");
1096
+ const char *views_init = strstr(filename, "views/__init__.py");
1097
+ if (views_py || views_init) {
1098
+ // Make sure it's actually the end of the path (not just a substring)
1099
+ size_t flen = strlen(filename);
1100
+ if ((views_py && (views_py == filename + flen - 8)) || // ends with "views.py"
1101
+ (views_init && (views_init == filename + flen - 17))) { // ends with "views/__init__.py"
1102
+ // if (SF_DEBUG) {
1103
+ // fprintf(stderr, "[_sffuncspan] FILTERED: %s::%s - Django view function (include_django_view_functions=false)\n", filename, funcname);
1104
+ // fflush(stderr);
1105
+ // }
1106
+ Py_DECREF(code);
1107
+ g_in_profiler = 0;
1108
+ return 0;
1109
+ }
1110
+ }
1111
+ }
1112
+
1113
+ if (what == PyTrace_CALL) {
1114
+ // Look up config for this function
1115
+ sf_funcspan_config_t func_config = get_function_config(filename, funcname);
1116
+
1117
+ // Debug: Log when a function is ACCEPTED (passed all filters)
1118
+ if (SF_DEBUG) {
1119
+ int accepted_count = atomic_fetch_add(&g_debug_accepted_count, 1);
1120
+ if (accepted_count < 10) {
1121
+ fprintf(stderr, "[_sffuncspan] ACCEPTED: %s::%s (event=CALL, capture_installed_packages=%d, capture_sf_veritas=%d)\n",
1122
+ filename, funcname, g_capture_installed_packages, g_capture_sf_veritas);
1123
+ fflush(stderr);
1124
+ }
1125
+ }
1126
+
1127
+ // Per-function sampling check (sample_rate is 0.0-1.0)
1128
+ if (func_config.sample_rate < 1.0f) {
1129
+ // Generate a random float between 0.0 and 1.0
1130
+ static _Atomic uint64_t g_per_func_sample_counter = 0;
1131
+ uint64_t sample_val = atomic_fetch_add(&g_per_func_sample_counter, 1);
1132
+ // Simple LCG for pseudo-random: multiply by large prime, modulo 2^32
1133
+ sample_val = (sample_val * 1103515245 + 12345) & 0x7FFFFFFF;
1134
+ float rand_val = (float)sample_val / (float)0x7FFFFFFF;
1135
+
1136
+ if (rand_val >= func_config.sample_rate) {
1137
+ Py_DECREF(code);
1138
+ atomic_fetch_add(&g_spans_sampled_out, 1);
1139
+ g_in_profiler = 0;
1140
+ return 0; // Skip this span
1141
+ }
1142
+ }
1143
+
1144
+ // Push frame onto call stack (ultra-minimal allocation)
1145
+ call_frame_t *new_frame = (call_frame_t*)malloc(sizeof(call_frame_t));
1146
+ if (!new_frame) {
1147
+ Py_DECREF(code);
1148
+ g_in_profiler = 0;
1149
+ return 0;
1150
+ }
1151
+
1152
+ new_frame->start_ns = now_epoch_ns();
1153
+ new_frame->span_id = generate_span_id();
1154
+
1155
+ // CRITICAL: OWN the strings! PyUnicode_AsUTF8() returns borrowed pointers owned by the code object.
1156
+ // When we Py_DECREF(code), those pointers become invalid. Duplicate them now while code is alive.
1157
+ new_frame->function_name = funcname ? str_dup(funcname) : str_dup("<unknown>");
1158
+ new_frame->file_path = filename ? str_dup(filename) : str_dup("");
1159
+
1160
+ // Check if str_dup failed (malloc failure)
1161
+ if (!new_frame->function_name || !new_frame->file_path) {
1162
+ if (new_frame->function_name) free(new_frame->function_name);
1163
+ if (new_frame->file_path) free(new_frame->file_path);
1164
+ if (new_frame->span_id) free(new_frame->span_id);
1165
+ free(new_frame);
1166
+ Py_DECREF(code);
1167
+ g_in_profiler = 0;
1168
+ return 0;
1169
+ }
1170
+
1171
+ new_frame->line_number = PyFrame_GetLineNumber(frame);
1172
+ new_frame->frame = frame; // Borrowed! (for argument capture on return)
1173
+
1174
+ // Capture arguments NOW (on function entry) if enabled by THIS function's config
1175
+ if (func_config.include_arguments) {
1176
+ // Use per-function arg limit
1177
+ size_t arg_limit = func_config.arg_limit_mb * 1048576;
1178
+ // Temporarily set global for capture_arguments_from_frame to use
1179
+ size_t saved_limit = g_arg_limit_bytes;
1180
+ g_arg_limit_bytes = arg_limit;
1181
+ new_frame->arguments_json = capture_arguments_from_frame(frame);
1182
+ g_arg_limit_bytes = saved_limit;
1183
+ } else {
1184
+ new_frame->arguments_json = NULL;
1185
+ }
1186
+
1187
+ // Store config in frame so PyTrace_RETURN uses same config (handles HTTP header overrides)
1188
+ new_frame->config = func_config;
1189
+
1190
+ new_frame->parent = get_call_stack();
1191
+ set_call_stack(new_frame);
1192
+
1193
+ } else if (what == PyTrace_RETURN || what == PyTrace_EXCEPTION) {
1194
+ // Pop frame and record span
1195
+ call_frame_t *current = get_call_stack();
1196
+ if (!current) {
1197
+ Py_DECREF(code);
1198
+ g_in_profiler = 0;
1199
+ return 0;
1200
+ }
1201
+
1202
+ uint64_t end_ns = now_epoch_ns();
1203
+ uint64_t duration_ns = end_ns - current->start_ns;
1204
+
1205
+ // Get session ID from thread-local (fast path: use cached TLS)
1206
+ // For now, use a simple thread ID as session (avoid Python call overhead)
1207
+ char session_buf[32];
1208
+ snprintf(session_buf, sizeof(session_buf), "thread-%lu", (unsigned long)pthread_self());
1209
+
1210
+ // Get parent span ID
1211
+ const char *parent_span_id = current->parent ? current->parent->span_id : NULL;
1212
+
1213
+ // Use config stored during PyTrace_CALL (ensures consistent config even if HTTP headers change)
1214
+ sf_funcspan_config_t func_config = current->config;
1215
+
1216
+ // Capture return value if enabled by config and it's a normal return (not exception)
1217
+ char *return_value_json = NULL;
1218
+ if (func_config.include_return_value && what == PyTrace_RETURN && arg) {
1219
+ size_t return_limit = func_config.return_limit_mb * 1048576;
1220
+ return_value_json = serialize_python_object_to_json(arg, return_limit);
1221
+ }
1222
+
1223
+ // Get arguments JSON (already captured on function entry, or NULL if disabled)
1224
+ const char *arguments_json = current->arguments_json ? current->arguments_json : "{}";
1225
+
1226
+ // Build span body and push to ring - RELEASE GIL for both!
1227
+ // This is the KEY optimization from Opportunity #2
1228
+ char *body = NULL;
1229
+ size_t len = 0;
1230
+ int ok = 0;
1231
+
1232
+ // OPPORTUNITY #2 OPTIMIZATION: Release GIL during JSON build + ring push
1233
+ Py_BEGIN_ALLOW_THREADS
1234
+ if (build_body_func_span(
1235
+ session_buf,
1236
+ current->span_id,
1237
+ parent_span_id,
1238
+ current->file_path,
1239
+ current->line_number,
1240
+ 0, // column_number,
1241
+ current->function_name,
1242
+ arguments_json,
1243
+ return_value_json,
1244
+ current->start_ns,
1245
+ duration_ns,
1246
+ &body,
1247
+ &len)) {
1248
+
1249
+ // Push to ring buffer (still GIL-free)
1250
+ ok = ring_push(body, len);
1251
+ }
1252
+ Py_END_ALLOW_THREADS
1253
+
1254
+ if (!ok) {
1255
+ free(body);
1256
+ }
1257
+
1258
+ // Pop from stack
1259
+ set_call_stack(current->parent);
1260
+ free(current->span_id);
1261
+ // CRITICAL: Free owned strings (we duplicated them on CALL to prevent UAF)
1262
+ if (current->function_name) free(current->function_name);
1263
+ if (current->file_path) free(current->file_path);
1264
+ if (current->arguments_json) free(current->arguments_json);
1265
+ if (return_value_json) free(return_value_json);
1266
+ free(current);
1267
+ }
1268
+
1269
+ Py_DECREF(code);
1270
+ g_in_profiler = 0;
1271
+ return 0;
1272
+ }
1273
+
1274
+ // ---------- Python API ----------
1275
+ static PyObject *py_init(PyObject *self, PyObject *args, PyObject *kw) {
1276
+ const char *url, *query, *api_key, *service_uuid, *library, *version;
1277
+ int http2 = 0;
1278
+ static char *kwlist[] = {"url","query","api_key","service_uuid","library","version","http2", NULL};
1279
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "ssssssi",
1280
+ kwlist, &url, &query, &api_key, &service_uuid, &library, &version, &http2)) {
1281
+ Py_RETURN_FALSE;
1282
+ }
1283
+ if (g_running) Py_RETURN_TRUE;
1284
+
1285
+ g_url = str_dup(url);
1286
+ g_func_span_query_escaped = json_escape_query(query);
1287
+ g_api_key = str_dup(api_key);
1288
+ g_service_uuid = str_dup(service_uuid);
1289
+ g_library = str_dup(library);
1290
+ g_version = str_dup(version);
1291
+ g_http2 = http2 ? 1 : 0;
1292
+ if (!g_url || !g_func_span_query_escaped || !g_api_key || !g_service_uuid || !g_library || !g_version) {
1293
+ Py_RETURN_FALSE;
1294
+ }
1295
+ if (!build_prefix_for_query(g_func_span_query_escaped, &g_json_prefix_func_span)) {
1296
+ Py_RETURN_FALSE;
1297
+ }
1298
+
1299
+ g_cap = SFFS_RING_CAP;
1300
+ g_ring = (sffs_msg_t*)calloc(g_cap, sizeof(sffs_msg_t));
1301
+ if (!g_ring) { Py_RETURN_FALSE; }
1302
+
1303
+ curl_global_init(CURL_GLOBAL_DEFAULT);
1304
+ g_hdrs = NULL;
1305
+ g_hdrs = curl_slist_append(g_hdrs, "Content-Type: application/json");
1306
+
1307
+ // Initialize config system integration
1308
+ init_config_system();
1309
+
1310
+ // Initialize SF_DEBUG from environment
1311
+ const char *debug_env = getenv("SF_DEBUG");
1312
+ if (debug_env && (strcmp(debug_env, "1") == 0 || strcmp(debug_env, "true") == 0 || strcmp(debug_env, "True") == 0)) {
1313
+ SF_DEBUG = 1;
1314
+ fprintf(stderr, "[_sffuncspan] SF_DEBUG enabled\n");
1315
+ fflush(stderr);
1316
+ }
1317
+
1318
+ // Parse SF_FUNCSPAN_SENDER_THREADS environment variable (default: 4, max: 16)
1319
+ // FuncSpan expected to have HIGH volume, so default to 4 threads
1320
+ const char *num_threads_env = getenv("SF_FUNCSPAN_SENDER_THREADS");
1321
+ g_num_sender_threads = num_threads_env ? atoi(num_threads_env) : 4;
1322
+ if (g_num_sender_threads < 1) g_num_sender_threads = 1;
1323
+ if (g_num_sender_threads > MAX_SENDER_THREADS) g_num_sender_threads = MAX_SENDER_THREADS;
1324
+
1325
+ atomic_store(&g_running, 1);
1326
+
1327
+ // Start thread pool
1328
+ for (int i = 0; i < g_num_sender_threads; i++) {
1329
+ if (pthread_create(&g_sender_threads[i], NULL, sender_main, NULL) != 0) {
1330
+ atomic_store(&g_running, 0);
1331
+ // Join any threads that were already created
1332
+ for (int j = 0; j < i; j++) {
1333
+ pthread_join(g_sender_threads[j], NULL);
1334
+ }
1335
+ Py_RETURN_FALSE;
1336
+ }
1337
+ }
1338
+
1339
+ Py_RETURN_TRUE;
1340
+ }
1341
+
1342
+ static PyObject *py_configure(PyObject *self, PyObject *args, PyObject *kw) {
1343
+ PyObject *capture_from_installed_libraries = NULL;
1344
+ int variable_capture_size_limit_mb = 1; // default 1MB (deprecated, use arg_limit/return_limit)
1345
+ float sample_rate = 1.0f; // default 1.0 = capture all (probabilistic 0.0-1.0)
1346
+ int enable_sampling = 0; // default disabled
1347
+ int parse_json_strings = 1; // default enabled
1348
+ int capture_arguments = 1; // default enabled
1349
+ int capture_return_value = 1; // default enabled
1350
+ int arg_limit_mb = 1; // default 1MB for arguments
1351
+ int return_limit_mb = 1; // default 1MB for return values
1352
+ int include_django_view_functions = 0; // default disabled
1353
+
1354
+ static char *kwlist[] = {
1355
+ "variable_capture_size_limit_mb",
1356
+ "capture_from_installed_libraries",
1357
+ "sample_rate",
1358
+ "enable_sampling",
1359
+ "parse_json_strings",
1360
+ "capture_arguments",
1361
+ "capture_return_value",
1362
+ "arg_limit_mb",
1363
+ "return_limit_mb",
1364
+ "include_django_view_functions",
1365
+ NULL
1366
+ };
1367
+
1368
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "|iOfppppiip", kwlist,
1369
+ &variable_capture_size_limit_mb,
1370
+ &capture_from_installed_libraries,
1371
+ &sample_rate,
1372
+ &enable_sampling,
1373
+ &parse_json_strings,
1374
+ &capture_arguments,
1375
+ &capture_return_value,
1376
+ &arg_limit_mb,
1377
+ &return_limit_mb,
1378
+ &include_django_view_functions)) {
1379
+ Py_RETURN_NONE;
1380
+ }
1381
+
1382
+ // Legacy: if variable_capture_size_limit_mb is set but not arg/return limits,
1383
+ // use it for both
1384
+ if (arg_limit_mb == 1 && return_limit_mb == 1 && variable_capture_size_limit_mb != 1) {
1385
+ arg_limit_mb = variable_capture_size_limit_mb;
1386
+ return_limit_mb = variable_capture_size_limit_mb;
1387
+ }
1388
+
1389
+ g_variable_capture_size_limit_bytes = (size_t)variable_capture_size_limit_mb * 1048576;
1390
+ g_arg_limit_bytes = (size_t)arg_limit_mb * 1048576;
1391
+ g_return_limit_bytes = (size_t)return_limit_mb * 1048576;
1392
+
1393
+ if (capture_from_installed_libraries && PyList_Check(capture_from_installed_libraries)) {
1394
+ Py_XDECREF(g_capture_from_installed_libraries);
1395
+ Py_INCREF(capture_from_installed_libraries);
1396
+ g_capture_from_installed_libraries = capture_from_installed_libraries;
1397
+ }
1398
+
1399
+ // Configure sampling
1400
+ // sample_rate is now a float (0.0-1.0 probability)
1401
+ // Convert to old modulo format: 1.0=capture all(1), 0.5=capture 50%(2), 0.1=capture 10%(10), etc.
1402
+ if (sample_rate < 0.001f) sample_rate = 0.001f; // Minimum 0.1% (1 in 1000)
1403
+ if (sample_rate > 1.0f) sample_rate = 1.0f; // Maximum 100%
1404
+ g_sample_rate = (uint64_t)(1.0f / sample_rate); // Convert probability to modulo divisor
1405
+ g_enable_sampling = enable_sampling;
1406
+
1407
+ // Configure JSON parsing
1408
+ g_parse_json_strings = parse_json_strings;
1409
+
1410
+ // Configure capture control
1411
+ g_capture_arguments = capture_arguments;
1412
+ g_capture_return_value = capture_return_value;
1413
+
1414
+ // Configure Django view function filtering
1415
+ g_include_django_view_functions = include_django_view_functions;
1416
+
1417
+ Py_RETURN_NONE;
1418
+ }
1419
+
1420
+ // ---------- Fast C-based object serialization ----------
1421
+ // Serialize any Python object to JSON string, with aggressive introspection
1422
+ // Returns malloc'd JSON string, caller must free()
1423
+ static char* serialize_python_object_to_json(PyObject *value, size_t max_size) {
1424
+ if (!value) {
1425
+ return str_dup("null");
1426
+ }
1427
+
1428
+ // Fast path: Try direct JSON serialization for primitives
1429
+ if (PyUnicode_Check(value)) {
1430
+ const char *str = PyUnicode_AsUTF8(value);
1431
+ if (!str) return str_dup("null");
1432
+
1433
+ // Try to parse as JSON if enabled and string looks like JSON
1434
+ if (g_parse_json_strings && str[0] && (str[0] == '{' || str[0] == '[')) {
1435
+ // Import json module and try to parse
1436
+ PyObject *json_module = PyImport_ImportModule("json");
1437
+ if (json_module) {
1438
+ PyObject *loads_func = PyObject_GetAttrString(json_module, "loads");
1439
+ if (loads_func && PyCallable_Check(loads_func)) {
1440
+ PyObject *args = PyTuple_Pack(1, value);
1441
+ if (args) {
1442
+ PyObject *parsed = PyObject_CallObject(loads_func, args);
1443
+ Py_DECREF(args);
1444
+
1445
+ if (parsed && !PyErr_Occurred()) {
1446
+ // Successfully parsed! Recursively serialize the parsed object
1447
+ char *parsed_json = serialize_python_object_to_json(parsed, max_size);
1448
+ Py_DECREF(parsed);
1449
+ Py_XDECREF(loads_func);
1450
+ Py_DECREF(json_module);
1451
+ return parsed_json;
1452
+ }
1453
+
1454
+ // Failed to parse, clear error and continue with string
1455
+ Py_XDECREF(parsed);
1456
+ if (PyErr_Occurred()) PyErr_Clear();
1457
+ }
1458
+ }
1459
+ Py_XDECREF(loads_func);
1460
+ Py_DECREF(json_module);
1461
+ }
1462
+ if (PyErr_Occurred()) PyErr_Clear();
1463
+ }
1464
+
1465
+ // Regular string serialization (not JSON or parsing disabled)
1466
+ // Check if string length exceeds max_size
1467
+ size_t str_len = strlen(str);
1468
+
1469
+ if (str_len > max_size) {
1470
+ // Truncate the string to max_size
1471
+ char *truncated_str = (char*)malloc(max_size + 20); // Extra space for <<TRIMMED>>
1472
+ if (!truncated_str) return str_dup("null");
1473
+
1474
+ // Copy up to max_size bytes
1475
+ memcpy(truncated_str, str, max_size);
1476
+ strcpy(truncated_str + max_size, "<<TRIMMED>>");
1477
+
1478
+ char *escaped = json_escape(truncated_str);
1479
+ free(truncated_str);
1480
+ if (!escaped) return str_dup("null");
1481
+
1482
+ size_t len = strlen(escaped) + 3; // quotes + null
1483
+ char *result = (char*)malloc(len);
1484
+ if (!result) { free(escaped); return str_dup("null"); }
1485
+ snprintf(result, len, "\"%s\"", escaped);
1486
+ free(escaped);
1487
+ return result;
1488
+ }
1489
+
1490
+ // String is within size limit, serialize normally
1491
+ char *escaped = json_escape(str);
1492
+ if (!escaped) return str_dup("null");
1493
+ size_t len = strlen(escaped) + 3; // quotes + null
1494
+ char *result = (char*)malloc(len);
1495
+ if (!result) { free(escaped); return str_dup("null"); }
1496
+ snprintf(result, len, "\"%s\"", escaped);
1497
+ free(escaped);
1498
+ return result;
1499
+ }
1500
+
1501
+ // Check bool BEFORE int (since bool is a subclass of int in Python)
1502
+ if (PyBool_Check(value)) {
1503
+ return str_dup(value == Py_True ? "true" : "false");
1504
+ }
1505
+
1506
+ if (PyLong_Check(value)) {
1507
+ long long num = PyLong_AsLongLong(value);
1508
+ char *result = (char*)malloc(32);
1509
+ if (!result) return str_dup("null");
1510
+ snprintf(result, 32, "%lld", num);
1511
+ return result;
1512
+ }
1513
+
1514
+ if (PyFloat_Check(value)) {
1515
+ double num = PyFloat_AsDouble(value);
1516
+ char *result = (char*)malloc(32);
1517
+ if (!result) return str_dup("null");
1518
+ snprintf(result, 32, "%.17g", num);
1519
+ return result;
1520
+ }
1521
+
1522
+ if (value == Py_None) {
1523
+ return str_dup("null");
1524
+ }
1525
+
1526
+ // Bytes - try to decode as UTF-8, fallback to repr
1527
+ if (PyBytes_Check(value)) {
1528
+ char *bytes_data = NULL;
1529
+ Py_ssize_t bytes_len = 0;
1530
+
1531
+ if (PyBytes_AsStringAndSize(value, &bytes_data, &bytes_len) == 0 && bytes_data) {
1532
+ // Try to decode as UTF-8
1533
+ PyObject *decoded = PyUnicode_DecodeUTF8(bytes_data, bytes_len, "strict");
1534
+ if (decoded && PyUnicode_Check(decoded)) {
1535
+ // Successfully decoded to string - recursively serialize it
1536
+ // This will trigger JSON parsing if enabled and string contains JSON
1537
+ char *result = serialize_python_object_to_json(decoded, max_size);
1538
+ Py_DECREF(decoded);
1539
+ return result;
1540
+ }
1541
+ Py_XDECREF(decoded);
1542
+ if (PyErr_Occurred()) PyErr_Clear();
1543
+ }
1544
+
1545
+ // Fallback: not UTF-8 or decode failed, use repr (b'...')
1546
+ PyObject *repr_obj = PyObject_Repr(value);
1547
+ if (repr_obj && PyUnicode_Check(repr_obj)) {
1548
+ const char *repr_str = PyUnicode_AsUTF8(repr_obj);
1549
+ if (repr_str) {
1550
+ char *escaped = json_escape(repr_str);
1551
+ Py_DECREF(repr_obj);
1552
+ if (!escaped) return str_dup("null");
1553
+ size_t len = strlen(escaped) + 3;
1554
+ char *result = (char*)malloc(len);
1555
+ if (!result) { free(escaped); return str_dup("null"); }
1556
+ snprintf(result, len, "\"%s\"", escaped);
1557
+ free(escaped);
1558
+ return result;
1559
+ }
1560
+ }
1561
+ Py_XDECREF(repr_obj);
1562
+ if (PyErr_Occurred()) PyErr_Clear();
1563
+ }
1564
+
1565
+ // Tuples - serialize as JSON arrays
1566
+ if (PyTuple_Check(value)) {
1567
+ Py_ssize_t tuple_len = PyTuple_Size(value);
1568
+ if (tuple_len > 100) tuple_len = 100;
1569
+
1570
+ size_t buf_size = 4096;
1571
+ char *buf = (char*)malloc(buf_size);
1572
+ if (!buf) return str_dup("null");
1573
+ size_t pos = 0;
1574
+ buf[pos++] = '[';
1575
+
1576
+ for (Py_ssize_t i = 0; i < tuple_len; i++) {
1577
+ PyObject *item = PyTuple_GetItem(value, i);
1578
+ char *item_json = serialize_python_object_to_json(item, max_size / 10);
1579
+ size_t item_len = strlen(item_json);
1580
+
1581
+ if (pos + item_len + 2 >= buf_size) {
1582
+ free(item_json);
1583
+ break;
1584
+ }
1585
+
1586
+ if (i > 0) buf[pos++] = ',';
1587
+ memcpy(buf + pos, item_json, item_len);
1588
+ pos += item_len;
1589
+ free(item_json);
1590
+ }
1591
+
1592
+ buf[pos++] = ']';
1593
+ buf[pos] = '\0';
1594
+ return buf;
1595
+ }
1596
+
1597
+ // Lists
1598
+ if (PyList_Check(value)) {
1599
+ Py_ssize_t list_len = PyList_Size(value);
1600
+ if (list_len > 100) list_len = 100; // Limit list introspection
1601
+
1602
+ size_t buf_size = 4096;
1603
+ char *buf = (char*)malloc(buf_size);
1604
+ if (!buf) return str_dup("null");
1605
+ size_t pos = 0;
1606
+ buf[pos++] = '[';
1607
+
1608
+ for (Py_ssize_t i = 0; i < list_len; i++) {
1609
+ PyObject *item = PyList_GetItem(value, i);
1610
+ char *item_json = serialize_python_object_to_json(item, max_size / 10);
1611
+ size_t item_len = strlen(item_json);
1612
+
1613
+ if (pos + item_len + 2 >= buf_size) {
1614
+ free(item_json);
1615
+ break; // truncate
1616
+ }
1617
+
1618
+ if (i > 0) buf[pos++] = ',';
1619
+ memcpy(buf + pos, item_json, item_len);
1620
+ pos += item_len;
1621
+ free(item_json);
1622
+ }
1623
+
1624
+ buf[pos++] = ']';
1625
+ buf[pos] = '\0';
1626
+ return buf;
1627
+ }
1628
+
1629
+ // Dicts
1630
+ if (PyDict_Check(value)) {
1631
+ PyObject *key, *val;
1632
+ Py_ssize_t dict_pos = 0;
1633
+ int count = 0;
1634
+
1635
+ size_t buf_size = 8192;
1636
+ char *buf = (char*)malloc(buf_size);
1637
+ if (!buf) return str_dup("null");
1638
+ size_t pos = 0;
1639
+ buf[pos++] = '{';
1640
+
1641
+ while (PyDict_Next(value, &dict_pos, &key, &val) && count < 50) {
1642
+ const char *key_str = PyUnicode_Check(key) ? PyUnicode_AsUTF8(key) : NULL;
1643
+ if (!key_str) continue;
1644
+
1645
+ // Skip private/dunder keys
1646
+ if (key_str[0] == '_') continue;
1647
+
1648
+ char *key_escaped = json_escape(key_str);
1649
+ char *val_json = serialize_python_object_to_json(val, max_size / 10);
1650
+
1651
+ size_t needed = strlen(key_escaped) + strlen(val_json) + 5;
1652
+ if (pos + needed >= buf_size) {
1653
+ free(key_escaped);
1654
+ free(val_json);
1655
+ break; // truncate
1656
+ }
1657
+
1658
+ if (count > 0) buf[pos++] = ',';
1659
+ buf[pos++] = '"';
1660
+ size_t key_len = strlen(key_escaped);
1661
+ memcpy(buf + pos, key_escaped, key_len);
1662
+ pos += key_len;
1663
+ buf[pos++] = '"';
1664
+ buf[pos++] = ':';
1665
+ size_t val_len = strlen(val_json);
1666
+ memcpy(buf + pos, val_json, val_len);
1667
+ pos += val_len;
1668
+
1669
+ free(key_escaped);
1670
+ free(val_json);
1671
+ count++;
1672
+ }
1673
+
1674
+ buf[pos++] = '}';
1675
+ buf[pos] = '\0';
1676
+ return buf;
1677
+ }
1678
+
1679
+ // Complex object introspection - build result dict
1680
+ PyObject *type_obj = PyObject_Type(value);
1681
+ if (!type_obj) {
1682
+ PyErr_Clear();
1683
+ return str_dup("null");
1684
+ }
1685
+
1686
+ PyObject *type_name_obj = PyObject_GetAttrString(type_obj, "__name__");
1687
+ if (!type_name_obj) PyErr_Clear();
1688
+
1689
+ PyObject *module_obj = PyObject_GetAttrString(type_obj, "__module__");
1690
+ if (!module_obj) PyErr_Clear();
1691
+
1692
+ const char *type_name = type_name_obj && PyUnicode_Check(type_name_obj) ? PyUnicode_AsUTF8(type_name_obj) : "unknown";
1693
+ const char *module_name = module_obj && PyUnicode_Check(module_obj) ? PyUnicode_AsUTF8(module_obj) : "builtins";
1694
+
1695
+ // Use a larger buffer to accommodate trimmed large attributes
1696
+ // max_size / 20 per attribute + overhead = could be 50KB+ per attribute
1697
+ // Allow room for ~10 attributes of max size
1698
+ size_t buf_size = (max_size / 2) > 16384 ? (max_size / 2) : 16384;
1699
+ if (buf_size > 1048576) buf_size = 1048576; // Cap at 1MB
1700
+
1701
+ char *buf = (char*)malloc(buf_size);
1702
+ if (!buf) {
1703
+ Py_XDECREF(type_obj);
1704
+ Py_XDECREF(type_name_obj);
1705
+ Py_XDECREF(module_obj);
1706
+ return str_dup("null");
1707
+ }
1708
+
1709
+ size_t pos = 0;
1710
+ buf[pos++] = '{';
1711
+
1712
+ // Add _type field
1713
+ if (strcmp(module_name, "builtins") == 0) {
1714
+ pos += snprintf(buf + pos, buf_size - pos, "\"_type\":\"%s\"", type_name);
1715
+ } else {
1716
+ pos += snprintf(buf + pos, buf_size - pos, "\"_type\":\"%s.%s\"", module_name, type_name);
1717
+ }
1718
+
1719
+ int added_attrs = 0;
1720
+
1721
+ // Try __dict__ introspection
1722
+ PyObject *obj_dict = PyObject_GetAttrString(value, "__dict__");
1723
+ if (PyErr_Occurred()) PyErr_Clear();
1724
+
1725
+ if (obj_dict && PyDict_Check(obj_dict)) {
1726
+ PyObject *key, *val;
1727
+ Py_ssize_t dict_pos = 0;
1728
+ int attr_count = 0;
1729
+
1730
+ if (!added_attrs) {
1731
+ pos += snprintf(buf + pos, buf_size - pos, ",\"attributes\":{");
1732
+ added_attrs = 1;
1733
+ }
1734
+
1735
+ while (PyDict_Next(obj_dict, &dict_pos, &key, &val) && attr_count < 30) {
1736
+ const char *key_str = PyUnicode_Check(key) ? PyUnicode_AsUTF8(key) : NULL;
1737
+ if (!key_str || key_str[0] == '_') continue; // Skip private
1738
+
1739
+ // Skip callables (methods)
1740
+ if (PyCallable_Check(val)) continue;
1741
+
1742
+ char *key_escaped = json_escape(key_str);
1743
+ char *val_json = serialize_python_object_to_json(val, max_size / 20);
1744
+
1745
+ size_t needed = strlen(key_escaped) + strlen(val_json) + 5;
1746
+ if (pos + needed >= buf_size - 100) {
1747
+ free(key_escaped);
1748
+ free(val_json);
1749
+ break;
1750
+ }
1751
+
1752
+ if (attr_count > 0) buf[pos++] = ',';
1753
+ pos += snprintf(buf + pos, buf_size - pos, "\"%s\":%s", key_escaped, val_json);
1754
+
1755
+ free(key_escaped);
1756
+ free(val_json);
1757
+ attr_count++;
1758
+ }
1759
+
1760
+ if (added_attrs) {
1761
+ buf[pos++] = '}';
1762
+ }
1763
+ }
1764
+ Py_XDECREF(obj_dict);
1765
+ if (PyErr_Occurred()) PyErr_Clear();
1766
+
1767
+ // Try common data attributes
1768
+ const char *data_attrs[] = {"data", "value", "content", "body", "result", "message", "text", NULL};
1769
+ for (int i = 0; data_attrs[i]; i++) {
1770
+ PyObject *attr = PyObject_GetAttrString(value, data_attrs[i]);
1771
+ if (PyErr_Occurred()) PyErr_Clear();
1772
+
1773
+ if (attr && !PyCallable_Check(attr)) {
1774
+ char *attr_json = serialize_python_object_to_json(attr, max_size / 20);
1775
+ size_t needed = strlen(data_attrs[i]) + strlen(attr_json) + 5;
1776
+
1777
+ if (pos + needed < buf_size - 100) {
1778
+ pos += snprintf(buf + pos, buf_size - pos, ",\"%s\":%s", data_attrs[i], attr_json);
1779
+ }
1780
+
1781
+ free(attr_json);
1782
+ }
1783
+ Py_XDECREF(attr);
1784
+ }
1785
+ if (PyErr_Occurred()) PyErr_Clear();
1786
+
1787
+ // Note: We removed _repr from the root level for cleaner output
1788
+ // The attributes and common data fields provide enough context
1789
+
1790
+ buf[pos++] = '}';
1791
+ buf[pos] = '\0';
1792
+
1793
+ Py_XDECREF(type_obj);
1794
+ Py_XDECREF(type_name_obj);
1795
+ Py_XDECREF(module_obj);
1796
+
1797
+ // Check size limit
1798
+ if (pos > max_size) {
1799
+ free(buf);
1800
+ char *truncated = (char*)malloc(256);
1801
+ snprintf(truncated, 256, "{\"_truncated\":true,\"_size\":%zu,\"_type\":\"%s.%s\"}",
1802
+ pos, module_name, type_name);
1803
+ return truncated;
1804
+ }
1805
+
1806
+ return buf;
1807
+ }
1808
+
1809
+ static PyObject *py_serialize_value(PyObject *self, PyObject *args) {
1810
+ PyObject *value;
1811
+ size_t max_size = 1048576; // 1MB default
1812
+
1813
+ if (!PyArg_ParseTuple(args, "O|n", &value, &max_size)) {
1814
+ Py_RETURN_NONE;
1815
+ }
1816
+
1817
+ char *json_str = serialize_python_object_to_json(value, max_size);
1818
+ if (!json_str) {
1819
+ Py_RETURN_NONE;
1820
+ }
1821
+
1822
+ PyObject *result = PyUnicode_FromString(json_str);
1823
+ free(json_str);
1824
+ return result;
1825
+ }
1826
+
1827
+ static PyObject *py_record_span(PyObject *self, PyObject *args, PyObject *kw) {
1828
+ const char *session_id, *span_id, *parent_span_id = NULL;
1829
+ const char *file_path, *function_name, *arguments_json, *return_value_json = NULL;
1830
+ int line_number = 0, column_number = 0;
1831
+ unsigned long long start_time_ns = 0, duration_ns = 0;
1832
+
1833
+ static char *kwlist[] = {
1834
+ "session_id", "span_id", "parent_span_id", "file_path", "line_number",
1835
+ "column_number", "function_name", "arguments_json", "return_value_json",
1836
+ "start_time_ns", "duration_ns", NULL
1837
+ };
1838
+
1839
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "sszsiissz|KK", kwlist,
1840
+ &session_id, &span_id, &parent_span_id,
1841
+ &file_path, &line_number, &column_number,
1842
+ &function_name, &arguments_json, &return_value_json,
1843
+ &start_time_ns, &duration_ns)) {
1844
+ Py_RETURN_NONE;
1845
+ }
1846
+ if (!g_running) Py_RETURN_NONE;
1847
+
1848
+ // Fast sampling check - exit early if not sampling this span
1849
+ if (!should_sample()) {
1850
+ Py_RETURN_NONE;
1851
+ }
1852
+
1853
+ // OPTIMIZATION: Release GIL during JSON building + ring push
1854
+ // All string arguments are already C strings from PyArg_ParseTupleAndKeywords,
1855
+ // so we can safely release GIL for the entire body building + transmission.
1856
+ // This extends GIL-free duration from ~100ns to ~500-2000ns (5-20x improvement).
1857
+ char *body = NULL;
1858
+ size_t len = 0;
1859
+ int ok = 0;
1860
+
1861
+ Py_BEGIN_ALLOW_THREADS
1862
+ // Build JSON body (WITHOUT GIL - pure C string operations)
1863
+ if (build_body_func_span(
1864
+ session_id, span_id, parent_span_id,
1865
+ file_path, line_number, column_number,
1866
+ function_name, arguments_json, return_value_json,
1867
+ (uint64_t)start_time_ns, (uint64_t)duration_ns,
1868
+ &body, &len)) {
1869
+ // Push to ring buffer (WITHOUT GIL)
1870
+ ok = ring_push(body, len);
1871
+ }
1872
+ Py_END_ALLOW_THREADS
1873
+
1874
+ if (!ok) { free(body); }
1875
+ Py_RETURN_NONE;
1876
+ }
1877
+
1878
+ static PyObject *py_generate_span_id(PyObject *self, PyObject *args) {
1879
+ char *span_id = generate_span_id();
1880
+ if (!span_id) Py_RETURN_NONE;
1881
+ PyObject *result = PyUnicode_FromString(span_id);
1882
+ free(span_id);
1883
+ return result;
1884
+ }
1885
+
1886
+ static PyObject *py_push_span(PyObject *self, PyObject *args) {
1887
+ const char *span_id;
1888
+ if (!PyArg_ParseTuple(args, "s", &span_id)) {
1889
+ Py_RETURN_NONE;
1890
+ }
1891
+ push_span(span_id);
1892
+ Py_RETURN_NONE;
1893
+ }
1894
+
1895
+ static PyObject *py_pop_span(PyObject *self, PyObject *args) {
1896
+ char *span_id = pop_span();
1897
+ if (!span_id) Py_RETURN_NONE;
1898
+ PyObject *result = PyUnicode_FromString(span_id);
1899
+ free(span_id);
1900
+ return result;
1901
+ }
1902
+
1903
+ static PyObject *py_peek_parent_span_id(PyObject *self, PyObject *args) {
1904
+ char *parent_span_id = peek_parent_span_id();
1905
+ if (!parent_span_id) Py_RETURN_NONE;
1906
+ PyObject *result = PyUnicode_FromString(parent_span_id);
1907
+ free(parent_span_id);
1908
+ return result;
1909
+ }
1910
+
1911
+ static PyObject *py_get_epoch_ns(PyObject *self, PyObject *args) {
1912
+ uint64_t ns = now_epoch_ns();
1913
+ return PyLong_FromUnsignedLongLong(ns);
1914
+ }
1915
+
1916
+ static PyObject *py_get_stats(PyObject *self, PyObject *args) {
1917
+ uint64_t recorded = atomic_load(&g_spans_recorded);
1918
+ uint64_t sampled_out = atomic_load(&g_spans_sampled_out);
1919
+ uint64_t dropped = atomic_load(&g_spans_dropped);
1920
+ size_t buffer_size = ring_count();
1921
+
1922
+ PyObject *dict = PyDict_New();
1923
+ if (!dict) Py_RETURN_NONE;
1924
+
1925
+ PyDict_SetItemString(dict, "spans_recorded", PyLong_FromUnsignedLongLong(recorded));
1926
+ PyDict_SetItemString(dict, "spans_sampled_out", PyLong_FromUnsignedLongLong(sampled_out));
1927
+ PyDict_SetItemString(dict, "spans_dropped", PyLong_FromUnsignedLongLong(dropped));
1928
+ PyDict_SetItemString(dict, "ring_buffer_used", PyLong_FromSize_t(buffer_size));
1929
+ PyDict_SetItemString(dict, "ring_buffer_capacity", PyLong_FromSize_t(g_cap));
1930
+ PyDict_SetItemString(dict, "sample_rate", PyLong_FromUnsignedLongLong(g_sample_rate));
1931
+ PyDict_SetItemString(dict, "sampling_enabled", PyBool_FromLong(g_enable_sampling));
1932
+
1933
+ return dict;
1934
+ }
1935
+
1936
+ static PyObject *py_reset_stats(PyObject *self, PyObject *args) {
1937
+ atomic_store(&g_spans_recorded, 0);
1938
+ atomic_store(&g_spans_sampled_out, 0);
1939
+ atomic_store(&g_spans_dropped, 0);
1940
+ atomic_store(&g_sample_counter, 0);
1941
+ Py_RETURN_NONE;
1942
+ }
1943
+
1944
+ // Global flag to enable profiler for new threads
1945
+ static _Atomic int g_profiler_enabled = 0;
1946
+
1947
+ // Thread start callback to enable profiler on new threads
1948
+ static void thread_start_callback(PyThreadState *tstate) {
1949
+ if (atomic_load(&g_profiler_enabled)) {
1950
+ PyEval_SetProfile(c_profile_func, NULL);
1951
+ }
1952
+ }
1953
+
1954
+ static PyObject *py_start_c_profiler(PyObject *self, PyObject *args) {
1955
+ if (!g_running) {
1956
+ PyErr_SetString(PyExc_RuntimeError, "Profiler not initialized - call init() first");
1957
+ return NULL;
1958
+ }
1959
+
1960
+ // Check if profiler is already running - prevent double installation
1961
+ if (atomic_load(&g_profiler_ready)) {
1962
+ // fprintf(stderr, "[_sffuncspan] WARNING: Profiler already running, skipping duplicate start_c_profiler() call\n");
1963
+ // fflush(stderr);
1964
+ Py_RETURN_NONE;
1965
+ }
1966
+
1967
+ fprintf(stderr, "[_sffuncspan] Installing C profiler...\n");
1968
+ fflush(stderr);
1969
+
1970
+ // Enable profiler flag for new threads
1971
+ atomic_store(&g_profiler_enabled, 1);
1972
+
1973
+ // Set the C-level profiler for current thread (ultra-fast!)
1974
+ PyEval_SetProfile(c_profile_func, NULL);
1975
+
1976
+ // CRITICAL: Mark profiler as ready AFTER PyEval_SetProfile() completes
1977
+ // This ensures any profiler callbacks during installation will skip early
1978
+ atomic_store(&g_profiler_ready, 1);
1979
+
1980
+ fprintf(stderr, "[_sffuncspan] C profiler installed successfully\n");
1981
+ fflush(stderr);
1982
+
1983
+ // Note: For Python 3.12+, we'd use PyEval_SetProfileAllThreads
1984
+ // For earlier versions, we rely on threading.setprofile in Python wrapper
1985
+
1986
+ Py_RETURN_NONE;
1987
+ }
1988
+
1989
+ static PyObject *py_stop_c_profiler(PyObject *self, PyObject *args) {
1990
+ // Mark profiler as not ready
1991
+ atomic_store(&g_profiler_ready, 0);
1992
+
1993
+ // Disable profiler flag for new threads
1994
+ atomic_store(&g_profiler_enabled, 0);
1995
+
1996
+ // Remove the C-level profiler
1997
+ PyEval_SetProfile(NULL, NULL);
1998
+
1999
+ Py_RETURN_NONE;
2000
+ }
2001
+
2002
+ static PyObject *py_cache_config(PyObject *self, PyObject *args) {
2003
+ const char *file_path;
2004
+ const char *func_name;
2005
+ int include_arguments;
2006
+ int include_return_value;
2007
+ int autocapture_all_children;
2008
+ int arg_limit_mb;
2009
+ int return_limit_mb;
2010
+ float sample_rate;
2011
+
2012
+ if (!PyArg_ParseTuple(args, "ssiiiiff",
2013
+ &file_path, &func_name,
2014
+ &include_arguments, &include_return_value,
2015
+ &autocapture_all_children,
2016
+ &arg_limit_mb, &return_limit_mb, &sample_rate)) {
2017
+ Py_RETURN_NONE;
2018
+ }
2019
+
2020
+ // Build config
2021
+ sf_funcspan_config_t config;
2022
+ config.include_arguments = (uint8_t)include_arguments;
2023
+ config.include_return_value = (uint8_t)include_return_value;
2024
+ config.autocapture_all_children = (uint8_t)autocapture_all_children;
2025
+ config.arg_limit_mb = (uint32_t)arg_limit_mb;
2026
+ config.return_limit_mb = (uint32_t)return_limit_mb;
2027
+ config.sample_rate = sample_rate;
2028
+
2029
+ // Compute hash and cache
2030
+ uint64_t hash = simple_hash(file_path, func_name);
2031
+ uint32_t cache_idx = hash % CONFIG_CACHE_SIZE;
2032
+
2033
+ pthread_mutex_lock(&g_config_cache_mutex);
2034
+ g_config_cache[cache_idx].hash = hash;
2035
+ g_config_cache[cache_idx].config = config;
2036
+ pthread_mutex_unlock(&g_config_cache_mutex);
2037
+
2038
+ Py_RETURN_NONE;
2039
+ }
2040
+
2041
+ static PyObject *py_set_function_spans_enabled(PyObject *self, PyObject *args) {
2042
+ int enabled;
2043
+ if (!PyArg_ParseTuple(args, "p", &enabled)) {
2044
+ return NULL;
2045
+ }
2046
+ g_enable_function_spans = enabled;
2047
+ Py_RETURN_NONE;
2048
+ }
2049
+
2050
+ static PyObject *py_set_capture_installed_packages(PyObject *self, PyObject *args) {
2051
+ int enabled;
2052
+ if (!PyArg_ParseTuple(args, "p", &enabled)) {
2053
+ return NULL;
2054
+ }
2055
+ g_capture_installed_packages = enabled;
2056
+ Py_RETURN_NONE;
2057
+ }
2058
+
2059
+ static PyObject *py_set_capture_sf_veritas(PyObject *self, PyObject *args) {
2060
+ int enabled;
2061
+ if (!PyArg_ParseTuple(args, "p", &enabled)) {
2062
+ return NULL;
2063
+ }
2064
+ g_capture_sf_veritas = enabled;
2065
+ Py_RETURN_NONE;
2066
+ }
2067
+
2068
+ static PyObject *py_set_interceptors_ready(PyObject *self, PyObject *args) {
2069
+ // Mark interceptors as fully initialized - profiling can now begin
2070
+ atomic_store(&g_interceptors_ready, 1);
2071
+ fprintf(stderr, "[_sffuncspan] Interceptors ready - profiling enabled\n");
2072
+ fflush(stderr);
2073
+ Py_RETURN_NONE;
2074
+ }
2075
+
2076
+ static PyObject *py_shutdown(PyObject *self, PyObject *args) {
2077
+ if (!g_running) Py_RETURN_NONE;
2078
+
2079
+ // Stop profiler first
2080
+ PyEval_SetProfile(NULL, NULL);
2081
+
2082
+ atomic_store(&g_running, 0);
2083
+
2084
+ // Wake ALL threads with broadcast (not signal)
2085
+ pthread_mutex_lock(&g_cv_mtx);
2086
+ pthread_cond_broadcast(&g_cv);
2087
+ pthread_mutex_unlock(&g_cv_mtx);
2088
+
2089
+ // Join all sender threads in thread pool
2090
+ for (int i = 0; i < g_num_sender_threads; i++) {
2091
+ if (g_sender_threads[i]) {
2092
+ pthread_join(g_sender_threads[i], NULL);
2093
+ g_sender_threads[i] = 0;
2094
+ }
2095
+ }
2096
+ g_num_sender_threads = 0;
2097
+
2098
+ // Cleanup curl (per-thread handles cleaned by pthread_cleanup_push)
2099
+ if (g_hdrs) { curl_slist_free_all(g_hdrs); g_hdrs = NULL; }
2100
+ curl_global_cleanup();
2101
+
2102
+ free(g_url); g_url = NULL;
2103
+ free(g_func_span_query_escaped); g_func_span_query_escaped = NULL;
2104
+ free(g_json_prefix_func_span); g_json_prefix_func_span = NULL;
2105
+ free(g_api_key); g_api_key = NULL;
2106
+ free(g_service_uuid); g_service_uuid = NULL;
2107
+ free(g_library); g_library = NULL;
2108
+ free(g_version); g_version = NULL;
2109
+
2110
+ Py_XDECREF(g_capture_from_installed_libraries);
2111
+ g_capture_from_installed_libraries = NULL;
2112
+
2113
+ if (g_ring) {
2114
+ char *b; size_t l;
2115
+ while (ring_pop(&b, &l)) free(b);
2116
+ free(g_ring); g_ring = NULL;
2117
+ }
2118
+ Py_RETURN_NONE;
2119
+ }
2120
+
2121
+ // ---------- Module table ----------
2122
+ static PyMethodDef SFFuncSpanMethods[] = {
2123
+ {"init", (PyCFunction)py_init, METH_VARARGS | METH_KEYWORDS, "Init and start sender"},
2124
+ {"configure", (PyCFunction)py_configure, METH_VARARGS | METH_KEYWORDS, "Configure function span settings"},
2125
+ {"record_span", (PyCFunction)py_record_span, METH_VARARGS | METH_KEYWORDS, "Record a function span"},
2126
+ {"serialize_value", (PyCFunction)py_serialize_value, METH_VARARGS, "Serialize Python object to JSON (ultra-fast C)"},
2127
+ {"generate_span_id", (PyCFunction)py_generate_span_id, METH_NOARGS, "Generate a new span ID"},
2128
+ {"push_span", (PyCFunction)py_push_span, METH_VARARGS, "Push span ID onto stack"},
2129
+ {"pop_span", (PyCFunction)py_pop_span, METH_NOARGS, "Pop span ID from stack"},
2130
+ {"peek_parent_span_id", (PyCFunction)py_peek_parent_span_id, METH_NOARGS, "Peek parent span ID"},
2131
+ {"get_epoch_ns", (PyCFunction)py_get_epoch_ns, METH_NOARGS, "Get current epoch nanoseconds"},
2132
+ {"get_stats", (PyCFunction)py_get_stats, METH_NOARGS, "Get performance statistics"},
2133
+ {"reset_stats", (PyCFunction)py_reset_stats, METH_NOARGS, "Reset performance statistics"},
2134
+ {"start_c_profiler", (PyCFunction)py_start_c_profiler, METH_NOARGS, "Start ultra-fast C profiler (replaces sys.setprofile)"},
2135
+ {"stop_c_profiler", (PyCFunction)py_stop_c_profiler, METH_NOARGS, "Stop ultra-fast C profiler"},
2136
+ {"cache_config", (PyCFunction)py_cache_config, METH_VARARGS, "Cache config for a function (avoids Python calls in profiler)"},
2137
+ {"set_function_spans_enabled", (PyCFunction)py_set_function_spans_enabled, METH_VARARGS, "Enable/disable function span capture and transmission (master kill switch)"},
2138
+ {"set_capture_installed_packages", (PyCFunction)py_set_capture_installed_packages, METH_VARARGS, "Enable/disable capturing spans from installed packages (site-packages, dist-packages)"},
2139
+ {"set_capture_sf_veritas", (PyCFunction)py_set_capture_sf_veritas, METH_VARARGS, "Enable/disable capturing spans from sf_veritas telemetry code itself"},
2140
+ {"set_interceptors_ready", (PyCFunction)py_set_interceptors_ready, METH_NOARGS, "Mark interceptors as ready - enables profiling (call after setup_interceptors completes)"},
2141
+ {"shutdown", (PyCFunction)py_shutdown, METH_NOARGS, "Shutdown sender and free state"},
2142
+ {NULL, NULL, 0, NULL}
2143
+ };
2144
+
2145
+ static struct PyModuleDef sffuncspanmodule = {
2146
+ PyModuleDef_HEAD_INIT,
2147
+ "_sffuncspan",
2148
+ "sf_veritas ultra-fast function span collection",
2149
+ -1,
2150
+ SFFuncSpanMethods
2151
+ };
2152
+
2153
+ PyMODINIT_FUNC PyInit__sffuncspan(void) {
2154
+ return PyModule_Create(&sffuncspanmodule);
2155
+ }