sf-veritas 0.10.5__cp313-cp313-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 (133) hide show
  1. sf_veritas/__init__.py +20 -0
  2. sf_veritas/_sffastlog.c +889 -0
  3. sf_veritas/_sffastlog.cpython-313-x86_64-linux-gnu.so +0 -0
  4. sf_veritas/_sffastnet.c +924 -0
  5. sf_veritas/_sffastnet.cpython-313-x86_64-linux-gnu.so +0 -0
  6. sf_veritas/_sffastnetworkrequest.c +730 -0
  7. sf_veritas/_sffastnetworkrequest.cpython-313-x86_64-linux-gnu.so +0 -0
  8. sf_veritas/_sffuncspan.c +2155 -0
  9. sf_veritas/_sffuncspan.cpython-313-x86_64-linux-gnu.so +0 -0
  10. sf_veritas/_sffuncspan_config.c +617 -0
  11. sf_veritas/_sffuncspan_config.cpython-313-x86_64-linux-gnu.so +0 -0
  12. sf_veritas/_sfheadercheck.c +341 -0
  13. sf_veritas/_sfheadercheck.cpython-313-x86_64-linux-gnu.so +0 -0
  14. sf_veritas/_sfnetworkhop.c +1451 -0
  15. sf_veritas/_sfnetworkhop.cpython-313-x86_64-linux-gnu.so +0 -0
  16. sf_veritas/_sfservice.c +1175 -0
  17. sf_veritas/_sfservice.cpython-313-x86_64-linux-gnu.so +0 -0
  18. sf_veritas/_sfteepreload.c +5167 -0
  19. sf_veritas/app_config.py +50 -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/_patch_tracker.py +74 -0
  49. sf_veritas/patches/concurrent_futures.py +19 -0
  50. sf_veritas/patches/constants.py +1 -0
  51. sf_veritas/patches/exceptions.py +82 -0
  52. sf_veritas/patches/multiprocessing.py +32 -0
  53. sf_veritas/patches/network_libraries/__init__.py +76 -0
  54. sf_veritas/patches/network_libraries/aiohttp.py +281 -0
  55. sf_veritas/patches/network_libraries/curl_cffi.py +363 -0
  56. sf_veritas/patches/network_libraries/http_client.py +419 -0
  57. sf_veritas/patches/network_libraries/httpcore.py +515 -0
  58. sf_veritas/patches/network_libraries/httplib2.py +204 -0
  59. sf_veritas/patches/network_libraries/httpx.py +544 -0
  60. sf_veritas/patches/network_libraries/niquests.py +211 -0
  61. sf_veritas/patches/network_libraries/pycurl.py +392 -0
  62. sf_veritas/patches/network_libraries/requests.py +639 -0
  63. sf_veritas/patches/network_libraries/tornado.py +341 -0
  64. sf_veritas/patches/network_libraries/treq.py +270 -0
  65. sf_veritas/patches/network_libraries/urllib_request.py +477 -0
  66. sf_veritas/patches/network_libraries/utils.py +398 -0
  67. sf_veritas/patches/os.py +17 -0
  68. sf_veritas/patches/threading.py +218 -0
  69. sf_veritas/patches/web_frameworks/__init__.py +54 -0
  70. sf_veritas/patches/web_frameworks/aiohttp.py +793 -0
  71. sf_veritas/patches/web_frameworks/async_websocket_consumer.py +317 -0
  72. sf_veritas/patches/web_frameworks/blacksheep.py +527 -0
  73. sf_veritas/patches/web_frameworks/bottle.py +502 -0
  74. sf_veritas/patches/web_frameworks/cherrypy.py +678 -0
  75. sf_veritas/patches/web_frameworks/cors_utils.py +122 -0
  76. sf_veritas/patches/web_frameworks/django.py +944 -0
  77. sf_veritas/patches/web_frameworks/eve.py +395 -0
  78. sf_veritas/patches/web_frameworks/falcon.py +926 -0
  79. sf_veritas/patches/web_frameworks/fastapi.py +724 -0
  80. sf_veritas/patches/web_frameworks/flask.py +520 -0
  81. sf_veritas/patches/web_frameworks/klein.py +501 -0
  82. sf_veritas/patches/web_frameworks/litestar.py +551 -0
  83. sf_veritas/patches/web_frameworks/pyramid.py +428 -0
  84. sf_veritas/patches/web_frameworks/quart.py +824 -0
  85. sf_veritas/patches/web_frameworks/robyn.py +697 -0
  86. sf_veritas/patches/web_frameworks/sanic.py +857 -0
  87. sf_veritas/patches/web_frameworks/starlette.py +723 -0
  88. sf_veritas/patches/web_frameworks/strawberry.py +813 -0
  89. sf_veritas/patches/web_frameworks/tornado.py +481 -0
  90. sf_veritas/patches/web_frameworks/utils.py +91 -0
  91. sf_veritas/print_override.py +13 -0
  92. sf_veritas/regular_data_transmitter.py +409 -0
  93. sf_veritas/request_interceptor.py +401 -0
  94. sf_veritas/request_utils.py +550 -0
  95. sf_veritas/server_status.py +1 -0
  96. sf_veritas/shutdown_flag.py +11 -0
  97. sf_veritas/subprocess_startup.py +3 -0
  98. sf_veritas/test_cli.py +145 -0
  99. sf_veritas/thread_local.py +970 -0
  100. sf_veritas/timeutil.py +114 -0
  101. sf_veritas/transmit_exception_to_sailfish.py +28 -0
  102. sf_veritas/transmitter.py +132 -0
  103. sf_veritas/types.py +47 -0
  104. sf_veritas/unified_interceptor.py +1586 -0
  105. sf_veritas/utils.py +39 -0
  106. sf_veritas-0.10.5.dist-info/METADATA +97 -0
  107. sf_veritas-0.10.5.dist-info/RECORD +133 -0
  108. sf_veritas-0.10.5.dist-info/WHEEL +5 -0
  109. sf_veritas-0.10.5.dist-info/entry_points.txt +2 -0
  110. sf_veritas-0.10.5.dist-info/top_level.txt +1 -0
  111. sf_veritas.libs/libbrotlicommon-6ce2a53c.so.1.0.6 +0 -0
  112. sf_veritas.libs/libbrotlidec-811d1be3.so.1.0.6 +0 -0
  113. sf_veritas.libs/libcom_err-730ca923.so.2.1 +0 -0
  114. sf_veritas.libs/libcrypt-52aca757.so.1.1.0 +0 -0
  115. sf_veritas.libs/libcrypto-bdaed0ea.so.1.1.1k +0 -0
  116. sf_veritas.libs/libcurl-eaa3cf66.so.4.5.0 +0 -0
  117. sf_veritas.libs/libgssapi_krb5-323bbd21.so.2.2 +0 -0
  118. sf_veritas.libs/libidn2-2f4a5893.so.0.3.6 +0 -0
  119. sf_veritas.libs/libk5crypto-9a74ff38.so.3.1 +0 -0
  120. sf_veritas.libs/libkeyutils-2777d33d.so.1.6 +0 -0
  121. sf_veritas.libs/libkrb5-a55300e8.so.3.3 +0 -0
  122. sf_veritas.libs/libkrb5support-e6594cfc.so.0.1 +0 -0
  123. sf_veritas.libs/liblber-2-d20824ef.4.so.2.10.9 +0 -0
  124. sf_veritas.libs/libldap-2-cea2a960.4.so.2.10.9 +0 -0
  125. sf_veritas.libs/libnghttp2-39367a22.so.14.17.0 +0 -0
  126. sf_veritas.libs/libpcre2-8-516f4c9d.so.0.7.1 +0 -0
  127. sf_veritas.libs/libpsl-99becdd3.so.5.3.1 +0 -0
  128. sf_veritas.libs/libsasl2-7de4d792.so.3.0.0 +0 -0
  129. sf_veritas.libs/libselinux-d0805dcb.so.1 +0 -0
  130. sf_veritas.libs/libssh-c11d285b.so.4.8.7 +0 -0
  131. sf_veritas.libs/libssl-60250281.so.1.1.1k +0 -0
  132. sf_veritas.libs/libunistring-05abdd40.so.2.1.0 +0 -0
  133. sf_veritas.libs/libuuid-95b83d40.so.1.3.0 +0 -0
@@ -0,0 +1,889 @@
1
+ // sf_veritas/_sffastlog.c
2
+ // NOTE: Previously used g_in_telemetry_send from _sfteepreload.c
3
+ // Now uses X-Sf3-IsTelemetryMessage header for detection (no external symbol needed)
4
+
5
+ #define PY_SSIZE_T_CLEAN
6
+ #include <Python.h>
7
+ #include <pthread.h>
8
+ #include <curl/curl.h>
9
+ #include <stdatomic.h>
10
+ #include <stdint.h>
11
+ #include <stdlib.h>
12
+ #include <string.h>
13
+ #include <time.h>
14
+ #include <sys/time.h>
15
+
16
+ // ---------- Thread-local guard flag ----------
17
+ // CRITICAL: Prevents _sfteepreload.c from capturing our telemetry traffic
18
+ // Set to 1 in sender threads, provides ~5ns overhead vs 50-100ns for header parsing
19
+ __attribute__((visibility("default")))
20
+ __thread int g_in_telemetry_send = 0;
21
+
22
+ // ---------- Ring buffer ----------
23
+ #ifndef SFF_RING_CAP
24
+ #define SFF_RING_CAP 65536 // power-of-two recommended
25
+ #endif
26
+
27
+ typedef struct {
28
+ char *body; // malloc'd HTTP JSON body
29
+ size_t len;
30
+ } sff_msg_t;
31
+
32
+ static sff_msg_t *g_ring = NULL;
33
+ static size_t g_cap = 0;
34
+ static _Atomic size_t g_head = 0; // consumer
35
+ static _Atomic size_t g_tail = 0; // producer
36
+
37
+ // tiny spinlock to make push MPMC-safe enough for Python producers
38
+ static atomic_flag g_push_lock = ATOMIC_FLAG_INIT;
39
+
40
+ // wake/sleep
41
+ static pthread_mutex_t g_cv_mtx = PTHREAD_MUTEX_INITIALIZER;
42
+ static pthread_cond_t g_cv = PTHREAD_COND_INITIALIZER;
43
+ static _Atomic int g_running = 0;
44
+
45
+ // Thread pool for parallel sending (configurable via SF_LOG_SENDER_THREADS)
46
+ #define MAX_SENDER_THREADS 16
47
+ static pthread_t g_sender_threads[MAX_SENDER_THREADS];
48
+ static int g_num_sender_threads = 0;
49
+ static int g_configured_sender_threads = 1; // Default: 1 thread
50
+
51
+ // curl state (per-thread)
52
+ __thread CURL *g_telem_curl = NULL;
53
+ static struct curl_slist *g_hdrs = NULL;
54
+
55
+ // config (owned strings)
56
+ static char *g_url = NULL;
57
+
58
+ static char *g_query_escaped = NULL; // logs mutation (escaped)
59
+ static char *g_api_key = NULL;
60
+ static char *g_service_uuid = NULL;
61
+ static char *g_library = NULL;
62
+ static char *g_version = NULL;
63
+ static int g_http2 = 0;
64
+
65
+ // prebuilt JSON prefix for LOGS:
66
+ // {"query":"<escaped_query>","variables":{"apiKey":"...","serviceUuid":"...","library":"...","version":"..."
67
+ static char *g_json_prefix = NULL;
68
+
69
+ // --- PRINT channel state ---
70
+ static char *g_print_query_escaped = NULL; // prints mutation (escaped)
71
+ static char *g_json_prefix_print = NULL; // same prefix style for print
72
+
73
+ // --- EXCEPTION channel state ---
74
+ static char *g_exception_query_escaped = NULL; // exception mutation (escaped)
75
+ static char *g_json_prefix_exception = NULL; // same prefix style for exception
76
+
77
+ static const char *JSON_SUFFIX = "}}";
78
+
79
+ // ---------- helpers ----------
80
+ static inline uint64_t now_ms(void) {
81
+ #if defined(CLOCK_REALTIME_COARSE)
82
+ struct timespec ts;
83
+ clock_gettime(CLOCK_REALTIME_COARSE, &ts);
84
+ return ((uint64_t)ts.tv_sec) * 1000ULL + (uint64_t)(ts.tv_nsec / 1000000ULL);
85
+ #else
86
+ struct timeval tv;
87
+ gettimeofday(&tv, NULL);
88
+ return ((uint64_t)tv.tv_sec) * 1000ULL + (uint64_t)(tv.tv_usec / 1000ULL);
89
+ #endif
90
+ }
91
+
92
+ static char *str_dup(const char *s) {
93
+ size_t n = strlen(s);
94
+ char *p = (char*)malloc(n + 1);
95
+ if (!p) return NULL;
96
+ memcpy(p, s, n);
97
+ p[n] = 0;
98
+ return p;
99
+ }
100
+
101
+ // escape for generic JSON string fields
102
+ static char *json_escape(const char *s) {
103
+ const unsigned char *in = (const unsigned char*)s;
104
+ size_t extra = 0;
105
+ for (const unsigned char *p = in; *p; ++p) {
106
+ switch (*p) {
107
+ case '\\': case '"': extra++; break;
108
+ default:
109
+ if (*p < 0x20) extra += 5; // \u00XX
110
+ }
111
+ }
112
+ size_t inlen = strlen(s);
113
+ char *out = (char*)malloc(inlen + extra + 1);
114
+ if (!out) return NULL;
115
+
116
+ char *o = out;
117
+ for (const unsigned char *p = in; *p; ++p) {
118
+ switch (*p) {
119
+ case '\\': *o++='\\'; *o++='\\'; break;
120
+ case '"': *o++='\\'; *o++='"'; break;
121
+ default:
122
+ if (*p < 0x20) {
123
+ static const char hex[] = "0123456789abcdef";
124
+ *o++='\\'; *o++='u'; *o++='0'; *o++='0';
125
+ *o++=hex[(*p)>>4]; *o++=hex[(*p)&0xF];
126
+ } else {
127
+ *o++ = (char)*p;
128
+ }
129
+ }
130
+ }
131
+ *o = 0;
132
+ return out;
133
+ }
134
+
135
+ // escape for the GraphQL "query" string (handle \n, \r, \t too)
136
+ static char *json_escape_query(const char *s) {
137
+ const unsigned char *in = (const unsigned char*)s;
138
+ size_t extra = 0;
139
+ for (const unsigned char *p = in; *p; ++p) {
140
+ switch (*p) {
141
+ case '\\': case '"': case '\n': case '\r': case '\t': extra++; break;
142
+ default: break;
143
+ }
144
+ }
145
+ size_t inlen = strlen(s);
146
+ char *out = (char*)malloc(inlen + extra + 1);
147
+ if (!out) return NULL;
148
+ char *o = out;
149
+ for (const unsigned char *p = in; *p; ++p) {
150
+ switch (*p) {
151
+ case '\\': *o++='\\'; *o++='\\'; break;
152
+ case '"': *o++='\\'; *o++='"'; break;
153
+ case '\n': *o++='\\'; *o++='n'; break;
154
+ case '\r': *o++='\\'; *o++='r'; break;
155
+ case '\t': *o++='\\'; *o++='t'; break;
156
+ default: *o++=(char)*p;
157
+ }
158
+ }
159
+ *o=0;
160
+ return out;
161
+ }
162
+
163
+ // generic prefix builder for a given escaped query
164
+ static int build_prefix_for_query(const char *query_escaped, char **out_prefix) {
165
+ const char *p1 = "{\"query\":\"";
166
+ const char *p2 = "\",\"variables\":{";
167
+ const char *k1 = "\"apiKey\":\"";
168
+ const char *k2 = "\",\"serviceUuid\":\"";
169
+ const char *k3 = "\",\"library\":\"";
170
+ const char *k4 = "\",\"version\":\"";
171
+
172
+ size_t n = strlen(p1) + strlen(query_escaped) + strlen(p2)
173
+ + strlen(k1) + strlen(g_api_key)
174
+ + strlen(k2) + strlen(g_service_uuid)
175
+ + strlen(k3) + strlen(g_library)
176
+ + strlen(k4) + strlen(g_version) + 5;
177
+
178
+ char *prefix = (char*)malloc(n);
179
+ if (!prefix) return 0;
180
+
181
+ char *o = prefix;
182
+ o += sprintf(o, "%s%s%s", p1, query_escaped, p2);
183
+ o += sprintf(o, "%s%s", k1, g_api_key);
184
+ o += sprintf(o, "%s%s", k2, g_service_uuid);
185
+ o += sprintf(o, "%s%s", k3, g_library);
186
+ o += sprintf(o, "%s%s\"", k4, g_version);
187
+ *o = '\0';
188
+
189
+ *out_prefix = prefix;
190
+ return 1;
191
+ }
192
+
193
+ // Build LOG body with level
194
+ static int build_body_log(
195
+ const char *session_id,
196
+ const char *level,
197
+ const char *contents,
198
+ int preactive,
199
+ char **out_body,
200
+ size_t *out_len
201
+ ) {
202
+ char *sid_esc = json_escape(session_id ? session_id : "");
203
+ char *lvl_esc = json_escape(level ? level : "UNKNOWN");
204
+ char *msg_esc = json_escape(contents ? contents : "");
205
+ if (!sid_esc || !lvl_esc || !msg_esc) {
206
+ free(sid_esc); free(lvl_esc); free(msg_esc);
207
+ return 0;
208
+ }
209
+
210
+ uint64_t tms = now_ms();
211
+ const char *k_sid = ",\"sessionId\":\"";
212
+ const char *k_lvl = "\",\"level\":\"";
213
+ const char *k_cts = "\",\"contents\":\"";
214
+ const char *k_ts = "\",\"timestampMs\":\"";
215
+ const char *k_pre = ",\"reentrancyGuardPreactive\":";
216
+ char ts_buf[32];
217
+ int ts_len = snprintf(ts_buf, sizeof(ts_buf), "%llu", (unsigned long long)tms);
218
+ const char *pre_str = preactive ? "true" : "false";
219
+
220
+ size_t len = strlen(g_json_prefix)
221
+ + strlen(k_sid) + strlen(sid_esc)
222
+ + strlen(k_lvl) + strlen(lvl_esc)
223
+ + strlen(k_cts) + strlen(msg_esc)
224
+ + strlen(k_ts) + (size_t)ts_len + 1 // +1 for closing quote
225
+ + strlen(k_pre) + strlen(pre_str)
226
+ + strlen(JSON_SUFFIX);
227
+
228
+ char *body = (char*)malloc(len + 1);
229
+ if (!body) { free(sid_esc); free(lvl_esc); free(msg_esc); return 0; }
230
+
231
+ char *o = body;
232
+ o += sprintf(o, "%s", g_json_prefix);
233
+ o += sprintf(o, "%s%s", k_sid, sid_esc);
234
+ o += sprintf(o, "%s%s", k_lvl, lvl_esc);
235
+ o += sprintf(o, "%s%s", k_cts, msg_esc);
236
+ o += sprintf(o, "%s%s\"", k_ts, ts_buf);
237
+ o += sprintf(o, "%s%s", k_pre, pre_str);
238
+ o += sprintf(o, "%s", JSON_SUFFIX);
239
+ *o = '\0';
240
+
241
+ *out_body = body;
242
+ *out_len = (size_t)(o - body);
243
+
244
+ free(sid_esc); free(lvl_esc); free(msg_esc);
245
+ return 1;
246
+ }
247
+
248
+ // Build PRINT body (no level), using g_json_prefix_print
249
+ static int build_body_print(
250
+ const char *session_id, size_t session_len,
251
+ const char *contents, size_t contents_len,
252
+ int preactive,
253
+ char **out_body, size_t *out_len
254
+ ){
255
+ // Escape session_id & contents
256
+ // (We have lengths available but json_escape takes NUL-terminated; copy into temp with NUL)
257
+ char *sid_tmp = (char*)malloc(session_len + 1);
258
+ if (!sid_tmp) return 0;
259
+ memcpy(sid_tmp, session_id ? session_id : "", session_len);
260
+ sid_tmp[session_len] = 0;
261
+
262
+ char *cts_tmp = (char*)malloc(contents_len + 1);
263
+ if (!cts_tmp) { free(sid_tmp); return 0; }
264
+ memcpy(cts_tmp, contents ? contents : "", contents_len);
265
+ cts_tmp[contents_len] = 0;
266
+
267
+ char *sid_esc = json_escape(sid_tmp);
268
+ char *msg_esc = json_escape(cts_tmp);
269
+ free(sid_tmp); free(cts_tmp);
270
+ if (!sid_esc || !msg_esc) {
271
+ free(sid_esc); free(msg_esc);
272
+ return 0;
273
+ }
274
+
275
+ uint64_t tms = now_ms();
276
+ const char *k_sid = ",\"sessionId\":\"";
277
+ const char *k_cts = "\",\"contents\":\"";
278
+ const char *k_ts = "\",\"timestampMs\":\"";
279
+ const char *k_pre = ",\"reentrancyGuardPreactive\":";
280
+ char ts_buf[32];
281
+ int ts_len = snprintf(ts_buf, sizeof(ts_buf), "%llu", (unsigned long long)tms);
282
+ const char *pre_str = preactive ? "true" : "false";
283
+
284
+ if (!g_json_prefix_print) { free(sid_esc); free(msg_esc); return 0; }
285
+
286
+ size_t len = strlen(g_json_prefix_print)
287
+ + strlen(k_sid) + strlen(sid_esc)
288
+ + strlen(k_cts) + strlen(msg_esc)
289
+ + strlen(k_ts) + (size_t)ts_len + 1 // +1 for closing quote
290
+ + strlen(k_pre) + strlen(pre_str)
291
+ + strlen(JSON_SUFFIX);
292
+
293
+ char *body = (char*)malloc(len + 1);
294
+ if (!body) { free(sid_esc); free(msg_esc); return 0; }
295
+
296
+ char *o = body;
297
+ o += sprintf(o, "%s", g_json_prefix_print);
298
+ o += sprintf(o, "%s%s", k_sid, sid_esc);
299
+ o += sprintf(o, "%s%s", k_cts, msg_esc);
300
+ o += sprintf(o, "%s%s\"", k_ts, ts_buf);
301
+ o += sprintf(o, "%s%s", k_pre, pre_str);
302
+ o += sprintf(o, "%s", JSON_SUFFIX);
303
+ *o = '\0';
304
+
305
+ *out_body = body;
306
+ *out_len = (size_t)(o - body);
307
+
308
+ free(sid_esc); free(msg_esc);
309
+ return 1;
310
+ }
311
+
312
+ // Build EXCEPTION body
313
+ static int build_body_exception(
314
+ const char *session_id, size_t session_len,
315
+ const char *exception_message, size_t exception_len,
316
+ const char *trace_json, size_t trace_len,
317
+ int was_caught,
318
+ int is_from_local_service,
319
+ char **out_body, size_t *out_len
320
+ ){
321
+ // Escape session_id, exception_message & trace_json
322
+ char *sid_tmp = (char*)malloc(session_len + 1);
323
+ if (!sid_tmp) return 0;
324
+ memcpy(sid_tmp, session_id ? session_id : "", session_len);
325
+ sid_tmp[session_len] = 0;
326
+
327
+ char *exc_tmp = (char*)malloc(exception_len + 1);
328
+ if (!exc_tmp) { free(sid_tmp); return 0; }
329
+ memcpy(exc_tmp, exception_message ? exception_message : "", exception_len);
330
+ exc_tmp[exception_len] = 0;
331
+
332
+ char *trace_tmp = (char*)malloc(trace_len + 1);
333
+ if (!trace_tmp) { free(sid_tmp); free(exc_tmp); return 0; }
334
+ memcpy(trace_tmp, trace_json ? trace_json : "", trace_len);
335
+ trace_tmp[trace_len] = 0;
336
+
337
+ char *sid_esc = json_escape(sid_tmp);
338
+ char *exc_esc = json_escape(exc_tmp);
339
+ char *trace_esc = json_escape(trace_tmp);
340
+ free(sid_tmp); free(exc_tmp); free(trace_tmp);
341
+
342
+ if (!sid_esc || !exc_esc || !trace_esc) {
343
+ free(sid_esc); free(exc_esc); free(trace_esc);
344
+ return 0;
345
+ }
346
+
347
+ uint64_t tms = now_ms();
348
+ const char *k_sid = ",\"sessionId\":\"";
349
+ const char *k_exc = "\",\"exceptionMessage\":\"";
350
+ const char *k_trace = "\",\"traceJson\":\"";
351
+ const char *k_caught = "\",\"wasCaught\":";
352
+ const char *k_local = ",\"isFromLocalService\":";
353
+ const char *k_ts = ",\"timestampMs\":\"";
354
+ const char *k_pre = ",\"reentrancyGuardPreactive\":";
355
+
356
+ char ts_buf[32];
357
+ int ts_len = snprintf(ts_buf, sizeof(ts_buf), "%llu", (unsigned long long)tms);
358
+ const char *caught_str = was_caught ? "true" : "false";
359
+ const char *local_str = is_from_local_service ? "true" : "false";
360
+
361
+ if (!g_json_prefix_exception) { free(sid_esc); free(exc_esc); free(trace_esc); return 0; }
362
+
363
+ size_t len = strlen(g_json_prefix_exception)
364
+ + strlen(k_sid) + strlen(sid_esc)
365
+ + strlen(k_exc) + strlen(exc_esc)
366
+ + strlen(k_trace) + strlen(trace_esc)
367
+ + strlen(k_caught) + strlen(caught_str)
368
+ + strlen(k_local) + strlen(local_str)
369
+ + strlen(k_ts) + (size_t)ts_len + 1 // +1 for closing quote
370
+ + strlen(k_pre) + 5 // "false"
371
+ + strlen(JSON_SUFFIX);
372
+
373
+ char *body = (char*)malloc(len + 1);
374
+ if (!body) { free(sid_esc); free(exc_esc); free(trace_esc); return 0; }
375
+
376
+ char *o = body;
377
+ o += sprintf(o, "%s", g_json_prefix_exception);
378
+ o += sprintf(o, "%s%s", k_sid, sid_esc);
379
+ o += sprintf(o, "%s%s", k_exc, exc_esc);
380
+ o += sprintf(o, "%s%s", k_trace, trace_esc);
381
+ o += sprintf(o, "%s%s", k_caught, caught_str);
382
+ o += sprintf(o, "%s%s", k_local, local_str);
383
+ o += sprintf(o, "%s%s\"", k_ts, ts_buf);
384
+ o += sprintf(o, "%sfalse", k_pre); // reentrancyGuardPreactive always false for exceptions
385
+ o += sprintf(o, "%s", JSON_SUFFIX);
386
+ *o = '\0';
387
+
388
+ *out_body = body;
389
+ *out_len = (size_t)(o - body);
390
+
391
+ free(sid_esc); free(exc_esc); free(trace_esc);
392
+ return 1;
393
+ }
394
+
395
+ // ---------- ring ops ----------
396
+ static inline size_t ring_count(void) {
397
+ size_t h = atomic_load_explicit(&g_head, memory_order_acquire);
398
+ size_t t = atomic_load_explicit(&g_tail, memory_order_acquire);
399
+ return t - h;
400
+ }
401
+ static inline int ring_empty(void) { return ring_count() == 0; }
402
+
403
+ static int ring_push(char *body, size_t len) {
404
+ while (atomic_flag_test_and_set_explicit(&g_push_lock, memory_order_acquire)) {
405
+ // brief spin
406
+ }
407
+ size_t t = atomic_load_explicit(&g_tail, memory_order_relaxed);
408
+ size_t h = atomic_load_explicit(&g_head, memory_order_acquire);
409
+ if ((t - h) >= g_cap) {
410
+ atomic_flag_clear_explicit(&g_push_lock, memory_order_release);
411
+ return 0; // full (drop)
412
+ }
413
+ size_t idx = t % g_cap;
414
+ g_ring[idx].body = body;
415
+ g_ring[idx].len = len;
416
+ atomic_store_explicit(&g_tail, t + 1, memory_order_release);
417
+ atomic_flag_clear_explicit(&g_push_lock, memory_order_release);
418
+
419
+ pthread_mutex_lock(&g_cv_mtx);
420
+ pthread_cond_signal(&g_cv);
421
+ pthread_mutex_unlock(&g_cv_mtx);
422
+ return 1;
423
+ }
424
+
425
+ static int ring_pop(char **body, size_t *len) {
426
+ size_t h = atomic_load_explicit(&g_head, memory_order_relaxed);
427
+ size_t t = atomic_load_explicit(&g_tail, memory_order_acquire);
428
+ if (h == t) return 0;
429
+ size_t idx = h % g_cap;
430
+ *body = g_ring[idx].body;
431
+ *len = g_ring[idx].len;
432
+ g_ring[idx].body = NULL;
433
+ g_ring[idx].len = 0;
434
+ atomic_store_explicit(&g_head, h + 1, memory_order_release);
435
+ return 1;
436
+ }
437
+
438
+ // ---------- curl sink callbacks ----------
439
+ static size_t _sink_write(char *ptr, size_t size, size_t nmemb, void *userdata) {
440
+ (void)ptr; (void)userdata;
441
+ return size * nmemb;
442
+ }
443
+ static size_t _sink_header(char *ptr, size_t size, size_t nmemb, void *userdata) {
444
+ (void)ptr; (void)userdata;
445
+ return size * nmemb;
446
+ }
447
+
448
+ // ---------- pthread cleanup handler ----------
449
+ static void sender_cleanup(void *arg) {
450
+ (void)arg;
451
+
452
+ // Close thread-local curl handle
453
+ if (g_telem_curl) {
454
+ curl_easy_cleanup(g_telem_curl);
455
+ g_telem_curl = NULL;
456
+ }
457
+ }
458
+
459
+ // ---------- sender thread ----------
460
+ static void *sender_main(void *arg) {
461
+ (void)arg;
462
+
463
+ // CRITICAL: Set telemetry guard for this thread (prevents _sfteepreload.c capture)
464
+ g_in_telemetry_send = 1;
465
+
466
+ // Initialize thread-local curl handle
467
+ g_telem_curl = curl_easy_init();
468
+ if (!g_telem_curl) {
469
+ return NULL;
470
+ }
471
+
472
+ // Configure curl handle (copy from global settings)
473
+ curl_easy_setopt(g_telem_curl, CURLOPT_URL, g_url);
474
+ curl_easy_setopt(g_telem_curl, CURLOPT_TCP_KEEPALIVE, 1L);
475
+ curl_easy_setopt(g_telem_curl, CURLOPT_TCP_NODELAY, 1L); // NEW: Eliminate Nagle delay
476
+ curl_easy_setopt(g_telem_curl, CURLOPT_HTTPHEADER, g_hdrs);
477
+ #ifdef CURL_HTTP_VERSION_2TLS
478
+ if (g_http2) {
479
+ curl_easy_setopt(g_telem_curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS);
480
+ }
481
+ #endif
482
+ curl_easy_setopt(g_telem_curl, CURLOPT_WRITEFUNCTION, _sink_write);
483
+ curl_easy_setopt(g_telem_curl, CURLOPT_HEADERFUNCTION, _sink_header);
484
+
485
+ // Register cleanup handler (executes on thread exit)
486
+ pthread_cleanup_push(sender_cleanup, NULL);
487
+
488
+ while (1) {
489
+ // Keep running until shutdown requested AND queue is fully drained
490
+ if (ring_empty()) {
491
+ // Queue is empty - check if we should exit
492
+ if (!atomic_load(&g_running)) {
493
+ // Shutting down and queue is empty - safe to exit
494
+ break;
495
+ }
496
+ // Still running - wait for new items
497
+ pthread_mutex_lock(&g_cv_mtx);
498
+ if (ring_empty() && atomic_load(&g_running))
499
+ pthread_cond_wait(&g_cv, &g_cv_mtx);
500
+ pthread_mutex_unlock(&g_cv_mtx);
501
+ continue;
502
+ }
503
+
504
+ // Drain ALL items from queue (don't check g_running mid-drain)
505
+ char *body = NULL; size_t len = 0;
506
+ while (ring_pop(&body, &len)) {
507
+ if (!body) continue;
508
+
509
+ // Use thread-local curl handle (each thread has its own persistent connection)
510
+ curl_easy_setopt(g_telem_curl, CURLOPT_POSTFIELDS, body);
511
+ curl_easy_setopt(g_telem_curl, CURLOPT_POSTFIELDSIZE, (long)len);
512
+ (void)curl_easy_perform(g_telem_curl); // fire-and-forget
513
+
514
+ free(body);
515
+ }
516
+ }
517
+
518
+ pthread_cleanup_pop(1); // Execute cleanup handler
519
+ return NULL;
520
+ }
521
+
522
+ // ---------- Python API ----------
523
+ static PyObject *py_init(PyObject *self, PyObject *args, PyObject *kw) {
524
+ const char *url, *query, *api_key, *service_uuid, *library, *version;
525
+ int http2 = 0;
526
+ static char *kwlist[] = {"url","query","api_key","service_uuid","library","version","http2", NULL};
527
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "ssssssi",
528
+ kwlist, &url, &query, &api_key, &service_uuid, &library, &version, &http2)) {
529
+ return NULL; // Exception already set by PyArg_ParseTupleAndKeywords
530
+ }
531
+ if (g_running) Py_RETURN_TRUE;
532
+
533
+ g_url = str_dup(url);
534
+ g_query_escaped = json_escape_query(query);
535
+ g_api_key = str_dup(api_key);
536
+ g_service_uuid = str_dup(service_uuid);
537
+ g_library = str_dup(library);
538
+ g_version = str_dup(version);
539
+ g_http2 = http2 ? 1 : 0;
540
+ if (!g_url || !g_query_escaped || !g_api_key || !g_service_uuid || !g_library || !g_version) {
541
+ Py_RETURN_FALSE;
542
+ }
543
+ if (!build_prefix_for_query(g_query_escaped, &g_json_prefix)) { Py_RETURN_FALSE; }
544
+
545
+ g_cap = SFF_RING_CAP;
546
+ g_ring = (sff_msg_t*)calloc(g_cap, sizeof(sff_msg_t));
547
+ if (!g_ring) { Py_RETURN_FALSE; }
548
+
549
+ // Parse SF_LOG_SENDER_THREADS environment variable
550
+ const char *env_threads = getenv("SF_LOG_SENDER_THREADS");
551
+ if (env_threads) {
552
+ int t = atoi(env_threads);
553
+ if (t > 0 && t <= MAX_SENDER_THREADS) {
554
+ g_configured_sender_threads = t;
555
+ }
556
+ }
557
+
558
+ curl_global_init(CURL_GLOBAL_DEFAULT);
559
+
560
+ // Initialize shared curl headers (Content-Type only)
561
+ // NOTE: Removed X-Sf3-IsTelemetryMessage header - now use g_in_telemetry_send flag
562
+ g_hdrs = NULL;
563
+ g_hdrs = curl_slist_append(g_hdrs, "Content-Type: application/json");
564
+
565
+ // Start sender thread pool
566
+ atomic_store(&g_running, 1);
567
+ g_num_sender_threads = g_configured_sender_threads;
568
+ for (int i = 0; i < g_num_sender_threads; i++) {
569
+ if (pthread_create(&g_sender_threads[i], NULL, sender_main, NULL) != 0) {
570
+ atomic_store(&g_running, 0);
571
+ // Clean up already-started threads
572
+ for (int j = 0; j < i; j++) {
573
+ pthread_join(g_sender_threads[j], NULL);
574
+ }
575
+ Py_RETURN_FALSE;
576
+ }
577
+ }
578
+
579
+ Py_RETURN_TRUE;
580
+ }
581
+
582
+ static PyObject *py_init_print(PyObject *self, PyObject *args, PyObject *kw) {
583
+ const char *url, *query, *api_key, *service_uuid, *library, *version;
584
+ int http2 = 1;
585
+ static char *kwlist[] = {"url","query","api_key","service_uuid","library","version","http2",NULL};
586
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "ssssssi", kwlist,
587
+ &url, &query, &api_key, &service_uuid, &library, &version, &http2)) {
588
+ return NULL; // Exception already set by PyArg_ParseTupleAndKeywords
589
+ }
590
+
591
+ // If not initialized yet, call init() first with the log query (empty for now)
592
+ if (!g_running) {
593
+ // Build a dummy log query just to initialize the transport
594
+ const char *dummy_log_query =
595
+ "mutation CollectLogs("
596
+ "$apiKey: String!, "
597
+ "$serviceUuid: String!, "
598
+ "$sessionId: String!, "
599
+ "$level: String!, "
600
+ "$contents: String!, "
601
+ "$reentrancyGuardPreactive: Boolean!, "
602
+ "$library: String!, "
603
+ "$timestampMs: String!, "
604
+ "$version: String!"
605
+ ") { "
606
+ "collectLogs("
607
+ "apiKey: $apiKey, "
608
+ "serviceUuid: $serviceUuid, "
609
+ "sessionId: $sessionId, "
610
+ "level: $level, "
611
+ "contents: $contents, "
612
+ "reentrancyGuardPreactive: $reentrancyGuardPreactive, "
613
+ "library: $library, "
614
+ "timestampMs: $timestampMs, "
615
+ "version: $version"
616
+ ") }";
617
+
618
+ // Call py_init to set up the transport
619
+ PyObject *init_args = Py_BuildValue("(ssssssi)", url, dummy_log_query, api_key, service_uuid, library, version, http2);
620
+ if (!init_args) return NULL;
621
+
622
+ PyObject *init_result = py_init(self, init_args, NULL);
623
+ Py_DECREF(init_args);
624
+
625
+ if (!init_result || init_result == Py_False) {
626
+ Py_XDECREF(init_result);
627
+ Py_RETURN_FALSE;
628
+ }
629
+ Py_DECREF(init_result);
630
+ }
631
+
632
+ // Now set up the print query + prefix
633
+ if (g_print_query_escaped) { free(g_print_query_escaped); g_print_query_escaped = NULL; }
634
+ if (g_json_prefix_print) { free(g_json_prefix_print); g_json_prefix_print = NULL; }
635
+
636
+ g_print_query_escaped = json_escape_query(query);
637
+ if (!g_print_query_escaped) Py_RETURN_FALSE;
638
+ if (!build_prefix_for_query(g_print_query_escaped, &g_json_prefix_print)) {
639
+ Py_RETURN_FALSE;
640
+ }
641
+ Py_RETURN_TRUE;
642
+ }
643
+
644
+ static PyObject *py_init_exception(PyObject *self, PyObject *args, PyObject *kw) {
645
+ const char *url, *query, *api_key, *service_uuid, *library, *version;
646
+ int http2 = 1;
647
+ static char *kwlist[] = {"url","query","api_key","service_uuid","library","version","http2",NULL};
648
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "ssssssi", kwlist,
649
+ &url, &query, &api_key, &service_uuid, &library, &version, &http2)) {
650
+ return NULL; // Exception already set by PyArg_ParseTupleAndKeywords
651
+ }
652
+
653
+ // If not initialized yet, call init() first with the log query
654
+ if (!g_running) {
655
+ // Build a dummy log query just to initialize the transport
656
+ const char *dummy_log_query =
657
+ "mutation CollectLogs("
658
+ "$apiKey: String!, "
659
+ "$serviceUuid: String!, "
660
+ "$sessionId: String!, "
661
+ "$level: String!, "
662
+ "$contents: String!, "
663
+ "$reentrancyGuardPreactive: Boolean!, "
664
+ "$library: String!, "
665
+ "$timestampMs: String!, "
666
+ "$version: String!"
667
+ ") { "
668
+ "collectLogs("
669
+ "apiKey: $apiKey, "
670
+ "serviceUuid: $serviceUuid, "
671
+ "sessionId: $sessionId, "
672
+ "level: $level, "
673
+ "contents: $contents, "
674
+ "reentrancyGuardPreactive: $reentrancyGuardPreactive, "
675
+ "library: $library, "
676
+ "timestampMs: $timestampMs, "
677
+ "version: $version"
678
+ ") }";
679
+
680
+ // Call py_init to set up the transport
681
+ PyObject *init_args = Py_BuildValue("(ssssssi)", url, dummy_log_query, api_key, service_uuid, library, version, http2);
682
+ if (!init_args) return NULL;
683
+
684
+ PyObject *init_result = py_init(self, init_args, NULL);
685
+ Py_DECREF(init_args);
686
+
687
+ if (!init_result || init_result == Py_False) {
688
+ Py_XDECREF(init_result);
689
+ Py_RETURN_FALSE;
690
+ }
691
+ Py_DECREF(init_result);
692
+ }
693
+
694
+ // Now set up the exception query + prefix
695
+ if (g_exception_query_escaped) { free(g_exception_query_escaped); g_exception_query_escaped = NULL; }
696
+ if (g_json_prefix_exception) { free(g_json_prefix_exception); g_json_prefix_exception = NULL; }
697
+
698
+ g_exception_query_escaped = json_escape_query(query);
699
+ if (!g_exception_query_escaped) Py_RETURN_FALSE;
700
+ if (!build_prefix_for_query(g_exception_query_escaped, &g_json_prefix_exception)) {
701
+ Py_RETURN_FALSE;
702
+ }
703
+ Py_RETURN_TRUE;
704
+ }
705
+
706
+ static PyObject *py_log(PyObject *self, PyObject *args, PyObject *kw) {
707
+ const char *level, *contents, *session_id;
708
+ int preactive = 0;
709
+ static char *kwlist[] = {"level","contents","session_id","preactive", NULL};
710
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "sss|p", kwlist,
711
+ &level, &contents, &session_id, &preactive)) {
712
+ Py_RETURN_NONE;
713
+ }
714
+ if (!g_running) Py_RETURN_NONE;
715
+
716
+ // OPTIMIZATION: Release GIL during JSON building + ring push
717
+ char *body = NULL;
718
+ size_t len = 0;
719
+ int ok = 0;
720
+
721
+ Py_BEGIN_ALLOW_THREADS
722
+ // Build JSON body (WITHOUT GIL - pure C string operations)
723
+ if (build_body_log(session_id, level, contents, preactive, &body, &len)) {
724
+ // Push to ring buffer (WITHOUT GIL)
725
+ ok = ring_push(body, len);
726
+ }
727
+ Py_END_ALLOW_THREADS
728
+
729
+ if (!ok) { free(body); }
730
+ Py_RETURN_NONE;
731
+ }
732
+
733
+ static PyObject *py_print(PyObject *self, PyObject *args, PyObject *kw) {
734
+ const char *contents, *session_id;
735
+ Py_ssize_t contents_len = 0, session_len = 0;
736
+ int preactive = 0;
737
+ static char *kwlist[] = {"contents","session_id","preactive", NULL};
738
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "s#s#|p", kwlist,
739
+ &contents, &contents_len,
740
+ &session_id, &session_len,
741
+ &preactive)) {
742
+ Py_RETURN_NONE;
743
+ }
744
+ if (!g_running || g_json_prefix_print == NULL) Py_RETURN_NONE;
745
+
746
+ // OPTIMIZATION: Release GIL during JSON building + ring push
747
+ char *body = NULL;
748
+ size_t len = 0;
749
+ int ok = 0;
750
+
751
+ Py_BEGIN_ALLOW_THREADS
752
+ // Build JSON body (WITHOUT GIL - pure C string operations)
753
+ if (build_body_print(session_id, (size_t)session_len,
754
+ contents, (size_t)contents_len,
755
+ preactive, &body, &len)) {
756
+ // Push to ring buffer (WITHOUT GIL)
757
+ ok = ring_push(body, len);
758
+ }
759
+ Py_END_ALLOW_THREADS
760
+
761
+ if (!ok) { free(body); } // ring owns on success
762
+ Py_RETURN_NONE;
763
+ }
764
+
765
+ static PyObject *py_exception(PyObject *self, PyObject *args, PyObject *kw) {
766
+ const char *exception_message, *trace_json, *session_id;
767
+ Py_ssize_t exception_len = 0, trace_len = 0, session_len = 0;
768
+ int was_caught = 1;
769
+ int is_from_local_service = 0;
770
+ static char *kwlist[] = {"exception_message","trace_json","session_id","was_caught","is_from_local_service", NULL};
771
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "s#s#s#|pp", kwlist,
772
+ &exception_message, &exception_len,
773
+ &trace_json, &trace_len,
774
+ &session_id, &session_len,
775
+ &was_caught,
776
+ &is_from_local_service)) {
777
+ Py_RETURN_NONE;
778
+ }
779
+ if (!g_running || g_json_prefix_exception == NULL) Py_RETURN_NONE;
780
+
781
+ // OPTIMIZATION: Release GIL during JSON building + ring push
782
+ char *body = NULL;
783
+ size_t len = 0;
784
+ int ok = 0;
785
+
786
+ Py_BEGIN_ALLOW_THREADS
787
+ // Build JSON body (WITHOUT GIL - pure C string operations)
788
+ if (build_body_exception(session_id, (size_t)session_len,
789
+ exception_message, (size_t)exception_len,
790
+ trace_json, (size_t)trace_len,
791
+ was_caught, is_from_local_service,
792
+ &body, &len)) {
793
+ // Push to ring buffer (WITHOUT GIL)
794
+ ok = ring_push(body, len);
795
+ }
796
+ Py_END_ALLOW_THREADS
797
+
798
+ if (!ok) { free(body); } // ring owns on success
799
+ Py_RETURN_NONE;
800
+ }
801
+
802
+ static PyObject *py_shutdown(PyObject *self, PyObject *args) {
803
+ if (!g_running) Py_RETURN_NONE;
804
+
805
+ atomic_store(&g_running, 0);
806
+
807
+ // Wake all sender threads (use broadcast for multiple threads)
808
+ pthread_mutex_lock(&g_cv_mtx);
809
+ pthread_cond_broadcast(&g_cv);
810
+ pthread_mutex_unlock(&g_cv_mtx);
811
+
812
+ // Join all sender threads (cleanup handlers execute automatically)
813
+ for (int i = 0; i < g_num_sender_threads; i++) {
814
+ if (g_sender_threads[i]) {
815
+ pthread_join(g_sender_threads[i], NULL);
816
+ }
817
+ }
818
+
819
+ // Clean up shared curl resources
820
+ // NOTE: g_curl is now per-thread, cleaned by pthread_cleanup_push
821
+ if (g_hdrs) {
822
+ curl_slist_free_all(g_hdrs);
823
+ g_hdrs = NULL;
824
+ }
825
+ curl_global_cleanup();
826
+
827
+ // Free all allocated strings (NULL after free to prevent use-after-free)
828
+ free(g_url);
829
+ g_url = NULL;
830
+
831
+ free(g_query_escaped);
832
+ g_query_escaped = NULL;
833
+ free(g_json_prefix);
834
+ g_json_prefix = NULL;
835
+
836
+ free(g_print_query_escaped);
837
+ g_print_query_escaped = NULL;
838
+ free(g_json_prefix_print);
839
+ g_json_prefix_print = NULL;
840
+
841
+ free(g_exception_query_escaped);
842
+ g_exception_query_escaped = NULL;
843
+ free(g_json_prefix_exception);
844
+ g_json_prefix_exception = NULL;
845
+
846
+ free(g_api_key);
847
+ g_api_key = NULL;
848
+ free(g_service_uuid);
849
+ g_service_uuid = NULL;
850
+ free(g_library);
851
+ g_library = NULL;
852
+ free(g_version);
853
+ g_version = NULL;
854
+
855
+ // Free ring buffer (drain remaining messages first)
856
+ if (g_ring) {
857
+ char *b;
858
+ size_t l;
859
+ while (ring_pop(&b, &l)) free(b);
860
+ free(g_ring);
861
+ g_ring = NULL;
862
+ }
863
+
864
+ Py_RETURN_NONE;
865
+ }
866
+
867
+ // ---------- Module table (SINGLE definition) ----------
868
+ static PyMethodDef SFFastLogMethods[] = {
869
+ {"init", (PyCFunction)py_init, METH_VARARGS | METH_KEYWORDS, "Init (logs) and start sender"},
870
+ {"init_print", (PyCFunction)py_init_print, METH_VARARGS | METH_KEYWORDS, "Init (prints) query/prefix"},
871
+ {"init_exception", (PyCFunction)py_init_exception, METH_VARARGS | METH_KEYWORDS, "Init (exception) query/prefix"},
872
+ {"log", (PyCFunction)py_log, METH_VARARGS | METH_KEYWORDS, "Send log"},
873
+ {"print_", (PyCFunction)py_print, METH_VARARGS | METH_KEYWORDS, "Send print"},
874
+ {"exception", (PyCFunction)py_exception, METH_VARARGS | METH_KEYWORDS, "Send exception"},
875
+ {"shutdown", (PyCFunction)py_shutdown, METH_NOARGS, "Shutdown sender and free state"},
876
+ {NULL, NULL, 0, NULL}
877
+ };
878
+
879
+ static struct PyModuleDef sffastlogmodule = {
880
+ PyModuleDef_HEAD_INIT,
881
+ "_sffastlog",
882
+ "sf_veritas ultra-fast logging/printing",
883
+ -1,
884
+ SFFastLogMethods
885
+ };
886
+
887
+ PyMODINIT_FUNC PyInit__sffastlog(void) {
888
+ return PyModule_Create(&sffastlogmodule);
889
+ }