sf-veritas 0.11.10__cp314-cp314-manylinux_2_28_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. sf_veritas/__init__.py +46 -0
  2. sf_veritas/_auto_preload.py +73 -0
  3. sf_veritas/_sfconfig.c +162 -0
  4. sf_veritas/_sfconfig.cpython-314-x86_64-linux-gnu.so +0 -0
  5. sf_veritas/_sfcrashhandler.c +267 -0
  6. sf_veritas/_sfcrashhandler.cpython-314-x86_64-linux-gnu.so +0 -0
  7. sf_veritas/_sffastlog.c +953 -0
  8. sf_veritas/_sffastlog.cpython-314-x86_64-linux-gnu.so +0 -0
  9. sf_veritas/_sffastnet.c +994 -0
  10. sf_veritas/_sffastnet.cpython-314-x86_64-linux-gnu.so +0 -0
  11. sf_veritas/_sffastnetworkrequest.c +727 -0
  12. sf_veritas/_sffastnetworkrequest.cpython-314-x86_64-linux-gnu.so +0 -0
  13. sf_veritas/_sffuncspan.c +2791 -0
  14. sf_veritas/_sffuncspan.cpython-314-x86_64-linux-gnu.so +0 -0
  15. sf_veritas/_sffuncspan_config.c +730 -0
  16. sf_veritas/_sffuncspan_config.cpython-314-x86_64-linux-gnu.so +0 -0
  17. sf_veritas/_sfheadercheck.c +341 -0
  18. sf_veritas/_sfheadercheck.cpython-314-x86_64-linux-gnu.so +0 -0
  19. sf_veritas/_sfnetworkhop.c +1454 -0
  20. sf_veritas/_sfnetworkhop.cpython-314-x86_64-linux-gnu.so +0 -0
  21. sf_veritas/_sfservice.c +1223 -0
  22. sf_veritas/_sfservice.cpython-314-x86_64-linux-gnu.so +0 -0
  23. sf_veritas/_sfteepreload.c +6227 -0
  24. sf_veritas/app_config.py +57 -0
  25. sf_veritas/cli.py +336 -0
  26. sf_veritas/constants.py +10 -0
  27. sf_veritas/custom_excepthook.py +304 -0
  28. sf_veritas/custom_log_handler.py +146 -0
  29. sf_veritas/custom_output_wrapper.py +153 -0
  30. sf_veritas/custom_print.py +153 -0
  31. sf_veritas/django_app.py +5 -0
  32. sf_veritas/env_vars.py +186 -0
  33. sf_veritas/exception_handling_middleware.py +18 -0
  34. sf_veritas/exception_metaclass.py +69 -0
  35. sf_veritas/fast_frame_info.py +116 -0
  36. sf_veritas/fast_network_hop.py +293 -0
  37. sf_veritas/frame_tools.py +112 -0
  38. sf_veritas/funcspan_config_loader.py +693 -0
  39. sf_veritas/function_span_profiler.py +1313 -0
  40. sf_veritas/get_preload_path.py +34 -0
  41. sf_veritas/import_hook.py +62 -0
  42. sf_veritas/infra_details/__init__.py +3 -0
  43. sf_veritas/infra_details/get_infra_details.py +24 -0
  44. sf_veritas/infra_details/kubernetes/__init__.py +3 -0
  45. sf_veritas/infra_details/kubernetes/get_cluster_name.py +147 -0
  46. sf_veritas/infra_details/kubernetes/get_details.py +7 -0
  47. sf_veritas/infra_details/running_on/__init__.py +17 -0
  48. sf_veritas/infra_details/running_on/kubernetes.py +11 -0
  49. sf_veritas/interceptors.py +543 -0
  50. sf_veritas/libsfnettee.so +0 -0
  51. sf_veritas/local_env_detect.py +118 -0
  52. sf_veritas/package_metadata.py +6 -0
  53. sf_veritas/patches/__init__.py +0 -0
  54. sf_veritas/patches/_patch_tracker.py +74 -0
  55. sf_veritas/patches/concurrent_futures.py +19 -0
  56. sf_veritas/patches/constants.py +1 -0
  57. sf_veritas/patches/exceptions.py +82 -0
  58. sf_veritas/patches/multiprocessing.py +32 -0
  59. sf_veritas/patches/network_libraries/__init__.py +99 -0
  60. sf_veritas/patches/network_libraries/aiohttp.py +294 -0
  61. sf_veritas/patches/network_libraries/curl_cffi.py +363 -0
  62. sf_veritas/patches/network_libraries/http_client.py +670 -0
  63. sf_veritas/patches/network_libraries/httpcore.py +580 -0
  64. sf_veritas/patches/network_libraries/httplib2.py +315 -0
  65. sf_veritas/patches/network_libraries/httpx.py +557 -0
  66. sf_veritas/patches/network_libraries/niquests.py +218 -0
  67. sf_veritas/patches/network_libraries/pycurl.py +399 -0
  68. sf_veritas/patches/network_libraries/requests.py +595 -0
  69. sf_veritas/patches/network_libraries/ssl_socket.py +822 -0
  70. sf_veritas/patches/network_libraries/tornado.py +360 -0
  71. sf_veritas/patches/network_libraries/treq.py +270 -0
  72. sf_veritas/patches/network_libraries/urllib_request.py +483 -0
  73. sf_veritas/patches/network_libraries/utils.py +598 -0
  74. sf_veritas/patches/os.py +17 -0
  75. sf_veritas/patches/threading.py +231 -0
  76. sf_veritas/patches/web_frameworks/__init__.py +54 -0
  77. sf_veritas/patches/web_frameworks/aiohttp.py +798 -0
  78. sf_veritas/patches/web_frameworks/async_websocket_consumer.py +337 -0
  79. sf_veritas/patches/web_frameworks/blacksheep.py +532 -0
  80. sf_veritas/patches/web_frameworks/bottle.py +513 -0
  81. sf_veritas/patches/web_frameworks/cherrypy.py +683 -0
  82. sf_veritas/patches/web_frameworks/cors_utils.py +122 -0
  83. sf_veritas/patches/web_frameworks/django.py +963 -0
  84. sf_veritas/patches/web_frameworks/eve.py +401 -0
  85. sf_veritas/patches/web_frameworks/falcon.py +931 -0
  86. sf_veritas/patches/web_frameworks/fastapi.py +738 -0
  87. sf_veritas/patches/web_frameworks/flask.py +526 -0
  88. sf_veritas/patches/web_frameworks/klein.py +501 -0
  89. sf_veritas/patches/web_frameworks/litestar.py +616 -0
  90. sf_veritas/patches/web_frameworks/pyramid.py +440 -0
  91. sf_veritas/patches/web_frameworks/quart.py +841 -0
  92. sf_veritas/patches/web_frameworks/robyn.py +708 -0
  93. sf_veritas/patches/web_frameworks/sanic.py +874 -0
  94. sf_veritas/patches/web_frameworks/starlette.py +742 -0
  95. sf_veritas/patches/web_frameworks/strawberry.py +1446 -0
  96. sf_veritas/patches/web_frameworks/tornado.py +485 -0
  97. sf_veritas/patches/web_frameworks/utils.py +170 -0
  98. sf_veritas/print_override.py +13 -0
  99. sf_veritas/regular_data_transmitter.py +444 -0
  100. sf_veritas/request_interceptor.py +401 -0
  101. sf_veritas/request_utils.py +550 -0
  102. sf_veritas/segfault_handler.py +116 -0
  103. sf_veritas/server_status.py +1 -0
  104. sf_veritas/shutdown_flag.py +11 -0
  105. sf_veritas/subprocess_startup.py +3 -0
  106. sf_veritas/test_cli.py +145 -0
  107. sf_veritas/thread_local.py +1319 -0
  108. sf_veritas/timeutil.py +114 -0
  109. sf_veritas/transmit_exception_to_sailfish.py +28 -0
  110. sf_veritas/transmitter.py +132 -0
  111. sf_veritas/types.py +47 -0
  112. sf_veritas/unified_interceptor.py +1678 -0
  113. sf_veritas/utils.py +39 -0
  114. sf_veritas-0.11.10.dist-info/METADATA +97 -0
  115. sf_veritas-0.11.10.dist-info/RECORD +141 -0
  116. sf_veritas-0.11.10.dist-info/WHEEL +5 -0
  117. sf_veritas-0.11.10.dist-info/entry_points.txt +2 -0
  118. sf_veritas-0.11.10.dist-info/top_level.txt +1 -0
  119. sf_veritas.libs/libbrotlicommon-6ce2a53c.so.1.0.6 +0 -0
  120. sf_veritas.libs/libbrotlidec-811d1be3.so.1.0.6 +0 -0
  121. sf_veritas.libs/libcom_err-730ca923.so.2.1 +0 -0
  122. sf_veritas.libs/libcrypt-52aca757.so.1.1.0 +0 -0
  123. sf_veritas.libs/libcrypto-bdaed0ea.so.1.1.1k +0 -0
  124. sf_veritas.libs/libcurl-eaa3cf66.so.4.5.0 +0 -0
  125. sf_veritas.libs/libgssapi_krb5-323bbd21.so.2.2 +0 -0
  126. sf_veritas.libs/libidn2-2f4a5893.so.0.3.6 +0 -0
  127. sf_veritas.libs/libk5crypto-9a74ff38.so.3.1 +0 -0
  128. sf_veritas.libs/libkeyutils-2777d33d.so.1.6 +0 -0
  129. sf_veritas.libs/libkrb5-a55300e8.so.3.3 +0 -0
  130. sf_veritas.libs/libkrb5support-e6594cfc.so.0.1 +0 -0
  131. sf_veritas.libs/liblber-2-d20824ef.4.so.2.10.9 +0 -0
  132. sf_veritas.libs/libldap-2-cea2a960.4.so.2.10.9 +0 -0
  133. sf_veritas.libs/libnghttp2-39367a22.so.14.17.0 +0 -0
  134. sf_veritas.libs/libpcre2-8-516f4c9d.so.0.7.1 +0 -0
  135. sf_veritas.libs/libpsl-99becdd3.so.5.3.1 +0 -0
  136. sf_veritas.libs/libsasl2-7de4d792.so.3.0.0 +0 -0
  137. sf_veritas.libs/libselinux-d0805dcb.so.1 +0 -0
  138. sf_veritas.libs/libssh-c11d285b.so.4.8.7 +0 -0
  139. sf_veritas.libs/libssl-60250281.so.1.1.1k +0 -0
  140. sf_veritas.libs/libunistring-05abdd40.so.2.1.0 +0 -0
  141. sf_veritas.libs/libuuid-95b83d40.so.1.3.0 +0 -0
@@ -0,0 +1,730 @@
1
+ // sf_veritas/_sffuncspan_config.c
2
+ // Ultra-fast configuration system for function span capture (<5ns lookups)
3
+ #define PY_SSIZE_T_CLEAN
4
+ #include <Python.h>
5
+ #include <stdint.h>
6
+ #include <stdlib.h>
7
+ #include <string.h>
8
+ #include <pthread.h>
9
+ #include "sf_tls.h"
10
+
11
+ // ---------- Configuration Structure ----------
12
+ // Priority levels (lower number = higher priority)
13
+ #define PRIORITY_HTTP_HEADER 1 // Highest priority (per-request override)
14
+ #define PRIORITY_DECORATOR 2 // @capture_function_spans decorator
15
+ #define PRIORITY_SAILFISH_FUNCTION 3 // Function config in .sailfish
16
+ #define PRIORITY_PRAGMA 4 // File pragma # sailfish-funcspan:
17
+ #define PRIORITY_SAILFISH_FILE 5 // File glob patterns in .sailfish
18
+ #define PRIORITY_DIRECTORY 6 // Directory .sailfish (inherited)
19
+ #define PRIORITY_ENV_VAR 8 // Environment variables
20
+ #define PRIORITY_DEFAULT 9 // Hard-coded defaults (lowest priority)
21
+
22
+ // 32 bytes, cache-line friendly
23
+ typedef struct {
24
+ uint8_t include_arguments;
25
+ uint8_t include_return_value;
26
+ uint8_t autocapture_all_children;
27
+ uint8_t priority; // Priority level (lower = higher priority)
28
+ float sample_rate;
29
+ uint32_t arg_limit_mb;
30
+ uint32_t return_limit_mb;
31
+ uint64_t hash; // Pre-computed for validation
32
+ } sf_funcspan_config_t;
33
+
34
+ // Thread-local storage for header overrides (highest priority)
35
+ typedef struct {
36
+ sf_funcspan_config_t config;
37
+ uint8_t has_override;
38
+ uint8_t _padding[7]; // Align to 32 bytes
39
+ } sf_thread_config_t;
40
+
41
+ static _Thread_local sf_thread_config_t g_thread_config = {0};
42
+
43
+ // Python function reference for reading ContextVar (async-safe fallback)
44
+ static PyObject *g_get_funcspan_override_func = NULL;
45
+
46
+ // ---------- Hash Table for O(1) Lookups ----------
47
+ // Using power-of-2 capacity for fast modulo (bitwise AND)
48
+ typedef struct {
49
+ sf_funcspan_config_t* entries; // Dense array of configs
50
+ uint64_t* keys; // File path hashes
51
+ uint32_t* indices; // Hash table -> entries index
52
+ uint32_t capacity; // Power of 2
53
+ uint32_t size; // Number of entries
54
+ } sf_config_table_t;
55
+
56
+ // Global lookup tables (built at startup)
57
+ static sf_config_table_t g_file_configs = {0};
58
+ static sf_config_table_t g_func_configs = {0};
59
+ static sf_funcspan_config_t g_default_config = {
60
+ .include_arguments = 1,
61
+ .include_return_value = 1,
62
+ .autocapture_all_children = 1,
63
+ .priority = PRIORITY_DEFAULT,
64
+ .sample_rate = 1.0f,
65
+ .arg_limit_mb = 1,
66
+ .return_limit_mb = 1,
67
+ .hash = 0
68
+ };
69
+
70
+ // Lock for table modifications (only used at startup)
71
+ static pthread_mutex_t g_config_mutex = PTHREAD_MUTEX_INITIALIZER;
72
+
73
+ // ---------- Fast Hash Function (xxHash-like) ----------
74
+ // Simple, fast hash for string keys (~2ns)
75
+ static inline uint64_t fast_hash(const char *str, size_t len) {
76
+ const uint64_t PRIME64_1 = 11400714785074694791ULL;
77
+ const uint64_t PRIME64_2 = 14029467366897019727ULL;
78
+ const uint64_t PRIME64_3 = 1609587929392839161ULL;
79
+ const uint64_t PRIME64_4 = 9650029242287828579ULL;
80
+ const uint64_t PRIME64_5 = 2870177450012600261ULL;
81
+
82
+ uint64_t h = PRIME64_5 + len;
83
+ const uint8_t *data = (const uint8_t*)str;
84
+
85
+ // Process 8 bytes at a time
86
+ while (len >= 8) {
87
+ h ^= *(const uint64_t*)data * PRIME64_2;
88
+ h = (h << 31) | (h >> 33);
89
+ h *= PRIME64_1;
90
+ data += 8;
91
+ len -= 8;
92
+ }
93
+
94
+ // Process remaining bytes
95
+ while (len--) {
96
+ h ^= (*data++) * PRIME64_5;
97
+ h = (h << 11) | (h >> 53);
98
+ h *= PRIME64_1;
99
+ }
100
+
101
+ // Final mix
102
+ h ^= h >> 33;
103
+ h *= PRIME64_2;
104
+ h ^= h >> 29;
105
+ h *= PRIME64_3;
106
+ h ^= h >> 32;
107
+
108
+ return h;
109
+ }
110
+
111
+ // ---------- Config Table Operations ----------
112
+ static int table_init(sf_config_table_t *table, uint32_t initial_capacity) {
113
+ // Round up to next power of 2
114
+ uint32_t capacity = 16;
115
+ while (capacity < initial_capacity) capacity <<= 1;
116
+
117
+ table->entries = (sf_funcspan_config_t*)calloc(capacity, sizeof(sf_funcspan_config_t));
118
+ table->keys = (uint64_t*)calloc(capacity, sizeof(uint64_t));
119
+ table->indices = (uint32_t*)malloc(capacity * sizeof(uint32_t));
120
+
121
+ if (!table->entries || !table->keys || !table->indices) {
122
+ free(table->entries);
123
+ free(table->keys);
124
+ free(table->indices);
125
+ return 0;
126
+ }
127
+
128
+ // Initialize indices to UINT32_MAX (empty marker)
129
+ for (uint32_t i = 0; i < capacity; i++) {
130
+ table->indices[i] = UINT32_MAX;
131
+ }
132
+
133
+ table->capacity = capacity;
134
+ table->size = 0;
135
+ return 1;
136
+ }
137
+
138
+ static void table_free(sf_config_table_t *table) {
139
+ if (table->entries) {
140
+ free(table->entries);
141
+ table->entries = NULL;
142
+ }
143
+ if (table->keys) {
144
+ free(table->keys);
145
+ table->keys = NULL;
146
+ }
147
+ if (table->indices) {
148
+ free(table->indices);
149
+ table->indices = NULL;
150
+ }
151
+ table->capacity = 0;
152
+ table->size = 0;
153
+ }
154
+
155
+ // Resize table when load factor > 0.75
156
+ static int table_resize(sf_config_table_t *table) {
157
+ uint32_t new_capacity = table->capacity * 2;
158
+ uint32_t *new_indices = (uint32_t*)malloc(new_capacity * sizeof(uint32_t));
159
+
160
+ if (!new_indices) return 0;
161
+
162
+ // Initialize new indices
163
+ for (uint32_t i = 0; i < new_capacity; i++) {
164
+ new_indices[i] = UINT32_MAX;
165
+ }
166
+
167
+ // Rehash all entries
168
+ for (uint32_t i = 0; i < table->size; i++) {
169
+ uint64_t hash = table->keys[i];
170
+ uint32_t slot = hash & (new_capacity - 1);
171
+
172
+ // Linear probing
173
+ while (new_indices[slot] != UINT32_MAX) {
174
+ slot = (slot + 1) & (new_capacity - 1);
175
+ }
176
+
177
+ new_indices[slot] = i;
178
+ }
179
+
180
+ free(table->indices);
181
+ table->indices = new_indices;
182
+ table->capacity = new_capacity;
183
+ return 1;
184
+ }
185
+
186
+ static int table_insert(sf_config_table_t *table, uint64_t hash, const sf_funcspan_config_t *config) {
187
+ pthread_mutex_lock(&g_config_mutex);
188
+
189
+ // Check if we need to resize
190
+ if (table->size >= (table->capacity * 3) / 4) {
191
+ if (!table_resize(table)) {
192
+ pthread_mutex_unlock(&g_config_mutex);
193
+ return 0;
194
+ }
195
+ }
196
+
197
+ // Find slot using linear probing
198
+ uint32_t slot = hash & (table->capacity - 1);
199
+ while (table->indices[slot] != UINT32_MAX) {
200
+ // Check if key already exists (update case)
201
+ uint32_t idx = table->indices[slot];
202
+ if (table->keys[idx] == hash) {
203
+ // PRIORITY CHECK: Only update if new config has HIGHER priority (lower number)
204
+ // This ensures decorator (priority=2) wins over .sailfish function (priority=3)
205
+ // and pragma (priority=4) wins over .sailfish file (priority=5)
206
+ if (config->priority < table->entries[idx].priority) {
207
+ // New config has higher priority - update the entry
208
+ table->entries[idx] = *config;
209
+ table->entries[idx].hash = hash;
210
+ }
211
+ // Else: existing config has higher/equal priority - keep it
212
+ pthread_mutex_unlock(&g_config_mutex);
213
+ return 1;
214
+ }
215
+ slot = (slot + 1) & (table->capacity - 1);
216
+ }
217
+
218
+ // Insert new entry
219
+ uint32_t idx = table->size++;
220
+ table->keys[idx] = hash;
221
+ table->entries[idx] = *config;
222
+ table->entries[idx].hash = hash;
223
+ table->indices[slot] = idx;
224
+
225
+ pthread_mutex_unlock(&g_config_mutex);
226
+ return 1;
227
+ }
228
+
229
+ // Forward declaration for parse_header_override (defined below)
230
+ static int parse_header_override(const char *header, sf_funcspan_config_t *config);
231
+
232
+ // ---------- Ultra-Fast Lookup (<5ns, with async-safe fallback) ----------
233
+ static inline const sf_funcspan_config_t* config_lookup(const char *file_path, const char *func_name) {
234
+ // 1. FAST PATH: Check C thread-local override first (highest priority, ~1-2ns)
235
+ if (g_thread_config.has_override) {
236
+ return &g_thread_config.config;
237
+ }
238
+
239
+ // 2. SLOW PATH: Check Python ContextVar as fallback (async-safe, ~100-200ns)
240
+ // This only happens on first call after async thread switch. We cache the result
241
+ // in g_thread_config so subsequent calls use the fast path above.
242
+ if (g_get_funcspan_override_func) {
243
+ PyObject *result = PyObject_CallObject(g_get_funcspan_override_func, NULL);
244
+ if (result && result != Py_None) {
245
+ // Got a string from ContextVar
246
+ if (PyUnicode_Check(result)) {
247
+ const char *override_str = PyUnicode_AsUTF8(result);
248
+ if (override_str) {
249
+ sf_funcspan_config_t temp_config;
250
+ if (parse_header_override(override_str, &temp_config)) {
251
+ // SUCCESS: Cache it in thread-local for subsequent calls (makes them fast)
252
+ g_thread_config.config = temp_config;
253
+ g_thread_config.has_override = 1;
254
+ Py_DECREF(result);
255
+ return &g_thread_config.config;
256
+ }
257
+ }
258
+ }
259
+ }
260
+ Py_XDECREF(result);
261
+ if (PyErr_Occurred()) {
262
+ PyErr_Clear(); // Don't let ContextVar errors break profiling
263
+ }
264
+ }
265
+
266
+ // 3. No override found - continue with file/function config lookups
267
+ // Build combined key for function-level lookup
268
+ // Format: "file_path:func_name"
269
+ char key_buf[512];
270
+ size_t file_len = strlen(file_path);
271
+ size_t func_len = strlen(func_name);
272
+
273
+ if (file_len + func_len + 2 < sizeof(key_buf)) {
274
+ memcpy(key_buf, file_path, file_len);
275
+ key_buf[file_len] = ':';
276
+ memcpy(key_buf + file_len + 1, func_name, func_len);
277
+ key_buf[file_len + 1 + func_len] = '\0';
278
+
279
+ // 4. Compute hash (fast, ~2ns)
280
+ uint64_t hash = fast_hash(key_buf, file_len + 1 + func_len);
281
+
282
+ // 5. Prefetch cache line (parallel with hash)
283
+ __builtin_prefetch(&g_func_configs.indices[hash & (g_func_configs.capacity - 1)]);
284
+
285
+ // 6. Lookup in function table (~1ns)
286
+ uint32_t slot = hash & (g_func_configs.capacity - 1);
287
+ uint32_t idx = g_func_configs.indices[slot];
288
+
289
+ if (idx != UINT32_MAX && g_func_configs.keys[idx] == hash) {
290
+ return &g_func_configs.entries[idx];
291
+ }
292
+ }
293
+
294
+ // 7. Fallback to file-level lookup
295
+ uint64_t file_hash = fast_hash(file_path, file_len);
296
+ __builtin_prefetch(&g_file_configs.indices[file_hash & (g_file_configs.capacity - 1)]);
297
+
298
+ uint32_t slot = file_hash & (g_file_configs.capacity - 1);
299
+ uint32_t idx = g_file_configs.indices[slot];
300
+
301
+ if (idx != UINT32_MAX && g_file_configs.keys[idx] == file_hash) {
302
+ return &g_file_configs.entries[idx];
303
+ }
304
+
305
+ // 8. Return default config
306
+ return &g_default_config;
307
+ }
308
+
309
+ // ---------- Header Parsing ----------
310
+ // Parse header format: "include_arguments-include_return_value-arg_limit_mb-return_limit_mb-autocapture_all_children-sample_rate"
311
+ // Example: "1-1-1-1-1-1.0" or "0-0-2-2-1-0.5"
312
+ static int parse_header_override(const char *header, sf_funcspan_config_t *config) {
313
+ if (!header || !config) return 0;
314
+
315
+ // Parse using sscanf (fast enough for header parsing, ~100ns)
316
+ int values[5];
317
+ float sample_rate;
318
+
319
+ int parsed = sscanf(header, "%d-%d-%d-%d-%d-%f",
320
+ &values[0], &values[1], &values[2],
321
+ &values[3], &values[4], &sample_rate);
322
+
323
+ if (parsed != 6) return 0; // Parse error
324
+
325
+ // Validate values
326
+ if (sample_rate < 0.0f || sample_rate > 1.0f) return 0;
327
+ for (int i = 0; i < 5; i++) {
328
+ if (values[i] < 0) return 0;
329
+ }
330
+
331
+ // Set config
332
+ config->include_arguments = (uint8_t)values[0];
333
+ config->include_return_value = (uint8_t)values[1];
334
+ config->arg_limit_mb = (uint32_t)values[2];
335
+ config->return_limit_mb = (uint32_t)values[3];
336
+ config->autocapture_all_children = (uint8_t)values[4];
337
+ config->sample_rate = sample_rate;
338
+ config->priority = PRIORITY_HTTP_HEADER; // HTTP headers always have highest priority
339
+ config->hash = 0;
340
+
341
+ return 1; // Success
342
+ }
343
+
344
+ // ---------- Python API ----------
345
+
346
+ static PyObject* py_init(PyObject *self, PyObject *args) {
347
+ PyObject *config_dict;
348
+
349
+ if (!PyArg_ParseTuple(args, "O", &config_dict)) {
350
+ return NULL;
351
+ }
352
+
353
+ if (!PyDict_Check(config_dict)) {
354
+ PyErr_SetString(PyExc_TypeError, "config must be a dictionary");
355
+ return NULL;
356
+ }
357
+
358
+ // Initialize default config from dict
359
+ PyObject *val;
360
+
361
+ val = PyDict_GetItemString(config_dict, "include_arguments");
362
+ if (val && PyBool_Check(val)) {
363
+ g_default_config.include_arguments = (val == Py_True) ? 1 : 0;
364
+ }
365
+
366
+ val = PyDict_GetItemString(config_dict, "include_return_value");
367
+ if (val && PyBool_Check(val)) {
368
+ g_default_config.include_return_value = (val == Py_True) ? 1 : 0;
369
+ }
370
+
371
+ val = PyDict_GetItemString(config_dict, "autocapture_all_children");
372
+ if (val && PyBool_Check(val)) {
373
+ g_default_config.autocapture_all_children = (val == Py_True) ? 1 : 0;
374
+ }
375
+
376
+ val = PyDict_GetItemString(config_dict, "arg_limit_mb");
377
+ if (val && PyLong_Check(val)) {
378
+ g_default_config.arg_limit_mb = (uint32_t)PyLong_AsLong(val);
379
+ }
380
+
381
+ val = PyDict_GetItemString(config_dict, "return_limit_mb");
382
+ if (val && PyLong_Check(val)) {
383
+ g_default_config.return_limit_mb = (uint32_t)PyLong_AsLong(val);
384
+ }
385
+
386
+ val = PyDict_GetItemString(config_dict, "sample_rate");
387
+ if (val && PyFloat_Check(val)) {
388
+ g_default_config.sample_rate = (float)PyFloat_AsDouble(val);
389
+ }
390
+
391
+ // Initialize hash tables
392
+ if (g_file_configs.capacity == 0) {
393
+ if (!table_init(&g_file_configs, 1024)) {
394
+ PyErr_SetString(PyExc_RuntimeError, "Failed to initialize file config table");
395
+ return NULL;
396
+ }
397
+ }
398
+
399
+ if (g_func_configs.capacity == 0) {
400
+ if (!table_init(&g_func_configs, 4096)) {
401
+ PyErr_SetString(PyExc_RuntimeError, "Failed to initialize function config table");
402
+ return NULL;
403
+ }
404
+ }
405
+
406
+ Py_RETURN_NONE;
407
+ }
408
+
409
+ static PyObject* py_add_file(PyObject *self, PyObject *args, PyObject *kwargs) {
410
+ const char *file_path;
411
+ PyObject *config_dict;
412
+ int priority = PRIORITY_SAILFISH_FILE; // Default priority
413
+
414
+ static char *kwlist[] = {"file_path", "config", "priority", NULL};
415
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs, "sO|i", kwlist, &file_path, &config_dict, &priority)) {
416
+ return NULL;
417
+ }
418
+
419
+ if (!PyDict_Check(config_dict)) {
420
+ PyErr_SetString(PyExc_TypeError, "config must be a dictionary");
421
+ return NULL;
422
+ }
423
+
424
+ // Build config from dict
425
+ sf_funcspan_config_t config = g_default_config; // Start with defaults
426
+ config.priority = (uint8_t)priority; // Set priority from parameter
427
+ PyObject *val;
428
+
429
+ val = PyDict_GetItemString(config_dict, "include_arguments");
430
+ if (val && PyBool_Check(val)) {
431
+ config.include_arguments = (val == Py_True) ? 1 : 0;
432
+ }
433
+
434
+ val = PyDict_GetItemString(config_dict, "include_return_value");
435
+ if (val && PyBool_Check(val)) {
436
+ config.include_return_value = (val == Py_True) ? 1 : 0;
437
+ }
438
+
439
+ val = PyDict_GetItemString(config_dict, "autocapture_all_children");
440
+ if (val && PyBool_Check(val)) {
441
+ config.autocapture_all_children = (val == Py_True) ? 1 : 0;
442
+ }
443
+
444
+ val = PyDict_GetItemString(config_dict, "arg_limit_mb");
445
+ if (val && PyLong_Check(val)) {
446
+ config.arg_limit_mb = (uint32_t)PyLong_AsLong(val);
447
+ }
448
+
449
+ val = PyDict_GetItemString(config_dict, "return_limit_mb");
450
+ if (val && PyLong_Check(val)) {
451
+ config.return_limit_mb = (uint32_t)PyLong_AsLong(val);
452
+ }
453
+
454
+ val = PyDict_GetItemString(config_dict, "sample_rate");
455
+ if (val && PyFloat_Check(val)) {
456
+ config.sample_rate = (float)PyFloat_AsDouble(val);
457
+ }
458
+
459
+ // Compute hash and insert
460
+ uint64_t hash = fast_hash(file_path, strlen(file_path));
461
+
462
+ if (!table_insert(&g_file_configs, hash, &config)) {
463
+ PyErr_SetString(PyExc_RuntimeError, "Failed to insert file config");
464
+ return NULL;
465
+ }
466
+
467
+ Py_RETURN_NONE;
468
+ }
469
+
470
+ static PyObject* py_add_function(PyObject *self, PyObject *args, PyObject *kwargs) {
471
+ const char *file_path;
472
+ const char *func_name;
473
+ PyObject *config_dict;
474
+ int priority = PRIORITY_SAILFISH_FUNCTION; // Default priority
475
+
476
+ static char *kwlist[] = {"file_path", "func_name", "config", "priority", NULL};
477
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ssO|i", kwlist, &file_path, &func_name, &config_dict, &priority)) {
478
+ return NULL;
479
+ }
480
+
481
+ if (!PyDict_Check(config_dict)) {
482
+ PyErr_SetString(PyExc_TypeError, "config must be a dictionary");
483
+ return NULL;
484
+ }
485
+
486
+ // Build config from dict (same as add_file)
487
+ sf_funcspan_config_t config = g_default_config;
488
+ config.priority = (uint8_t)priority; // Set priority from parameter
489
+ PyObject *val;
490
+
491
+ val = PyDict_GetItemString(config_dict, "include_arguments");
492
+ if (val && PyBool_Check(val)) {
493
+ config.include_arguments = (val == Py_True) ? 1 : 0;
494
+ }
495
+
496
+ val = PyDict_GetItemString(config_dict, "include_return_value");
497
+ if (val && PyBool_Check(val)) {
498
+ config.include_return_value = (val == Py_True) ? 1 : 0;
499
+ }
500
+
501
+ val = PyDict_GetItemString(config_dict, "autocapture_all_children");
502
+ if (val && PyBool_Check(val)) {
503
+ config.autocapture_all_children = (val == Py_True) ? 1 : 0;
504
+ }
505
+
506
+ val = PyDict_GetItemString(config_dict, "arg_limit_mb");
507
+ if (val && PyLong_Check(val)) {
508
+ config.arg_limit_mb = (uint32_t)PyLong_AsLong(val);
509
+ }
510
+
511
+ val = PyDict_GetItemString(config_dict, "return_limit_mb");
512
+ if (val && PyLong_Check(val)) {
513
+ config.return_limit_mb = (uint32_t)PyLong_AsLong(val);
514
+ }
515
+
516
+ val = PyDict_GetItemString(config_dict, "sample_rate");
517
+ if (val && PyFloat_Check(val)) {
518
+ config.sample_rate = (float)PyFloat_AsDouble(val);
519
+ }
520
+
521
+ // Build combined key: "file_path:func_name"
522
+ size_t file_len = strlen(file_path);
523
+ size_t func_len = strlen(func_name);
524
+ char *key = (char*)malloc(file_len + func_len + 2);
525
+
526
+ if (!key) {
527
+ PyErr_SetString(PyExc_MemoryError, "Failed to allocate key");
528
+ return NULL;
529
+ }
530
+
531
+ memcpy(key, file_path, file_len);
532
+ key[file_len] = ':';
533
+ memcpy(key + file_len + 1, func_name, func_len);
534
+ key[file_len + 1 + func_len] = '\0';
535
+
536
+ uint64_t hash = fast_hash(key, file_len + 1 + func_len);
537
+ free(key);
538
+
539
+ if (!table_insert(&g_func_configs, hash, &config)) {
540
+ PyErr_SetString(PyExc_RuntimeError, "Failed to insert function config");
541
+ return NULL;
542
+ }
543
+
544
+ Py_RETURN_NONE;
545
+ }
546
+
547
+ static PyObject* py_set_thread_override(PyObject *self, PyObject *args) {
548
+ const char *header;
549
+
550
+ if (!PyArg_ParseTuple(args, "s", &header)) {
551
+ return NULL;
552
+ }
553
+
554
+ sf_funcspan_config_t config;
555
+ if (!parse_header_override(header, &config)) {
556
+ PyErr_SetString(PyExc_ValueError, "Invalid header format. Expected: 'include_arguments-include_return_value-arg_limit_mb-return_limit_mb-autocapture_all_children-sample_rate'");
557
+ return NULL;
558
+ }
559
+
560
+ g_thread_config.config = config;
561
+ g_thread_config.has_override = 1;
562
+
563
+ Py_RETURN_NONE;
564
+ }
565
+
566
+ static PyObject* py_clear_thread_override(PyObject *self, PyObject *args) {
567
+ g_thread_config.has_override = 0;
568
+ memset(&g_thread_config.config, 0, sizeof(sf_funcspan_config_t));
569
+ Py_RETURN_NONE;
570
+ }
571
+
572
+ static PyObject* py_has_thread_override(PyObject *self, PyObject *args) {
573
+ // Return True if there's an active HTTP header override for this thread
574
+ if (g_thread_config.has_override) {
575
+ Py_RETURN_TRUE;
576
+ }
577
+
578
+ // Also check ContextVar for async contexts
579
+ if (g_get_funcspan_override_func) {
580
+ PyObject *result = PyObject_CallObject(g_get_funcspan_override_func, NULL);
581
+ if (result && result != Py_None) {
582
+ Py_DECREF(result);
583
+ Py_RETURN_TRUE;
584
+ }
585
+ Py_XDECREF(result);
586
+ if (PyErr_Occurred()) {
587
+ PyErr_Clear();
588
+ }
589
+ }
590
+
591
+ Py_RETURN_FALSE;
592
+ }
593
+
594
+ static PyObject* py_get_thread_override(PyObject *self, PyObject *args) {
595
+ // Return current thread-local override as formatted string, or None if not set
596
+ if (!g_thread_config.has_override) {
597
+ Py_RETURN_NONE;
598
+ }
599
+
600
+ const sf_funcspan_config_t *config = &g_thread_config.config;
601
+
602
+ // Format: "include_arguments-include_return_value-arg_limit_mb-return_limit_mb-autocapture_all_children-sample_rate"
603
+ // Example: "1-1-1-1-1-1.0" or "0-0-2-2-1-0.5"
604
+ char buffer[128];
605
+ int written = snprintf(
606
+ buffer,
607
+ sizeof(buffer),
608
+ "%d-%d-%u-%u-%d-%.2f",
609
+ (int)config->include_arguments,
610
+ (int)config->include_return_value,
611
+ config->arg_limit_mb,
612
+ config->return_limit_mb,
613
+ (int)config->autocapture_all_children,
614
+ config->sample_rate
615
+ );
616
+
617
+ if (written < 0 || written >= (int)sizeof(buffer)) {
618
+ PyErr_SetString(PyExc_RuntimeError, "Failed to format thread override string");
619
+ return NULL;
620
+ }
621
+
622
+ return PyUnicode_FromString(buffer);
623
+ }
624
+
625
+ static PyObject* py_get(PyObject *self, PyObject *args) {
626
+ const char *file_path;
627
+ const char *func_name;
628
+
629
+ if (!PyArg_ParseTuple(args, "ss", &file_path, &func_name)) {
630
+ return NULL;
631
+ }
632
+
633
+ const sf_funcspan_config_t *config = config_lookup(file_path, func_name);
634
+
635
+ // Return as dictionary
636
+ PyObject *dict = PyDict_New();
637
+ if (!dict) return NULL;
638
+
639
+ PyDict_SetItemString(dict, "include_arguments", PyBool_FromLong(config->include_arguments));
640
+ PyDict_SetItemString(dict, "include_return_value", PyBool_FromLong(config->include_return_value));
641
+ PyDict_SetItemString(dict, "autocapture_all_children", PyBool_FromLong(config->autocapture_all_children));
642
+ PyDict_SetItemString(dict, "arg_limit_mb", PyLong_FromLong(config->arg_limit_mb));
643
+ PyDict_SetItemString(dict, "return_limit_mb", PyLong_FromLong(config->return_limit_mb));
644
+ PyDict_SetItemString(dict, "sample_rate", PyFloat_FromDouble(config->sample_rate));
645
+
646
+ return dict;
647
+ }
648
+
649
+ static PyObject* py_get_raw_pointer(PyObject *self, PyObject *args) {
650
+ const char *file_path;
651
+ const char *func_name;
652
+
653
+ if (!PyArg_ParseTuple(args, "ss", &file_path, &func_name)) {
654
+ return NULL;
655
+ }
656
+
657
+ const sf_funcspan_config_t *config = config_lookup(file_path, func_name);
658
+
659
+ // Return pointer as PyCapsule (for C-level integration)
660
+ return PyCapsule_New((void*)config, "sf_funcspan_config_ptr", NULL);
661
+ }
662
+
663
+ static PyObject* py_shutdown(PyObject *self, PyObject *args) {
664
+ pthread_mutex_lock(&g_config_mutex);
665
+
666
+ table_free(&g_file_configs);
667
+ table_free(&g_func_configs);
668
+
669
+ // Reset default config
670
+ g_default_config.include_arguments = 1;
671
+ g_default_config.include_return_value = 1;
672
+ g_default_config.autocapture_all_children = 1;
673
+ g_default_config.sample_rate = 1.0f;
674
+ g_default_config.arg_limit_mb = 1;
675
+ g_default_config.return_limit_mb = 1;
676
+
677
+ pthread_mutex_unlock(&g_config_mutex);
678
+
679
+ Py_RETURN_NONE;
680
+ }
681
+
682
+ // ---------- Module Definition ----------
683
+ static PyMethodDef ConfigMethods[] = {
684
+ {"init", py_init, METH_VARARGS, "Initialize config system with default config"},
685
+ {"add_file", (PyCFunction)py_add_file, METH_VARARGS | METH_KEYWORDS, "Add file-level config (with optional priority parameter)"},
686
+ {"add_function", (PyCFunction)py_add_function, METH_VARARGS | METH_KEYWORDS, "Add function-level config (with optional priority parameter)"},
687
+ {"set_thread_override", py_set_thread_override, METH_VARARGS, "Set thread-local override from header"},
688
+ {"clear_thread_override", py_clear_thread_override, METH_NOARGS, "Clear thread-local override"},
689
+ {"has_thread_override", py_has_thread_override, METH_NOARGS, "Check if there's an active HTTP header override"},
690
+ {"get_thread_override", py_get_thread_override, METH_NOARGS, "Get thread-local override as formatted string"},
691
+ {"get", py_get, METH_VARARGS, "Get config for file:func (returns dict)"},
692
+ {"get_raw_pointer", py_get_raw_pointer, METH_VARARGS, "Get raw config pointer (PyCapsule)"},
693
+ {"shutdown", py_shutdown, METH_NOARGS, "Shutdown and free all tables"},
694
+ {NULL, NULL, 0, NULL}
695
+ };
696
+
697
+ static struct PyModuleDef sffuncspanconfigmodule = {
698
+ PyModuleDef_HEAD_INIT,
699
+ "_sffuncspan_config",
700
+ "Ultra-fast configuration system for function span capture (<5ns lookups)",
701
+ -1,
702
+ ConfigMethods
703
+ };
704
+
705
+ PyMODINIT_FUNC PyInit__sffuncspan_config(void) {
706
+ PyObject *module = PyModule_Create(&sffuncspanconfigmodule);
707
+ if (!module) {
708
+ return NULL;
709
+ }
710
+
711
+ // Import the bridge function for reading ContextVar (async-safe fallback)
712
+ // This allows config_lookup() to check ContextVar when thread-local is empty
713
+ PyObject *thread_local_module = PyImport_ImportModule("sf_veritas.thread_local");
714
+ if (thread_local_module) {
715
+ g_get_funcspan_override_func = PyObject_GetAttrString(thread_local_module, "_get_funcspan_override_for_c");
716
+ if (!g_get_funcspan_override_func || !PyCallable_Check(g_get_funcspan_override_func)) {
717
+ // If function not found or not callable, clear it
718
+ Py_XDECREF(g_get_funcspan_override_func);
719
+ g_get_funcspan_override_func = NULL;
720
+ // Not a fatal error - continue without async fallback
721
+ PyErr_Clear();
722
+ }
723
+ Py_DECREF(thread_local_module);
724
+ } else {
725
+ // Module import failed - not fatal, continue without async fallback
726
+ PyErr_Clear();
727
+ }
728
+
729
+ return module;
730
+ }