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