sf-veritas 0.11.10__cp314-cp314-manylinux_2_28_x86_64.whl

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