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,1223 @@
1
+ // sf_veritas/_sfservice.c
2
+ #define PY_SSIZE_T_CLEAN
3
+ #include <Python.h>
4
+ #include <pthread.h>
5
+ #include <curl/curl.h>
6
+ #include <stdatomic.h>
7
+ #include <stdint.h>
8
+ #include <stdlib.h>
9
+ #include <string.h>
10
+ #include <time.h>
11
+ #include <sys/time.h>
12
+ #include "sf_tls.h"
13
+ extern void sf_guard_enter(void);
14
+ extern void sf_guard_leave(void);
15
+ extern int sf_guard_active(void);
16
+
17
+ // ===================== Thread-local guard flag ====================
18
+ // CRITICAL: Prevents _sfteepreload.c from capturing our telemetry traffic
19
+ __attribute__((visibility("default")))
20
+
21
+ // ---------- Ring buffer ----------
22
+ #ifndef SFS_RING_CAP
23
+ #define SFS_RING_CAP 256 // Smaller capacity for lower-load service operations
24
+ #endif
25
+
26
+ typedef struct {
27
+ char *body; // malloc'd HTTP JSON body
28
+ size_t len;
29
+ } sfs_msg_t;
30
+
31
+ static sfs_msg_t *g_ring = NULL;
32
+ static size_t g_cap = 0;
33
+ static _Atomic size_t g_head = 0; // consumer
34
+ static _Atomic size_t g_tail = 0; // producer
35
+
36
+ // tiny spinlock to make push MPMC-safe enough for Python producers
37
+ static atomic_flag g_push_lock = ATOMIC_FLAG_INIT;
38
+
39
+ // wake/sleep
40
+ static pthread_mutex_t g_cv_mtx = PTHREAD_MUTEX_INITIALIZER;
41
+ static pthread_cond_t g_cv = PTHREAD_COND_INITIALIZER;
42
+ static _Atomic int g_running = 0;
43
+
44
+ // Thread pool for parallel sending (configurable via SF_SERVICE_SENDER_THREADS)
45
+ #define MAX_SENDER_THREADS 16
46
+ static pthread_t g_sender_threads[MAX_SENDER_THREADS];
47
+ static int g_num_sender_threads = 0;
48
+ static int g_configured_sender_threads = 1; // Default: 1 thread
49
+
50
+ // curl state (per-thread handles + shared headers)
51
+ __thread CURL *g_telem_curl = NULL;
52
+ static struct curl_slist *g_hdrs = NULL;
53
+
54
+ // config (owned strings)
55
+ static char *g_url = NULL;
56
+
57
+ static char *g_api_key = NULL;
58
+ static char *g_service_uuid = NULL;
59
+ static char *g_library = NULL;
60
+ static char *g_version = NULL;
61
+ static int g_http2 = 0;
62
+
63
+ // --- SERVICE IDENTIFIER channel state ---
64
+ static char *g_service_identifier_query_escaped = NULL;
65
+ static char *g_json_prefix_service_identifier = NULL;
66
+
67
+ // --- DOMAINS channel state ---
68
+ static char *g_domains_query_escaped = NULL;
69
+ static char *g_json_prefix_domains = NULL;
70
+
71
+ // --- UPDATE SERVICE DETAILS channel state ---
72
+ static char *g_update_service_query_escaped = NULL;
73
+ static char *g_json_prefix_update_service = NULL;
74
+
75
+ // --- COLLECT METADATA channel state ---
76
+ static char *g_collect_metadata_query_escaped = NULL;
77
+ static char *g_json_prefix_collect_metadata = NULL;
78
+
79
+ static const char *JSON_SUFFIX = "}}";
80
+
81
+ // ---------- helpers ----------
82
+ static inline uint64_t now_ms(void) {
83
+ #if defined(CLOCK_REALTIME_COARSE)
84
+ struct timespec ts;
85
+ clock_gettime(CLOCK_REALTIME_COARSE, &ts);
86
+ return ((uint64_t)ts.tv_sec) * 1000ULL + (uint64_t)(ts.tv_nsec / 1000000ULL);
87
+ #else
88
+ struct timeval tv;
89
+ gettimeofday(&tv, NULL);
90
+ return ((uint64_t)tv.tv_sec) * 1000ULL + (uint64_t)(tv.tv_usec / 1000ULL);
91
+ #endif
92
+ }
93
+
94
+ static char *str_dup(const char *s) {
95
+ size_t n = strlen(s);
96
+ char *p = (char*)malloc(n + 1);
97
+ if (!p) return NULL;
98
+ memcpy(p, s, n);
99
+ p[n] = 0;
100
+ return p;
101
+ }
102
+
103
+ // escape for generic JSON string fields
104
+ static char *json_escape(const char *s) {
105
+ const unsigned char *in = (const unsigned char*)s;
106
+ size_t extra = 0;
107
+ for (const unsigned char *p = in; *p; ++p) {
108
+ switch (*p) {
109
+ case '\\': case '"': extra++; break;
110
+ default:
111
+ if (*p < 0x20) extra += 5; // \u00XX
112
+ }
113
+ }
114
+ size_t inlen = strlen(s);
115
+ char *out = (char*)malloc(inlen + extra + 1);
116
+ if (!out) return NULL;
117
+
118
+ char *o = out;
119
+ for (const unsigned char *p = in; *p; ++p) {
120
+ switch (*p) {
121
+ case '\\': *o++='\\'; *o++='\\'; break;
122
+ case '"': *o++='\\'; *o++='"'; break;
123
+ default:
124
+ if (*p < 0x20) {
125
+ static const char hex[] = "0123456789abcdef";
126
+ *o++='\\'; *o++='u'; *o++='0'; *o++='0';
127
+ *o++=hex[(*p)>>4]; *o++=hex[(*p)&0xF];
128
+ } else {
129
+ *o++ = (char)*p;
130
+ }
131
+ }
132
+ }
133
+ *o = 0;
134
+ return out;
135
+ }
136
+
137
+ // escape for the GraphQL "query" string (handle \n, \r, \t too)
138
+ static char *json_escape_query(const char *s) {
139
+ const unsigned char *in = (const unsigned char*)s;
140
+ size_t extra = 0;
141
+ for (const unsigned char *p = in; *p; ++p) {
142
+ switch (*p) {
143
+ case '\\': case '"': case '\n': case '\r': case '\t': extra++; break;
144
+ default: break;
145
+ }
146
+ }
147
+ size_t inlen = strlen(s);
148
+ char *out = (char*)malloc(inlen + extra + 1);
149
+ if (!out) return NULL;
150
+ char *o = out;
151
+ for (const unsigned char *p = in; *p; ++p) {
152
+ switch (*p) {
153
+ case '\\': *o++='\\'; *o++='\\'; break;
154
+ case '"': *o++='\\'; *o++='"'; break;
155
+ case '\n': *o++='\\'; *o++='n'; break;
156
+ case '\r': *o++='\\'; *o++='r'; break;
157
+ case '\t': *o++='\\'; *o++='t'; break;
158
+ default: *o++=(char)*p;
159
+ }
160
+ }
161
+ *o=0;
162
+ return out;
163
+ }
164
+
165
+ // generic prefix builder for a given escaped query
166
+ static int build_prefix_for_query(const char *query_escaped, char **out_prefix) {
167
+ const char *p1 = "{\"query\":\"";
168
+ const char *p2 = "\",\"variables\":{";
169
+ const char *k1 = "\"apiKey\":\"";
170
+ const char *k2 = "\",\"serviceUuid\":\"";
171
+ const char *k3 = "\",\"library\":\"";
172
+ const char *k4 = "\",\"version\":\"";
173
+
174
+ size_t n = strlen(p1) + strlen(query_escaped) + strlen(p2)
175
+ + strlen(k1) + strlen(g_api_key)
176
+ + strlen(k2) + strlen(g_service_uuid)
177
+ + strlen(k3) + strlen(g_library)
178
+ + strlen(k4) + strlen(g_version) + 5;
179
+
180
+ char *prefix = (char*)malloc(n);
181
+ if (!prefix) return 0;
182
+
183
+ char *o = prefix;
184
+ o += sprintf(o, "%s%s%s", p1, query_escaped, p2);
185
+ o += sprintf(o, "%s%s", k1, g_api_key);
186
+ o += sprintf(o, "%s%s", k2, g_service_uuid);
187
+ o += sprintf(o, "%s%s", k3, g_library);
188
+ o += sprintf(o, "%s%s\"", k4, g_version);
189
+ *o = '\0';
190
+
191
+ *out_prefix = prefix;
192
+ return 1;
193
+ }
194
+
195
+ // Build SERVICE IDENTIFIER body
196
+ static int build_body_service_identifier(
197
+ const char *service_identifier, size_t service_identifier_len,
198
+ const char *service_version, size_t service_version_len,
199
+ const char *service_display_name, size_t service_display_name_len,
200
+ const char *service_additional_metadata, size_t service_additional_metadata_len,
201
+ const char *git_sha, size_t git_sha_len,
202
+ const char *infrastructure_type, size_t infrastructure_type_len,
203
+ const char *infrastructure_details, size_t infrastructure_details_len,
204
+ const char *setup_interceptors_file_path, size_t setup_interceptors_file_path_len,
205
+ int setup_interceptors_line_number,
206
+ char **out_body, size_t *out_len
207
+ ){
208
+ // Escape all string fields
209
+ char *si_tmp = (char*)malloc(service_identifier_len + 1);
210
+ if (!si_tmp) return 0;
211
+ memcpy(si_tmp, service_identifier ? service_identifier : "", service_identifier_len);
212
+ si_tmp[service_identifier_len] = 0;
213
+
214
+ char *sv_tmp = (char*)malloc(service_version_len + 1);
215
+ if (!sv_tmp) { free(si_tmp); return 0; }
216
+ memcpy(sv_tmp, service_version ? service_version : "", service_version_len);
217
+ sv_tmp[service_version_len] = 0;
218
+
219
+ char *sdn_tmp = (char*)malloc(service_display_name_len + 1);
220
+ if (!sdn_tmp) { free(si_tmp); free(sv_tmp); return 0; }
221
+ memcpy(sdn_tmp, service_display_name ? service_display_name : "", service_display_name_len);
222
+ sdn_tmp[service_display_name_len] = 0;
223
+
224
+ char *sam_tmp = (char*)malloc(service_additional_metadata_len + 3); // +3 for "{}" and null
225
+ if (!sam_tmp) { free(si_tmp); free(sv_tmp); free(sdn_tmp); return 0; }
226
+ if (service_additional_metadata && service_additional_metadata_len > 0) {
227
+ memcpy(sam_tmp, service_additional_metadata, service_additional_metadata_len);
228
+ sam_tmp[service_additional_metadata_len] = 0;
229
+ } else {
230
+ strcpy(sam_tmp, "{}");
231
+ }
232
+
233
+ char *gs_tmp = (char*)malloc(git_sha_len + 1);
234
+ if (!gs_tmp) { free(si_tmp); free(sv_tmp); free(sdn_tmp); free(sam_tmp); return 0; }
235
+ memcpy(gs_tmp, git_sha ? git_sha : "", git_sha_len);
236
+ gs_tmp[git_sha_len] = 0;
237
+
238
+ char *it_tmp = (char*)malloc(infrastructure_type_len + 1);
239
+ if (!it_tmp) { free(si_tmp); free(sv_tmp); free(sdn_tmp); free(sam_tmp); free(gs_tmp); return 0; }
240
+ memcpy(it_tmp, infrastructure_type ? infrastructure_type : "", infrastructure_type_len);
241
+ it_tmp[infrastructure_type_len] = 0;
242
+
243
+ char *id_tmp = (char*)malloc(infrastructure_details_len + 3); // +3 for "{}" and null
244
+ if (!id_tmp) { free(si_tmp); free(sv_tmp); free(sdn_tmp); free(sam_tmp); free(gs_tmp); free(it_tmp); return 0; }
245
+ if (infrastructure_details && infrastructure_details_len > 0) {
246
+ memcpy(id_tmp, infrastructure_details, infrastructure_details_len);
247
+ id_tmp[infrastructure_details_len] = 0;
248
+ } else {
249
+ strcpy(id_tmp, "{}");
250
+ }
251
+
252
+ char *sifp_tmp = (char*)malloc(setup_interceptors_file_path_len + 1);
253
+ if (!sifp_tmp) { free(si_tmp); free(sv_tmp); free(sdn_tmp); free(sam_tmp); free(gs_tmp); free(it_tmp); free(id_tmp); return 0; }
254
+ memcpy(sifp_tmp, setup_interceptors_file_path ? setup_interceptors_file_path : "", setup_interceptors_file_path_len);
255
+ sifp_tmp[setup_interceptors_file_path_len] = 0;
256
+
257
+ char *si_esc = json_escape(si_tmp);
258
+ char *sv_esc = json_escape(sv_tmp);
259
+ char *sdn_esc = json_escape(sdn_tmp);
260
+ // DON'T escape serviceAdditionalMetadata - it's already JSON
261
+ // DON'T escape infrastructureDetails - it's already JSON
262
+ char *sam_esc = sam_tmp; // Use raw JSON string
263
+ char *gs_esc = json_escape(gs_tmp);
264
+ char *it_esc = json_escape(it_tmp);
265
+ char *id_esc = id_tmp; // Use raw JSON string
266
+ char *sifp_esc = json_escape(sifp_tmp);
267
+
268
+ // Note: sam_tmp and id_tmp are now used directly (not escaped), so don't free them yet
269
+ free(si_tmp); free(sv_tmp); free(sdn_tmp); free(gs_tmp); free(it_tmp); free(sifp_tmp);
270
+
271
+ if (!si_esc || !sv_esc || !sdn_esc || !sam_esc || !gs_esc || !it_esc || !id_esc || !sifp_esc) {
272
+ free(si_esc); free(sv_esc); free(sdn_esc); free(sam_tmp); free(gs_esc); free(it_esc); free(id_tmp); free(sifp_esc);
273
+ return 0;
274
+ }
275
+
276
+ uint64_t tms = now_ms();
277
+ const char *k_si = ",\"serviceIdentifier\":\"";
278
+ const char *k_sv = "\",\"serviceVersion\":\"";
279
+ const char *k_sdn = "\",\"serviceDisplayName\":\"";
280
+ const char *k_sam = "\",\"serviceAdditionalMetadata\":"; // No quotes - raw JSON
281
+ const char *k_gs = ",\"gitSha\":\"";
282
+ const char *k_it = "\",\"infrastructureType\":\"";
283
+ const char *k_id = "\",\"infrastructureDetails\":"; // No quotes - raw JSON
284
+ const char *k_sifp = ",\"setupInterceptorsFilePath\":\"";
285
+ const char *k_siln = "\",\"setupInterceptorsLineNumber\":";
286
+ const char *k_ts = ",\"timestampMs\":\"";
287
+
288
+ char ts_buf[32];
289
+ int ts_len = snprintf(ts_buf, sizeof(ts_buf), "%llu", (unsigned long long)tms);
290
+ char ln_buf[32];
291
+ int ln_len = snprintf(ln_buf, sizeof(ln_buf), "%d", setup_interceptors_line_number);
292
+
293
+ if (!g_json_prefix_service_identifier) {
294
+ free(si_esc); free(sv_esc); free(sdn_esc); free(sam_esc); free(gs_esc); free(it_esc); free(id_esc); free(sifp_esc);
295
+ return 0;
296
+ }
297
+
298
+ size_t len = strlen(g_json_prefix_service_identifier)
299
+ + strlen(k_si) + strlen(si_esc)
300
+ + strlen(k_sv) + strlen(sv_esc)
301
+ + strlen(k_sdn) + strlen(sdn_esc)
302
+ + strlen(k_sam) + strlen(sam_esc)
303
+ + strlen(k_gs) + strlen(gs_esc)
304
+ + strlen(k_it) + strlen(it_esc)
305
+ + strlen(k_id) + strlen(id_esc)
306
+ + strlen(k_sifp) + strlen(sifp_esc)
307
+ + strlen(k_siln) + (size_t)ln_len
308
+ + strlen(k_ts) + (size_t)ts_len + 1 // +1 for closing quote
309
+ + strlen(JSON_SUFFIX);
310
+
311
+ char *body = (char*)malloc(len + 1);
312
+ if (!body) {
313
+ free(si_esc); free(sv_esc); free(sdn_esc);
314
+ free(sam_esc); // sam_esc points to sam_tmp
315
+ free(gs_esc); free(it_esc);
316
+ free(id_esc); // id_esc points to id_tmp
317
+ free(sifp_esc);
318
+ return 0;
319
+ }
320
+
321
+ char *o = body;
322
+ o += sprintf(o, "%s", g_json_prefix_service_identifier);
323
+ o += sprintf(o, "%s%s", k_si, si_esc);
324
+ o += sprintf(o, "%s%s", k_sv, sv_esc);
325
+ o += sprintf(o, "%s%s", k_sdn, sdn_esc);
326
+ o += sprintf(o, "%s%s", k_sam, sam_esc);
327
+ o += sprintf(o, "%s%s", k_gs, gs_esc);
328
+ o += sprintf(o, "%s%s", k_it, it_esc);
329
+ o += sprintf(o, "%s%s", k_id, id_esc);
330
+ o += sprintf(o, "%s%s", k_sifp, sifp_esc);
331
+ o += sprintf(o, "%s%s", k_siln, ln_buf);
332
+ o += sprintf(o, "%s%s\"", k_ts, ts_buf);
333
+ o += sprintf(o, "%s", JSON_SUFFIX);
334
+ *o = '\0';
335
+
336
+ *out_body = body;
337
+ *out_len = (size_t)(o - body);
338
+
339
+ // Free escaped strings (sam_esc/id_esc point to original sam_tmp/id_tmp buffers)
340
+ free(si_esc); free(sv_esc); free(sdn_esc);
341
+ free(sam_esc); // Frees sam_tmp
342
+ free(gs_esc); free(it_esc);
343
+ free(id_esc); // Frees id_tmp
344
+ free(sifp_esc);
345
+ return 1;
346
+ }
347
+
348
+ // Build DOMAINS body (takes array of domain strings)
349
+ static int build_body_domains(
350
+ const char **domains, size_t domain_count,
351
+ char **out_body, size_t *out_len
352
+ ){
353
+ if (!g_json_prefix_domains) return 0;
354
+
355
+ // Escape each domain and build JSON array
356
+ char **escaped_domains = (char**)calloc(domain_count, sizeof(char*));
357
+ if (!escaped_domains) return 0;
358
+
359
+ size_t total_domains_len = 0;
360
+ for (size_t i = 0; i < domain_count; i++) {
361
+ escaped_domains[i] = json_escape(domains[i] ? domains[i] : "");
362
+ if (!escaped_domains[i]) {
363
+ for (size_t j = 0; j < i; j++) free(escaped_domains[j]);
364
+ free(escaped_domains);
365
+ return 0;
366
+ }
367
+ total_domains_len += strlen(escaped_domains[i]) + 3; // quotes + comma
368
+ }
369
+
370
+ uint64_t tms = now_ms();
371
+ const char *k_domains = ",\"domains\":[";
372
+ const char *k_ts = "],\"timestampMs\":\"";
373
+ char ts_buf[32];
374
+ int ts_len = snprintf(ts_buf, sizeof(ts_buf), "%llu", (unsigned long long)tms);
375
+
376
+ size_t len = strlen(g_json_prefix_domains)
377
+ + strlen(k_domains) + total_domains_len
378
+ + strlen(k_ts) + (size_t)ts_len + 1 // +1 for closing quote
379
+ + strlen(JSON_SUFFIX);
380
+
381
+ char *body = (char*)malloc(len + 1);
382
+ if (!body) {
383
+ for (size_t i = 0; i < domain_count; i++) free(escaped_domains[i]);
384
+ free(escaped_domains);
385
+ return 0;
386
+ }
387
+
388
+ char *o = body;
389
+ o += sprintf(o, "%s", g_json_prefix_domains);
390
+ o += sprintf(o, "%s", k_domains);
391
+
392
+ for (size_t i = 0; i < domain_count; i++) {
393
+ if (i > 0) *o++ = ',';
394
+ o += sprintf(o, "\"%s\"", escaped_domains[i]);
395
+ }
396
+
397
+ o += sprintf(o, "%s%s\"", k_ts, ts_buf);
398
+ o += sprintf(o, "%s", JSON_SUFFIX);
399
+ *o = '\0';
400
+
401
+ *out_body = body;
402
+ *out_len = (size_t)(o - body);
403
+
404
+ for (size_t i = 0; i < domain_count; i++) free(escaped_domains[i]);
405
+ free(escaped_domains);
406
+ return 1;
407
+ }
408
+
409
+ // Build UPDATE SERVICE DETAILS body (similar to domains)
410
+ static int build_body_update_service(
411
+ const char **domains, size_t domain_count,
412
+ char **out_body, size_t *out_len
413
+ ){
414
+ if (!g_json_prefix_update_service) return 0;
415
+
416
+ // Escape each domain and build JSON array
417
+ char **escaped_domains = (char**)calloc(domain_count, sizeof(char*));
418
+ if (!escaped_domains) return 0;
419
+
420
+ size_t total_domains_len = 0;
421
+ for (size_t i = 0; i < domain_count; i++) {
422
+ escaped_domains[i] = json_escape(domains[i] ? domains[i] : "");
423
+ if (!escaped_domains[i]) {
424
+ for (size_t j = 0; j < i; j++) free(escaped_domains[j]);
425
+ free(escaped_domains);
426
+ return 0;
427
+ }
428
+ total_domains_len += strlen(escaped_domains[i]) + 3; // quotes + comma
429
+ }
430
+
431
+ uint64_t tms = now_ms();
432
+ const char *k_domains = ",\"domains\":[";
433
+ const char *k_ts = "],\"timestampMs\":\"";
434
+ char ts_buf[32];
435
+ int ts_len = snprintf(ts_buf, sizeof(ts_buf), "%llu", (unsigned long long)tms);
436
+
437
+ size_t len = strlen(g_json_prefix_update_service)
438
+ + strlen(k_domains) + total_domains_len
439
+ + strlen(k_ts) + (size_t)ts_len + 1 // +1 for closing quote
440
+ + strlen(JSON_SUFFIX);
441
+
442
+ char *body = (char*)malloc(len + 1);
443
+ if (!body) {
444
+ for (size_t i = 0; i < domain_count; i++) free(escaped_domains[i]);
445
+ free(escaped_domains);
446
+ return 0;
447
+ }
448
+
449
+ char *o = body;
450
+ o += sprintf(o, "%s", g_json_prefix_update_service);
451
+ o += sprintf(o, "%s", k_domains);
452
+
453
+ for (size_t i = 0; i < domain_count; i++) {
454
+ if (i > 0) *o++ = ',';
455
+ o += sprintf(o, "\"%s\"", escaped_domains[i]);
456
+ }
457
+
458
+ o += sprintf(o, "%s%s\"", k_ts, ts_buf);
459
+ o += sprintf(o, "%s", JSON_SUFFIX);
460
+ *o = '\0';
461
+
462
+ *out_body = body;
463
+ *out_len = (size_t)(o - body);
464
+
465
+ for (size_t i = 0; i < domain_count; i++) free(escaped_domains[i]);
466
+ free(escaped_domains);
467
+ return 1;
468
+ }
469
+
470
+ // Build COLLECT METADATA body
471
+ static int build_body_collect_metadata(
472
+ const char *session_id, size_t session_id_len,
473
+ const char *user_id, size_t user_id_len,
474
+ const char *traits_json, size_t traits_json_len,
475
+ const char **excluded_fields, size_t excluded_fields_count,
476
+ int override,
477
+ char **out_body, size_t *out_len
478
+ ){
479
+ if (!g_json_prefix_collect_metadata) return 0;
480
+
481
+ // Escape session_id, user_id, and traits_json
482
+ char *sid_tmp = (char*)malloc(session_id_len + 1);
483
+ if (!sid_tmp) return 0;
484
+ memcpy(sid_tmp, session_id ? session_id : "", session_id_len);
485
+ sid_tmp[session_id_len] = 0;
486
+
487
+ char *uid_tmp = (char*)malloc(user_id_len + 1);
488
+ if (!uid_tmp) { free(sid_tmp); return 0; }
489
+ memcpy(uid_tmp, user_id ? user_id : "", user_id_len);
490
+ uid_tmp[user_id_len] = 0;
491
+
492
+ char *tj_tmp = (char*)malloc(traits_json_len + 1);
493
+ if (!tj_tmp) { free(sid_tmp); free(uid_tmp); return 0; }
494
+ memcpy(tj_tmp, traits_json ? traits_json : "", traits_json_len);
495
+ tj_tmp[traits_json_len] = 0;
496
+
497
+ char *sid_esc = json_escape(sid_tmp);
498
+ char *uid_esc = json_escape(uid_tmp);
499
+ char *tj_esc = json_escape(tj_tmp);
500
+ free(sid_tmp); free(uid_tmp); free(tj_tmp);
501
+
502
+ if (!sid_esc || !uid_esc || !tj_esc) {
503
+ free(sid_esc); free(uid_esc); free(tj_esc);
504
+ return 0;
505
+ }
506
+
507
+ // Escape excluded fields array
508
+ char **escaped_fields = NULL;
509
+ size_t total_fields_len = 0;
510
+ if (excluded_fields_count > 0) {
511
+ escaped_fields = (char**)calloc(excluded_fields_count, sizeof(char*));
512
+ if (!escaped_fields) {
513
+ free(sid_esc); free(uid_esc); free(tj_esc);
514
+ return 0;
515
+ }
516
+
517
+ for (size_t i = 0; i < excluded_fields_count; i++) {
518
+ escaped_fields[i] = json_escape(excluded_fields[i] ? excluded_fields[i] : "");
519
+ if (!escaped_fields[i]) {
520
+ for (size_t j = 0; j < i; j++) free(escaped_fields[j]);
521
+ free(escaped_fields);
522
+ free(sid_esc); free(uid_esc); free(tj_esc);
523
+ return 0;
524
+ }
525
+ total_fields_len += strlen(escaped_fields[i]) + 3; // quotes + comma
526
+ }
527
+ }
528
+
529
+ uint64_t tms = now_ms();
530
+ const char *k_sid = ",\"sessionId\":\"";
531
+ const char *k_uid = "\",\"userId\":\"";
532
+ const char *k_tj = "\",\"traitsJson\":\"";
533
+ const char *k_ef = "\",\"excludedFields\":[";
534
+ const char *k_ov = "],\"override\":";
535
+ const char *k_ts = ",\"timestampMs\":\"";
536
+
537
+ char ts_buf[32];
538
+ int ts_len = snprintf(ts_buf, sizeof(ts_buf), "%llu", (unsigned long long)tms);
539
+ const char *override_str = override ? "true" : "false";
540
+
541
+ size_t len = strlen(g_json_prefix_collect_metadata)
542
+ + strlen(k_sid) + strlen(sid_esc)
543
+ + strlen(k_uid) + strlen(uid_esc)
544
+ + strlen(k_tj) + strlen(tj_esc)
545
+ + strlen(k_ef) + total_fields_len
546
+ + strlen(k_ov) + strlen(override_str)
547
+ + strlen(k_ts) + (size_t)ts_len + 1 // +1 for closing quote
548
+ + strlen(JSON_SUFFIX);
549
+
550
+ char *body = (char*)malloc(len + 1);
551
+ if (!body) {
552
+ if (escaped_fields) {
553
+ for (size_t i = 0; i < excluded_fields_count; i++) free(escaped_fields[i]);
554
+ free(escaped_fields);
555
+ }
556
+ free(sid_esc); free(uid_esc); free(tj_esc);
557
+ return 0;
558
+ }
559
+
560
+ char *o = body;
561
+ o += sprintf(o, "%s", g_json_prefix_collect_metadata);
562
+ o += sprintf(o, "%s%s", k_sid, sid_esc);
563
+ o += sprintf(o, "%s%s", k_uid, uid_esc);
564
+ o += sprintf(o, "%s%s", k_tj, tj_esc);
565
+ o += sprintf(o, "%s", k_ef);
566
+
567
+ if (excluded_fields_count > 0) {
568
+ for (size_t i = 0; i < excluded_fields_count; i++) {
569
+ if (i > 0) *o++ = ',';
570
+ o += sprintf(o, "\"%s\"", escaped_fields[i]);
571
+ }
572
+ }
573
+
574
+ o += sprintf(o, "%s%s", k_ov, override_str);
575
+ o += sprintf(o, "%s%s\"", k_ts, ts_buf);
576
+ o += sprintf(o, "%s", JSON_SUFFIX);
577
+ *o = '\0';
578
+
579
+ *out_body = body;
580
+ *out_len = (size_t)(o - body);
581
+
582
+ if (escaped_fields) {
583
+ for (size_t i = 0; i < excluded_fields_count; i++) free(escaped_fields[i]);
584
+ free(escaped_fields);
585
+ }
586
+ free(sid_esc); free(uid_esc); free(tj_esc);
587
+ return 1;
588
+ }
589
+
590
+ // ---------- ring ops ----------
591
+ static inline size_t ring_count(void) {
592
+ size_t h = atomic_load_explicit(&g_head, memory_order_acquire);
593
+ size_t t = atomic_load_explicit(&g_tail, memory_order_acquire);
594
+ return t - h;
595
+ }
596
+ static inline int ring_empty(void) { return ring_count() == 0; }
597
+
598
+ static int ring_push(char *body, size_t len) {
599
+ while (atomic_flag_test_and_set_explicit(&g_push_lock, memory_order_acquire)) {
600
+ // brief spin
601
+ }
602
+ size_t t = atomic_load_explicit(&g_tail, memory_order_relaxed);
603
+ size_t h = atomic_load_explicit(&g_head, memory_order_acquire);
604
+ if ((t - h) >= g_cap) {
605
+ atomic_flag_clear_explicit(&g_push_lock, memory_order_release);
606
+ return 0; // full (drop)
607
+ }
608
+ size_t idx = t % g_cap;
609
+ g_ring[idx].body = body;
610
+ g_ring[idx].len = len;
611
+ atomic_store_explicit(&g_tail, t + 1, memory_order_release);
612
+ atomic_flag_clear_explicit(&g_push_lock, memory_order_release);
613
+
614
+ pthread_mutex_lock(&g_cv_mtx);
615
+ pthread_cond_signal(&g_cv);
616
+ pthread_mutex_unlock(&g_cv_mtx);
617
+ return 1;
618
+ }
619
+
620
+ static int ring_pop(char **body, size_t *len) {
621
+ size_t h = atomic_load_explicit(&g_head, memory_order_relaxed);
622
+ size_t t = atomic_load_explicit(&g_tail, memory_order_acquire);
623
+ if (h == t) return 0;
624
+ size_t idx = h % g_cap;
625
+ *body = g_ring[idx].body;
626
+ *len = g_ring[idx].len;
627
+ g_ring[idx].body = NULL;
628
+ g_ring[idx].len = 0;
629
+ atomic_store_explicit(&g_head, h + 1, memory_order_release);
630
+ return 1;
631
+ }
632
+
633
+ // ---------- curl sink callbacks ----------
634
+ static size_t _sink_write(char *ptr, size_t size, size_t nmemb, void *userdata) {
635
+ (void)ptr; (void)userdata;
636
+ return size * nmemb;
637
+ }
638
+ static size_t _sink_header(char *ptr, size_t size, size_t nmemb, void *userdata) {
639
+ (void)ptr; (void)userdata;
640
+ return size * nmemb;
641
+ }
642
+
643
+ // ---------- sender thread ----------
644
+ static void *sender_main(void *arg) {
645
+ (void)arg;
646
+
647
+ // CRITICAL: Set telemetry guard for this thread (prevents _sfteepreload.c capture)
648
+ sf_guard_enter();
649
+
650
+ // Initialize thread-local curl handle
651
+ g_telem_curl = curl_easy_init();
652
+ if (!g_telem_curl) {
653
+ sf_guard_leave();
654
+ return NULL;
655
+ }
656
+
657
+ // Configure curl handle (copy from global settings)
658
+ curl_easy_setopt(g_telem_curl, CURLOPT_URL, g_url);
659
+ curl_easy_setopt(g_telem_curl, CURLOPT_TCP_KEEPALIVE, 1L);
660
+ curl_easy_setopt(g_telem_curl, CURLOPT_TCP_NODELAY, 1L); // NEW: Eliminate Nagle delay
661
+ curl_easy_setopt(g_telem_curl, CURLOPT_HTTPHEADER, g_hdrs);
662
+ #ifdef CURL_HTTP_VERSION_2TLS
663
+ if (g_http2) {
664
+ curl_easy_setopt(g_telem_curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS);
665
+ }
666
+ #endif
667
+ curl_easy_setopt(g_telem_curl, CURLOPT_SSL_VERIFYPEER, 0L);
668
+ curl_easy_setopt(g_telem_curl, CURLOPT_SSL_VERIFYHOST, 0L);
669
+ curl_easy_setopt(g_telem_curl, CURLOPT_WRITEFUNCTION, _sink_write);
670
+ curl_easy_setopt(g_telem_curl, CURLOPT_HEADERFUNCTION, _sink_header);
671
+
672
+ while (atomic_load(&g_running)) {
673
+ if (ring_empty()) {
674
+ pthread_mutex_lock(&g_cv_mtx);
675
+ if (ring_empty() && atomic_load(&g_running))
676
+ pthread_cond_wait(&g_cv, &g_cv_mtx);
677
+ pthread_mutex_unlock(&g_cv_mtx);
678
+ if (!atomic_load(&g_running)) break;
679
+ }
680
+ char *body = NULL; size_t len = 0;
681
+ while (ring_pop(&body, &len)) {
682
+ if (!body) continue;
683
+
684
+ // Use thread-local curl handle (each thread has its own persistent connection)
685
+ curl_easy_setopt(g_telem_curl, CURLOPT_POSTFIELDS, body);
686
+ curl_easy_setopt(g_telem_curl, CURLOPT_POSTFIELDSIZE, (long)len);
687
+ (void)curl_easy_perform(g_telem_curl); // fire-and-forget
688
+
689
+ free(body);
690
+ if (!atomic_load(&g_running)) break;
691
+ }
692
+ }
693
+ if (g_telem_curl) {
694
+ curl_easy_cleanup(g_telem_curl);
695
+ g_telem_curl = NULL;
696
+ }
697
+ sf_guard_leave();
698
+ return NULL;
699
+ }
700
+
701
+ // ---------- Python API ----------
702
+ static PyObject *py_init(PyObject *self, PyObject *args, PyObject *kw) {
703
+ const char *url, *query, *api_key, *service_uuid, *library, *version;
704
+ int http2 = 0;
705
+ static char *kwlist[] = {"url","query","api_key","service_uuid","library","version","http2", NULL};
706
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "ssssssi",
707
+ kwlist, &url, &query, &api_key, &service_uuid, &library, &version, &http2)) {
708
+ Py_RETURN_FALSE;
709
+ }
710
+ if (g_running) Py_RETURN_TRUE;
711
+
712
+ g_url = str_dup(url);
713
+ g_api_key = str_dup(api_key);
714
+ g_service_uuid = str_dup(service_uuid);
715
+ g_library = str_dup(library);
716
+ g_version = str_dup(version);
717
+ g_http2 = http2 ? 1 : 0;
718
+ if (!g_url || !g_api_key || !g_service_uuid || !g_library || !g_version) {
719
+ Py_RETURN_FALSE;
720
+ }
721
+
722
+ g_cap = SFS_RING_CAP;
723
+ g_ring = (sfs_msg_t*)calloc(g_cap, sizeof(sfs_msg_t));
724
+ if (!g_ring) { Py_RETURN_FALSE; }
725
+
726
+ // Parse SF_SERVICE_SENDER_THREADS environment variable
727
+ const char *env_threads = getenv("SF_SERVICE_SENDER_THREADS");
728
+ if (env_threads) {
729
+ int t = atoi(env_threads);
730
+ if (t > 0 && t <= MAX_SENDER_THREADS) {
731
+ g_configured_sender_threads = t;
732
+ }
733
+ }
734
+
735
+ // Initialize curl (shared headers only - handles are per-thread)
736
+ curl_global_init(CURL_GLOBAL_DEFAULT);
737
+ g_hdrs = NULL;
738
+ g_hdrs = curl_slist_append(g_hdrs, "Content-Type: application/json");
739
+
740
+ // Start sender thread pool
741
+ atomic_store(&g_running, 1);
742
+ g_num_sender_threads = g_configured_sender_threads;
743
+ for (int i = 0; i < g_num_sender_threads; i++) {
744
+ if (pthread_create(&g_sender_threads[i], NULL, sender_main, NULL) != 0) {
745
+ atomic_store(&g_running, 0);
746
+ // Clean up already-started threads
747
+ for (int j = 0; j < i; j++) {
748
+ pthread_join(g_sender_threads[j], NULL);
749
+ }
750
+ Py_RETURN_FALSE;
751
+ }
752
+ }
753
+ Py_RETURN_TRUE;
754
+ }
755
+
756
+ static PyObject *py_init_service_identifier(PyObject *self, PyObject *args, PyObject *kw) {
757
+ const char *url, *query, *api_key, *service_uuid, *library, *version;
758
+ int http2 = 1;
759
+ static char *kwlist[] = {"url","query","api_key","service_uuid","library","version","http2",NULL};
760
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "ssssssi", kwlist,
761
+ &url, &query, &api_key, &service_uuid, &library, &version, &http2)) {
762
+ Py_RETURN_FALSE;
763
+ }
764
+ if (g_service_identifier_query_escaped) { free(g_service_identifier_query_escaped); g_service_identifier_query_escaped = NULL; }
765
+ if (g_json_prefix_service_identifier) { free(g_json_prefix_service_identifier); g_json_prefix_service_identifier = NULL; }
766
+
767
+ g_service_identifier_query_escaped = json_escape_query(query);
768
+ if (!g_service_identifier_query_escaped) Py_RETURN_FALSE;
769
+ if (!build_prefix_for_query(g_service_identifier_query_escaped, &g_json_prefix_service_identifier)) {
770
+ Py_RETURN_FALSE;
771
+ }
772
+ Py_RETURN_TRUE;
773
+ }
774
+
775
+ static PyObject *py_init_domains(PyObject *self, PyObject *args, PyObject *kw) {
776
+ const char *url, *query, *api_key, *service_uuid, *library, *version;
777
+ int http2 = 1;
778
+ static char *kwlist[] = {"url","query","api_key","service_uuid","library","version","http2",NULL};
779
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "ssssssi", kwlist,
780
+ &url, &query, &api_key, &service_uuid, &library, &version, &http2)) {
781
+ Py_RETURN_FALSE;
782
+ }
783
+ if (g_domains_query_escaped) { free(g_domains_query_escaped); g_domains_query_escaped = NULL; }
784
+ if (g_json_prefix_domains) { free(g_json_prefix_domains); g_json_prefix_domains = NULL; }
785
+
786
+ g_domains_query_escaped = json_escape_query(query);
787
+ if (!g_domains_query_escaped) Py_RETURN_FALSE;
788
+ if (!build_prefix_for_query(g_domains_query_escaped, &g_json_prefix_domains)) {
789
+ Py_RETURN_FALSE;
790
+ }
791
+ Py_RETURN_TRUE;
792
+ }
793
+
794
+ static PyObject *py_init_update_service(PyObject *self, PyObject *args, PyObject *kw) {
795
+ const char *url, *query, *api_key, *service_uuid, *library, *version;
796
+ int http2 = 1;
797
+ static char *kwlist[] = {"url","query","api_key","service_uuid","library","version","http2",NULL};
798
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "ssssssi", kwlist,
799
+ &url, &query, &api_key, &service_uuid, &library, &version, &http2)) {
800
+ Py_RETURN_FALSE;
801
+ }
802
+ if (g_update_service_query_escaped) { free(g_update_service_query_escaped); g_update_service_query_escaped = NULL; }
803
+ if (g_json_prefix_update_service) { free(g_json_prefix_update_service); g_json_prefix_update_service = NULL; }
804
+
805
+ g_update_service_query_escaped = json_escape_query(query);
806
+ if (!g_update_service_query_escaped) Py_RETURN_FALSE;
807
+ if (!build_prefix_for_query(g_update_service_query_escaped, &g_json_prefix_update_service)) {
808
+ Py_RETURN_FALSE;
809
+ }
810
+ Py_RETURN_TRUE;
811
+ }
812
+
813
+ static PyObject *py_init_collect_metadata(PyObject *self, PyObject *args, PyObject *kw) {
814
+ const char *url, *query, *api_key, *service_uuid, *library, *version;
815
+ int http2 = 1;
816
+ static char *kwlist[] = {"url","query","api_key","service_uuid","library","version","http2",NULL};
817
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "ssssssi", kwlist,
818
+ &url, &query, &api_key, &service_uuid, &library, &version, &http2)) {
819
+ Py_RETURN_FALSE;
820
+ }
821
+ if (g_collect_metadata_query_escaped) { free(g_collect_metadata_query_escaped); g_collect_metadata_query_escaped = NULL; }
822
+ if (g_json_prefix_collect_metadata) { free(g_json_prefix_collect_metadata); g_json_prefix_collect_metadata = NULL; }
823
+
824
+ g_collect_metadata_query_escaped = json_escape_query(query);
825
+ if (!g_collect_metadata_query_escaped) Py_RETURN_FALSE;
826
+ if (!build_prefix_for_query(g_collect_metadata_query_escaped, &g_json_prefix_collect_metadata)) {
827
+ Py_RETURN_FALSE;
828
+ }
829
+ Py_RETURN_TRUE;
830
+ }
831
+
832
+ static PyObject *py_service_identifier(PyObject *self, PyObject *args, PyObject *kw) {
833
+ fprintf(stderr, "[DEBUG _sfservice.c] py_service_identifier: ENTRY\n");
834
+ fflush(stderr);
835
+
836
+ const char *service_identifier, *service_version, *service_display_name, *service_additional_metadata;
837
+ const char *git_sha, *infrastructure_type, *infrastructure_details;
838
+ const char *setup_interceptors_file_path;
839
+ Py_ssize_t si_len = 0, sv_len = 0, sdn_len = 0, sam_len = 0, gs_len = 0;
840
+ Py_ssize_t it_len = 0, id_len = 0, sifp_len = 0;
841
+ int setup_interceptors_line_number = 0;
842
+
843
+ static char *kwlist[] = {
844
+ "service_identifier", "service_version", "service_display_name", "service_additional_metadata",
845
+ "git_sha", "infrastructure_type", "infrastructure_details",
846
+ "setup_interceptors_file_path", "setup_interceptors_line_number", NULL
847
+ };
848
+
849
+ fprintf(stderr, "[DEBUG _sfservice.c] py_service_identifier: parsing arguments\n");
850
+ fflush(stderr);
851
+
852
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "s#s#s#s#s#s#s#s#i", kwlist,
853
+ &service_identifier, &si_len,
854
+ &service_version, &sv_len,
855
+ &service_display_name, &sdn_len,
856
+ &service_additional_metadata, &sam_len,
857
+ &git_sha, &gs_len,
858
+ &infrastructure_type, &it_len,
859
+ &infrastructure_details, &id_len,
860
+ &setup_interceptors_file_path, &sifp_len,
861
+ &setup_interceptors_line_number)) {
862
+ fprintf(stderr, "[DEBUG _sfservice.c] py_service_identifier: argument parsing FAILED\n");
863
+ fflush(stderr);
864
+ return NULL; // PyArg_ParseTupleAndKeywords already set exception
865
+ }
866
+
867
+ fprintf(stderr, "[DEBUG _sfservice.c] py_service_identifier: arguments parsed OK\n");
868
+ fflush(stderr);
869
+
870
+ if (!g_running || g_json_prefix_service_identifier == NULL) {
871
+ fprintf(stderr, "[DEBUG _sfservice.c] py_service_identifier: not running or prefix NULL, returning None\n");
872
+ fflush(stderr);
873
+ Py_RETURN_NONE;
874
+ }
875
+
876
+ // OPTIMIZATION: Release GIL during JSON building + ring push
877
+ // All string arguments are already C strings from PyArg_ParseTupleAndKeywords,
878
+ // so we can safely release GIL for the entire body building + transmission.
879
+ // This extends GIL-free duration from ~100ns to ~500-2000ns (5-20x improvement).
880
+ char *body = NULL;
881
+ size_t len = 0;
882
+ int ok = 0;
883
+
884
+ fprintf(stderr, "[DEBUG _sfservice.c] py_service_identifier: about to build body\n");
885
+ fflush(stderr);
886
+
887
+ Py_BEGIN_ALLOW_THREADS
888
+ // Build JSON body (WITHOUT GIL - pure C string operations)
889
+ if (build_body_service_identifier(
890
+ service_identifier, (size_t)si_len,
891
+ service_version, (size_t)sv_len,
892
+ service_display_name, (size_t)sdn_len,
893
+ service_additional_metadata, (size_t)sam_len,
894
+ git_sha, (size_t)gs_len,
895
+ infrastructure_type, (size_t)it_len,
896
+ infrastructure_details, (size_t)id_len,
897
+ setup_interceptors_file_path, (size_t)sifp_len,
898
+ setup_interceptors_line_number,
899
+ &body, &len)) {
900
+ // Push to ring buffer (WITHOUT GIL)
901
+ ok = ring_push(body, len);
902
+ }
903
+ Py_END_ALLOW_THREADS
904
+
905
+ fprintf(stderr, "[DEBUG _sfservice.c] py_service_identifier: body built, ok=%d\n", ok);
906
+ fflush(stderr);
907
+
908
+ if (!ok && body) free(body);
909
+
910
+ fprintf(stderr, "[DEBUG _sfservice.c] py_service_identifier: returning None\n");
911
+ fflush(stderr);
912
+
913
+ Py_RETURN_NONE;
914
+ }
915
+
916
+ static PyObject *py_domains(PyObject *self, PyObject *args, PyObject *kw) {
917
+ fprintf(stderr, "[DEBUG _sfservice.c] py_domains: ENTRY\n");
918
+ fflush(stderr);
919
+
920
+ PyObject *domains_list = NULL;
921
+ static char *kwlist[] = {"domains", NULL};
922
+
923
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "O", kwlist, &domains_list)) {
924
+ fprintf(stderr, "[DEBUG _sfservice.c] py_domains: argument parsing FAILED\n");
925
+ fflush(stderr);
926
+ return NULL; // PyArg_ParseTupleAndKeywords already set exception
927
+ }
928
+
929
+ fprintf(stderr, "[DEBUG _sfservice.c] py_domains: arguments parsed OK\n");
930
+ fflush(stderr);
931
+
932
+ if (!g_running || g_json_prefix_domains == NULL) {
933
+ fprintf(stderr, "[DEBUG _sfservice.c] py_domains: not running or prefix NULL\n");
934
+ fflush(stderr);
935
+ Py_RETURN_NONE;
936
+ }
937
+
938
+ if (!PyList_Check(domains_list)) {
939
+ fprintf(stderr, "[DEBUG _sfservice.c] py_domains: domains_list is not a list\n");
940
+ fflush(stderr);
941
+ PyErr_SetString(PyExc_TypeError, "domains must be a list");
942
+ return NULL; // Return NULL when exception is set
943
+ }
944
+
945
+ Py_ssize_t domain_count = PyList_Size(domains_list);
946
+ if (domain_count == 0) Py_RETURN_NONE;
947
+
948
+ const char **domains = (const char**)malloc(sizeof(char*) * domain_count);
949
+ if (!domains) Py_RETURN_NONE;
950
+
951
+ for (Py_ssize_t i = 0; i < domain_count; i++) {
952
+ PyObject *item = PyList_GetItem(domains_list, i);
953
+ if (!PyUnicode_Check(item)) {
954
+ free(domains);
955
+ PyErr_SetString(PyExc_TypeError, "all domains must be strings");
956
+ return NULL; // Return NULL when exception is set
957
+ }
958
+ domains[i] = PyUnicode_AsUTF8(item);
959
+ }
960
+
961
+ // OPTIMIZATION: Release GIL during JSON building + ring push
962
+ // All string arguments are already C strings from PyArg_ParseTupleAndKeywords,
963
+ // so we can safely release GIL for the entire body building + transmission.
964
+ char *body = NULL;
965
+ size_t len = 0;
966
+ int ok = 0;
967
+
968
+ Py_BEGIN_ALLOW_THREADS
969
+ // Build JSON body (WITHOUT GIL - pure C string operations)
970
+ if (build_body_domains(domains, (size_t)domain_count, &body, &len)) {
971
+ // Push to ring buffer (WITHOUT GIL)
972
+ ok = ring_push(body, len);
973
+ }
974
+ Py_END_ALLOW_THREADS
975
+
976
+ free(domains);
977
+ if (!ok && body) free(body);
978
+ Py_RETURN_NONE;
979
+ }
980
+
981
+ static PyObject *py_update_service(PyObject *self, PyObject *args, PyObject *kw) {
982
+ PyObject *domains_list = NULL;
983
+ static char *kwlist[] = {"domains", NULL};
984
+
985
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "O", kwlist, &domains_list)) {
986
+ Py_RETURN_NONE;
987
+ }
988
+ if (!g_running || g_json_prefix_update_service == NULL) Py_RETURN_NONE;
989
+
990
+ if (!PyList_Check(domains_list)) {
991
+ PyErr_SetString(PyExc_TypeError, "domains must be a list");
992
+ Py_RETURN_NONE;
993
+ }
994
+
995
+ Py_ssize_t domain_count = PyList_Size(domains_list);
996
+ if (domain_count == 0) Py_RETURN_NONE;
997
+
998
+ const char **domains = (const char**)malloc(sizeof(char*) * domain_count);
999
+ if (!domains) Py_RETURN_NONE;
1000
+
1001
+ for (Py_ssize_t i = 0; i < domain_count; i++) {
1002
+ PyObject *item = PyList_GetItem(domains_list, i);
1003
+ if (!PyUnicode_Check(item)) {
1004
+ free(domains);
1005
+ PyErr_SetString(PyExc_TypeError, "all domains must be strings");
1006
+ Py_RETURN_NONE;
1007
+ }
1008
+ domains[i] = PyUnicode_AsUTF8(item);
1009
+ }
1010
+
1011
+ // OPTIMIZATION: Release GIL during JSON building + ring push
1012
+ // All string arguments are already C strings from PyArg_ParseTupleAndKeywords,
1013
+ // so we can safely release GIL for the entire body building + transmission.
1014
+ char *body = NULL;
1015
+ size_t len = 0;
1016
+ int ok = 0;
1017
+
1018
+ Py_BEGIN_ALLOW_THREADS
1019
+ // Build JSON body (WITHOUT GIL - pure C string operations)
1020
+ if (build_body_update_service(domains, (size_t)domain_count, &body, &len)) {
1021
+ // Push to ring buffer (WITHOUT GIL)
1022
+ ok = ring_push(body, len);
1023
+ }
1024
+ Py_END_ALLOW_THREADS
1025
+
1026
+ free(domains);
1027
+ if (!ok && body) free(body);
1028
+ Py_RETURN_NONE;
1029
+ }
1030
+
1031
+ static PyObject *py_collect_metadata(PyObject *self, PyObject *args, PyObject *kw) {
1032
+ const char *session_id, *user_id, *traits_json;
1033
+ Py_ssize_t session_id_len = 0, user_id_len = 0, traits_json_len = 0;
1034
+ PyObject *excluded_fields_list = NULL;
1035
+ int override = 0;
1036
+
1037
+ static char *kwlist[] = {"session_id", "user_id", "traits_json", "excluded_fields", "override", NULL};
1038
+
1039
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "s#s#s#O|p", kwlist,
1040
+ &session_id, &session_id_len,
1041
+ &user_id, &user_id_len,
1042
+ &traits_json, &traits_json_len,
1043
+ &excluded_fields_list,
1044
+ &override)) {
1045
+ Py_RETURN_NONE;
1046
+ }
1047
+ if (!g_running || g_json_prefix_collect_metadata == NULL) Py_RETURN_NONE;
1048
+
1049
+ if (!PyList_Check(excluded_fields_list)) {
1050
+ PyErr_SetString(PyExc_TypeError, "excluded_fields must be a list");
1051
+ Py_RETURN_NONE;
1052
+ }
1053
+
1054
+ Py_ssize_t excluded_fields_count = PyList_Size(excluded_fields_list);
1055
+ const char **excluded_fields = NULL;
1056
+
1057
+ if (excluded_fields_count > 0) {
1058
+ excluded_fields = (const char**)malloc(sizeof(char*) * excluded_fields_count);
1059
+ if (!excluded_fields) Py_RETURN_NONE;
1060
+
1061
+ for (Py_ssize_t i = 0; i < excluded_fields_count; i++) {
1062
+ PyObject *item = PyList_GetItem(excluded_fields_list, i);
1063
+ if (!PyUnicode_Check(item)) {
1064
+ free(excluded_fields);
1065
+ PyErr_SetString(PyExc_TypeError, "all excluded_fields must be strings");
1066
+ Py_RETURN_NONE;
1067
+ }
1068
+ excluded_fields[i] = PyUnicode_AsUTF8(item);
1069
+ }
1070
+ }
1071
+
1072
+ // OPTIMIZATION: Release GIL during JSON building + ring push
1073
+ // All string arguments are already C strings from PyArg_ParseTupleAndKeywords,
1074
+ // so we can safely release GIL for the entire body building + transmission.
1075
+ // This extends GIL-free duration from ~100ns to ~500-2000ns (5-20x improvement).
1076
+ char *body = NULL;
1077
+ size_t len = 0;
1078
+ int ok = 0;
1079
+
1080
+ Py_BEGIN_ALLOW_THREADS
1081
+ // Build JSON body (WITHOUT GIL - pure C string operations)
1082
+ if (build_body_collect_metadata(
1083
+ session_id, (size_t)session_id_len,
1084
+ user_id, (size_t)user_id_len,
1085
+ traits_json, (size_t)traits_json_len,
1086
+ excluded_fields, (size_t)excluded_fields_count,
1087
+ override,
1088
+ &body, &len)) {
1089
+ // Push to ring buffer (WITHOUT GIL)
1090
+ ok = ring_push(body, len);
1091
+ }
1092
+ Py_END_ALLOW_THREADS
1093
+
1094
+ if (excluded_fields) free(excluded_fields);
1095
+ if (!ok && body) free(body);
1096
+ Py_RETURN_NONE;
1097
+ }
1098
+
1099
+ // Convenience wrapper: identify (calls collect_metadata)
1100
+ static PyObject *py_identify(PyObject *self, PyObject *args, PyObject *kw) {
1101
+ const char *session_id, *user_id, *traits_json;
1102
+ Py_ssize_t session_id_len = 0, user_id_len = 0, traits_json_len = 0;
1103
+ PyObject *excluded_fields_list = NULL;
1104
+ int override = 0;
1105
+
1106
+ static char *kwlist[] = {"session_id", "user_id", "traits_json", "excluded_fields", "override", NULL};
1107
+
1108
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "s#s#s#O|p", kwlist,
1109
+ &session_id, &session_id_len,
1110
+ &user_id, &user_id_len,
1111
+ &traits_json, &traits_json_len,
1112
+ &excluded_fields_list,
1113
+ &override)) {
1114
+ Py_RETURN_NONE;
1115
+ }
1116
+
1117
+ // Just delegate to collect_metadata
1118
+ return py_collect_metadata(self, args, kw);
1119
+ }
1120
+
1121
+ // Convenience wrapper: add_or_update_metadata (calls collect_metadata)
1122
+ static PyObject *py_add_or_update_metadata(PyObject *self, PyObject *args, PyObject *kw) {
1123
+ const char *session_id, *user_id, *traits_json;
1124
+ Py_ssize_t session_id_len = 0, user_id_len = 0, traits_json_len = 0;
1125
+ PyObject *excluded_fields_list = NULL;
1126
+ int override = 0;
1127
+
1128
+ static char *kwlist[] = {"session_id", "user_id", "traits_json", "excluded_fields", "override", NULL};
1129
+
1130
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "s#s#s#O|p", kwlist,
1131
+ &session_id, &session_id_len,
1132
+ &user_id, &user_id_len,
1133
+ &traits_json, &traits_json_len,
1134
+ &excluded_fields_list,
1135
+ &override)) {
1136
+ Py_RETURN_NONE;
1137
+ }
1138
+
1139
+ // Just delegate to collect_metadata
1140
+ return py_collect_metadata(self, args, kw);
1141
+ }
1142
+
1143
+ static PyObject *py_shutdown(PyObject *self, PyObject *args) {
1144
+ if (!g_running) Py_RETURN_NONE;
1145
+
1146
+ atomic_store(&g_running, 0);
1147
+
1148
+ // Wake ALL threads with broadcast (not signal)
1149
+ pthread_mutex_lock(&g_cv_mtx);
1150
+ pthread_cond_broadcast(&g_cv);
1151
+ pthread_mutex_unlock(&g_cv_mtx);
1152
+
1153
+ // Join all sender threads in thread pool
1154
+ for (int i = 0; i < g_num_sender_threads; i++) {
1155
+ if (g_sender_threads[i]) {
1156
+ pthread_join(g_sender_threads[i], NULL);
1157
+ g_sender_threads[i] = 0;
1158
+ }
1159
+ }
1160
+ g_num_sender_threads = 0;
1161
+
1162
+ // Cleanup curl (per-thread handles cleaned by pthread_cleanup_push)
1163
+ if (g_hdrs) { curl_slist_free_all(g_hdrs); g_hdrs = NULL; }
1164
+ curl_global_cleanup();
1165
+
1166
+ // Free all config strings and NULL pointers
1167
+ free(g_url); g_url = NULL;
1168
+
1169
+ free(g_service_identifier_query_escaped); g_service_identifier_query_escaped = NULL;
1170
+ free(g_json_prefix_service_identifier); g_json_prefix_service_identifier = NULL;
1171
+
1172
+ free(g_domains_query_escaped); g_domains_query_escaped = NULL;
1173
+ free(g_json_prefix_domains); g_json_prefix_domains = NULL;
1174
+
1175
+ free(g_update_service_query_escaped); g_update_service_query_escaped = NULL;
1176
+ free(g_json_prefix_update_service); g_json_prefix_update_service = NULL;
1177
+
1178
+ free(g_collect_metadata_query_escaped); g_collect_metadata_query_escaped = NULL;
1179
+ free(g_json_prefix_collect_metadata); g_json_prefix_collect_metadata = NULL;
1180
+
1181
+ free(g_api_key); g_api_key = NULL;
1182
+ free(g_service_uuid); g_service_uuid = NULL;
1183
+ free(g_library); g_library = NULL;
1184
+ free(g_version); g_version = NULL;
1185
+
1186
+ // Drain and free ring buffer
1187
+ if (g_ring) {
1188
+ char *b; size_t l;
1189
+ while (ring_pop(&b, &l)) free(b);
1190
+ free(g_ring); g_ring = NULL;
1191
+ }
1192
+
1193
+ Py_RETURN_NONE;
1194
+ }
1195
+
1196
+ // ---------- Module table ----------
1197
+ static PyMethodDef SFServiceMethods[] = {
1198
+ {"init", (PyCFunction)py_init, METH_VARARGS | METH_KEYWORDS, "Init and start sender"},
1199
+ {"init_service_identifier", (PyCFunction)py_init_service_identifier,METH_VARARGS | METH_KEYWORDS, "Init (service identifier) query/prefix"},
1200
+ {"init_domains", (PyCFunction)py_init_domains, METH_VARARGS | METH_KEYWORDS, "Init (domains) query/prefix"},
1201
+ {"init_update_service", (PyCFunction)py_init_update_service, METH_VARARGS | METH_KEYWORDS, "Init (update service) query/prefix"},
1202
+ {"init_collect_metadata", (PyCFunction)py_init_collect_metadata, METH_VARARGS | METH_KEYWORDS, "Init (collect metadata) query/prefix"},
1203
+ {"service_identifier", (PyCFunction)py_service_identifier, METH_VARARGS | METH_KEYWORDS, "Send service identifier"},
1204
+ {"domains", (PyCFunction)py_domains, METH_VARARGS | METH_KEYWORDS, "Send domains"},
1205
+ {"update_service", (PyCFunction)py_update_service, METH_VARARGS | METH_KEYWORDS, "Send update service"},
1206
+ {"collect_metadata", (PyCFunction)py_collect_metadata, METH_VARARGS | METH_KEYWORDS, "Send collect metadata"},
1207
+ {"identify", (PyCFunction)py_identify, METH_VARARGS | METH_KEYWORDS, "Identify user (alias for collect_metadata)"},
1208
+ {"add_or_update_metadata", (PyCFunction)py_add_or_update_metadata, METH_VARARGS | METH_KEYWORDS, "Add/update metadata (alias for collect_metadata)"},
1209
+ {"shutdown", (PyCFunction)py_shutdown, METH_NOARGS, "Shutdown sender and free state"},
1210
+ {NULL, NULL, 0, NULL}
1211
+ };
1212
+
1213
+ static struct PyModuleDef sfservicemodule = {
1214
+ PyModuleDef_HEAD_INIT,
1215
+ "_sfservice",
1216
+ "sf_veritas ultra-fast service operations",
1217
+ -1,
1218
+ SFServiceMethods
1219
+ };
1220
+
1221
+ PyMODINIT_FUNC PyInit__sfservice(void) {
1222
+ return PyModule_Create(&sfservicemodule);
1223
+ }