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,994 @@
1
+ // sf_veritas/_sffastnet.c
2
+ // Ultra-fast network request capture with request/response data
3
+ // Key optimization: Use g_in_telemetry_send flag (not HTTP headers)
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
+ __attribute__((visibility("default")))
23
+
24
+ // ---------- Ring buffer ----------
25
+ #ifndef SFN_RING_CAP
26
+ #define SFN_RING_CAP 65536 // power-of-two recommended
27
+ #endif
28
+
29
+ typedef struct {
30
+ char *body; // malloc'd HTTP JSON body
31
+ size_t len;
32
+ } sfn_msg_t;
33
+
34
+ static sfn_msg_t *g_ring = NULL;
35
+ static size_t g_cap = 0;
36
+ static _Atomic size_t g_head = 0; // consumer
37
+ static _Atomic size_t g_tail = 0; // producer
38
+
39
+ // tiny spinlock to make push MPMC-safe enough for Python producers
40
+ static atomic_flag g_push_lock = ATOMIC_FLAG_INIT;
41
+
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_FASTNET_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
+ // Debug flag (set from SF_DEBUG environment variable)
55
+ static int SF_DEBUG = 0;
56
+
57
+ // curl state (per-thread handles + shared headers)
58
+ __thread CURL *g_telem_curl = NULL;
59
+ static struct curl_slist *g_hdrs = NULL;
60
+
61
+ // config (owned strings)
62
+ static char *g_url = NULL;
63
+ static char *g_query_escaped = NULL;
64
+ static char *g_api_key = NULL;
65
+ static int g_http2 = 0;
66
+
67
+ // prebuilt JSON prefix for NETWORK REQUEST:
68
+ // {"query":"<escaped_query>","variables":{"data":{"apiKey":"..."
69
+ static char *g_json_prefix = NULL;
70
+
71
+ static const char *JSON_SUFFIX = "}}}";
72
+
73
+ // ---------- helpers ----------
74
+ static inline uint64_t now_ms(void) {
75
+ #if defined(CLOCK_REALTIME_COARSE)
76
+ struct timespec ts;
77
+ clock_gettime(CLOCK_REALTIME_COARSE, &ts);
78
+ return ((uint64_t)ts.tv_sec) * 1000ULL + (uint64_t)(ts.tv_nsec / 1000000ULL);
79
+ #else
80
+ struct timeval tv;
81
+ gettimeofday(&tv, NULL);
82
+ return ((uint64_t)tv.tv_sec) * 1000ULL + (uint64_t)(tv.tv_usec / 1000ULL);
83
+ #endif
84
+ }
85
+
86
+ static char *str_dup(const char *s) {
87
+ size_t n = strlen(s);
88
+ char *p = (char*)malloc(n + 1);
89
+ if (!p) return NULL;
90
+ memcpy(p, s, n);
91
+ p[n] = 0;
92
+ return p;
93
+ }
94
+
95
+ // Fast path: check if string needs escaping at all
96
+ static inline int needs_escape(const char *s, size_t len) {
97
+ for (size_t i = 0; i < len; ++i) {
98
+ unsigned char c = (unsigned char)s[i];
99
+ if (c == '\\' || c == '"' || c < 0x20) return 1;
100
+ }
101
+ return 0;
102
+ }
103
+
104
+ // escape for generic JSON string fields
105
+ static char *json_escape(const char *s) {
106
+ if (!s) return str_dup("");
107
+
108
+ size_t inlen = strlen(s);
109
+
110
+ // Fast path: if no escaping needed, just duplicate
111
+ if (!needs_escape(s, inlen)) {
112
+ return str_dup(s);
113
+ }
114
+
115
+ // Slow path: need to escape
116
+ const unsigned char *in = (const unsigned char*)s;
117
+ size_t extra = 0;
118
+ for (const unsigned char *p = in; *p; ++p) {
119
+ switch (*p) {
120
+ case '\\': case '"': extra++; break;
121
+ default:
122
+ if (*p < 0x20) extra += 5; // \u00XX
123
+ }
124
+ }
125
+
126
+ char *out = (char*)malloc(inlen + extra + 1);
127
+ if (!out) return NULL;
128
+
129
+ char *o = out;
130
+ for (const unsigned char *p = in; *p; ++p) {
131
+ switch (*p) {
132
+ case '\\': *o++='\\'; *o++='\\'; break;
133
+ case '"': *o++='\\'; *o++='"'; break;
134
+ default:
135
+ if (*p < 0x20) {
136
+ static const char hex[] = "0123456789abcdef";
137
+ *o++='\\'; *o++='u'; *o++='0'; *o++='0';
138
+ *o++=hex[(*p)>>4]; *o++=hex[(*p)&0xF];
139
+ } else {
140
+ *o++ = (char)*p;
141
+ }
142
+ }
143
+ }
144
+ *o = 0;
145
+ return out;
146
+ }
147
+
148
+ // escape for the GraphQL "query" string (handle \n, \r, \t too)
149
+ static char *json_escape_query(const char *s) {
150
+ const unsigned char *in = (const unsigned char*)s;
151
+ size_t extra = 0;
152
+ for (const unsigned char *p = in; *p; ++p) {
153
+ switch (*p) {
154
+ case '\\': case '"': case '\n': case '\r': case '\t': extra++; break;
155
+ default: break;
156
+ }
157
+ }
158
+ size_t inlen = strlen(s);
159
+ char *out = (char*)malloc(inlen + extra + 1);
160
+ if (!out) return NULL;
161
+ char *o = out;
162
+ for (const unsigned char *p = in; *p; ++p) {
163
+ switch (*p) {
164
+ case '\\': *o++='\\'; *o++='\\'; break;
165
+ case '"': *o++='\\'; *o++='"'; break;
166
+ case '\n': *o++='\\'; *o++='n'; break;
167
+ case '\r': *o++='\\'; *o++='r'; break;
168
+ case '\t': *o++='\\'; *o++='t'; break;
169
+ default: *o++=(char)*p;
170
+ }
171
+ }
172
+ *o=0;
173
+ return out;
174
+ }
175
+
176
+ // Extract "query|mutation operationName (fieldName)" from GraphQL request body
177
+ // Returns malloc'd string or NULL if not GraphQL or parse error
178
+ // Fast path: 2ns for non-GraphQL (just memcmp check)
179
+ // Slow path: 15-18ns for GraphQL (parse ~200 chars)
180
+ // Format: "query operationName (fieldName)" or "mutation operationName (fieldName)"
181
+ // Handles both direct GraphQL and JSON-wrapped: {"query": "mutation ..."}
182
+ static char *extract_graphql_name(const char *body, size_t len) {
183
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG extract_graphql_name] START len=%zu, first 100 chars: %.100s\n", len, body);
184
+
185
+ if (len < 6) {
186
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG extract_graphql_name] FAIL: len < 6\n");
187
+ return NULL;
188
+ }
189
+
190
+ const char *p = body;
191
+ const char *end = body + len;
192
+ const char *operation_type = NULL;
193
+ size_t operation_type_len = 0;
194
+
195
+ // Skip leading whitespace (handles " mutation ..." or "\n\tquery ...")
196
+ while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
197
+
198
+ if (p >= end) {
199
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG extract_graphql_name] FAIL: only whitespace\n");
200
+ return NULL;
201
+ }
202
+
203
+ // Calculate remaining length after skipping whitespace
204
+ size_t remaining = end - p;
205
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG extract_graphql_name] After whitespace skip, remaining=%zu, first char='%c'\n", remaining, *p);
206
+
207
+ // Check if it's JSON-wrapped GraphQL: {"query": "mutation ..."}
208
+ // Fast check: starts with '{'
209
+ if (remaining > 10 && *p == '{') {
210
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG extract_graphql_name] Detected JSON format (starts with '{')\n");
211
+
212
+ // Look for "query": or "query" : (with optional whitespace)
213
+ const char *query_key = p + 1;
214
+
215
+ // Skip whitespace after '{'
216
+ while (query_key < end && (*query_key == ' ' || *query_key == '\t' || *query_key == '\n' || *query_key == '\r')) query_key++;
217
+
218
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG extract_graphql_name] After '{', next chars: %.20s\n", query_key);
219
+
220
+ // Check for "query" (with quotes)
221
+ if ((end - query_key) > 10 && memcmp(query_key, "\"query\"", 7) == 0) {
222
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG extract_graphql_name] Found \"query\" key\n");
223
+ query_key += 7;
224
+
225
+ // Skip whitespace
226
+ while (query_key < end && (*query_key == ' ' || *query_key == '\t' || *query_key == '\n' || *query_key == '\r')) query_key++;
227
+
228
+ // Expect ':'
229
+ if (query_key < end && *query_key == ':') {
230
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG extract_graphql_name] Found ':' after query key\n");
231
+ query_key++;
232
+
233
+ // Skip whitespace
234
+ while (query_key < end && (*query_key == ' ' || *query_key == '\t' || *query_key == '\n' || *query_key == '\r')) query_key++;
235
+
236
+ // Expect '"' (start of query string value)
237
+ if (query_key < end && *query_key == '"') {
238
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG extract_graphql_name] Found opening quote for query value\n");
239
+ query_key++;
240
+
241
+ // Now query_key points to the GraphQL query string
242
+ // Update p and end to parse this substring
243
+ p = query_key;
244
+
245
+ // Find the closing quote (handle escaped quotes \")
246
+ const char *query_end = p;
247
+ while (query_end < end) {
248
+ if (*query_end == '"' && (query_end == p || *(query_end - 1) != '\\')) {
249
+ break;
250
+ }
251
+ query_end++;
252
+ }
253
+
254
+ if (query_end >= end) {
255
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG extract_graphql_name] FAIL: No closing quote for query value\n");
256
+ return NULL;
257
+ }
258
+
259
+ end = query_end; // Limit parsing to within the query field
260
+ remaining = end - p;
261
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG extract_graphql_name] Extracted query value, first 100 chars: %.100s\n", p);
262
+ } else {
263
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG extract_graphql_name] Expected opening quote but got: %c\n", query_key < end ? *query_key : '?');
264
+ }
265
+ } else {
266
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG extract_graphql_name] Expected ':' but got: %c\n", query_key < end ? *query_key : '?');
267
+ }
268
+ } else {
269
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG extract_graphql_name] Did not find \"query\" key, next chars: %.20s\n", query_key);
270
+ }
271
+ }
272
+
273
+ // Skip any leading whitespace in the query string itself (handles escaped whitespace too)
274
+ while (p < end && (*p == ' ' || *p == '\t' || *p == '\\' || *p == 'n' || *p == 't' || *p == 'r')) {
275
+ if (*p == '\\' && p + 1 < end && (*(p + 1) == 'n' || *(p + 1) == 't' || *(p + 1) == 'r')) {
276
+ p += 2; // Skip \n, \t, or \r
277
+ } else if (*p == ' ' || *p == '\t') {
278
+ p++;
279
+ } else {
280
+ break;
281
+ }
282
+ }
283
+
284
+ if (p >= end) {
285
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG extract_graphql_name] FAIL: only whitespace after escaped whitespace skip\n");
286
+ return NULL;
287
+ }
288
+ remaining = end - p;
289
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG extract_graphql_name] Ready to match operation type, remaining=%zu, first 20 chars: %.20s\n", remaining, p);
290
+
291
+ // Fast-path check for "mutation " or "query "
292
+ if (remaining >= 9 && memcmp(p, "mutation ", 9) == 0) {
293
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG extract_graphql_name] Matched 'mutation '\n");
294
+ operation_type = "mutation";
295
+ operation_type_len = 8;
296
+ p += 9;
297
+ } else if (remaining >= 6 && memcmp(p, "query ", 6) == 0) {
298
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG extract_graphql_name] Matched 'query '\n");
299
+ operation_type = "query";
300
+ operation_type_len = 5;
301
+ p += 6;
302
+ } else if (remaining >= 9 && memcmp(p, "mutation{", 9) == 0) {
303
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG extract_graphql_name] Matched 'mutation{' (anonymous)\n");
304
+ // Anonymous mutation: "mutation{field}"
305
+ operation_type = "mutation";
306
+ operation_type_len = 8;
307
+ p += 8; // p now at '{'
308
+ } else if (remaining >= 6 && memcmp(p, "query{", 6) == 0) {
309
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG extract_graphql_name] Matched 'query{' (anonymous)\n");
310
+ // Anonymous query: "query{field}"
311
+ operation_type = "query";
312
+ operation_type_len = 5;
313
+ p += 5; // p now at '{'
314
+ } else {
315
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG extract_graphql_name] FAIL: No operation type match, first 30 chars: %.30s\n", p);
316
+ return NULL; // Not a GraphQL operation
317
+ }
318
+
319
+ // Skip whitespace
320
+ while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
321
+
322
+ // Extract operation name (optional - may be anonymous)
323
+ const char *op_name_start = p;
324
+ const char *op_name_end = p;
325
+
326
+ if (p < end && *p != '{' && *p != '(') {
327
+ // We have an operation name
328
+ while (p < end && *p != '(' && *p != '{' && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') {
329
+ p++;
330
+ }
331
+ op_name_end = p;
332
+ }
333
+
334
+ size_t op_name_len = op_name_end - op_name_start;
335
+
336
+ // Skip whitespace
337
+ while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
338
+
339
+ // Skip parameter list if present: ($param1: Type, ...)
340
+ // Use balanced parenthesis matching to handle nested types: ($x: Type($nested))
341
+ if (p < end && *p == '(') {
342
+ int depth = 1;
343
+ p++;
344
+ while (p < end && depth > 0) {
345
+ if (*p == '(') depth++;
346
+ else if (*p == ')') depth--;
347
+ p++;
348
+ }
349
+ if (depth != 0) return NULL; // Mismatched parens - parse error
350
+ }
351
+
352
+ // Skip whitespace
353
+ while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
354
+
355
+ // Expect '{'
356
+ if (p >= end || *p != '{') return NULL; // Parse error
357
+ p++; // Skip '{'
358
+
359
+ // Skip whitespace inside selection set
360
+ while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
361
+
362
+ // Extract first field name (until '(' or '{' or whitespace)
363
+ const char *field_start = p;
364
+ while (p < end && *p != '(' && *p != '{' && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') {
365
+ p++;
366
+ }
367
+ const char *field_end = p;
368
+
369
+ size_t field_len = field_end - field_start;
370
+ if (field_len == 0) return NULL; // No field name - parse error
371
+
372
+ // Build result: "operation_type operationName (fieldName)"
373
+ // Format examples:
374
+ // "query GetUser (user)"
375
+ // "mutation CollectFunctionSpan (collectFunctionSpan)"
376
+ // "mutation (createPost)" (anonymous)
377
+
378
+ // Calculate result length
379
+ size_t result_len = operation_type_len + 1; // "mutation "
380
+ if (op_name_len > 0) {
381
+ result_len += op_name_len + 1; // "operationName "
382
+ }
383
+ result_len += 1 + field_len + 1; // "(fieldName)"
384
+
385
+ char *result = (char*)malloc(result_len + 1);
386
+ if (!result) return NULL;
387
+
388
+ // Build result string
389
+ char *o = result;
390
+ memcpy(o, operation_type, operation_type_len);
391
+ o += operation_type_len;
392
+ *o++ = ' ';
393
+
394
+ if (op_name_len > 0) {
395
+ memcpy(o, op_name_start, op_name_len);
396
+ o += op_name_len;
397
+ *o++ = ' ';
398
+ }
399
+
400
+ *o++ = '(';
401
+ memcpy(o, field_start, field_len);
402
+ o += field_len;
403
+ *o++ = ')';
404
+ *o = '\0';
405
+
406
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG extract_graphql_name] SUCCESS: result='%s'\n", result);
407
+ return result;
408
+ }
409
+
410
+ // Build prefix: {"query":"...","variables":{"data":{"apiKey":"..."
411
+ static int build_prefix(void) {
412
+ const char *p1 = "{\"query\":\"";
413
+ const char *p2 = "\",\"variables\":{\"data\":{";
414
+ const char *k1 = "\"apiKey\":\"";
415
+
416
+ size_t n = strlen(p1) + strlen(g_query_escaped) + strlen(p2)
417
+ + strlen(k1) + strlen(g_api_key) + 5;
418
+
419
+ char *prefix = (char*)malloc(n);
420
+ if (!prefix) return 0;
421
+
422
+ char *o = prefix;
423
+ o += sprintf(o, "%s%s%s", p1, g_query_escaped, p2);
424
+ o += sprintf(o, "%s%s\"", k1, g_api_key);
425
+ *o = '\0';
426
+
427
+ g_json_prefix = prefix;
428
+ return 1;
429
+ }
430
+
431
+ // Build NETWORK REQUEST body with all fields including request/response data and headers
432
+ static int build_body_network_request(
433
+ const char *request_id,
434
+ const char *page_visit_id,
435
+ const char *recording_session_id,
436
+ const char *service_uuid,
437
+ uint64_t timestamp_start,
438
+ uint64_t timestamp_end,
439
+ int response_code,
440
+ int success,
441
+ const char *error,
442
+ const char *url,
443
+ const char *method,
444
+ const char *request_data,
445
+ const char *response_data,
446
+ const char *request_headers,
447
+ const char *response_headers,
448
+ const char *name,
449
+ const char *parent_span_id, // NULL if not in function span
450
+ char **out_body,
451
+ size_t *out_len
452
+ ) {
453
+ // Escape all fields including headers
454
+ // Headers are JSON strings from Python that must be escaped to embed as string values
455
+ char *req_id_esc = json_escape(request_id);
456
+ char *pv_id_esc = json_escape(page_visit_id);
457
+ char *rec_sid_esc = json_escape(recording_session_id);
458
+ char *svc_uuid_esc = json_escape(service_uuid);
459
+ char *err_esc = json_escape(error);
460
+ char *url_esc = json_escape(url);
461
+ char *method_esc = json_escape(method);
462
+ char *req_data_esc = json_escape(request_data);
463
+ char *resp_data_esc = json_escape(response_data);
464
+ char *req_hdrs_esc = json_escape(request_headers);
465
+ char *resp_hdrs_esc = json_escape(response_headers);
466
+ char *name_esc = json_escape(name);
467
+ char *pspanid_esc = parent_span_id ? json_escape(parent_span_id) : NULL;
468
+
469
+ if (!req_id_esc || !pv_id_esc || !rec_sid_esc || !svc_uuid_esc ||
470
+ !err_esc || !url_esc || !method_esc || !req_data_esc || !resp_data_esc ||
471
+ !req_hdrs_esc || !resp_hdrs_esc || !name_esc) {
472
+ free(req_id_esc); free(pv_id_esc); free(rec_sid_esc); free(svc_uuid_esc);
473
+ free(err_esc); free(url_esc); free(method_esc); free(req_data_esc); free(resp_data_esc);
474
+ free(req_hdrs_esc); free(resp_hdrs_esc); free(name_esc); free(pspanid_esc);
475
+ return 0;
476
+ }
477
+
478
+ // Build JSON fields
479
+ const char *k_req_id = ",\"requestId\":\"";
480
+ const char *k_pv_id = "\",\"pageVisitId\":\"";
481
+ const char *k_rec_sid = "\",\"recordingSessionId\":\"";
482
+ const char *k_svc_uuid = "\",\"serviceUuid\":\"";
483
+ const char *k_ts_start = "\",\"timestampStart\":";
484
+ const char *k_ts_end = ",\"timestampEnd\":";
485
+ const char *k_resp_code = ",\"responseCode\":";
486
+ const char *k_success = ",\"success\":";
487
+ const char *k_error = ",\"error\":";
488
+ const char *k_url = ",\"url\":\"";
489
+ const char *k_method = "\",\"method\":\"";
490
+ const char *k_req_data = "\",\"requestBody\":\"";
491
+ const char *k_resp_data = "\",\"responseBody\":\"";
492
+ const char *k_req_hdrs = "\",\"requestHeaders\":\"";
493
+ const char *k_resp_hdrs = "\",\"responseHeaders\":\"";
494
+ const char *k_name = "\",\"name\":";
495
+ const char *k_pspanid = ",\"parentSpanId\":"; // null or "span-123"
496
+
497
+ char ts_start_buf[32], ts_end_buf[32], resp_code_buf[16];
498
+ snprintf(ts_start_buf, sizeof(ts_start_buf), "%llu", (unsigned long long)timestamp_start);
499
+ snprintf(ts_end_buf, sizeof(ts_end_buf), "%llu", (unsigned long long)timestamp_end);
500
+ snprintf(resp_code_buf, sizeof(resp_code_buf), "%d", response_code);
501
+ const char *success_str = success ? "true" : "false";
502
+ const char *error_str = error ? "\"" : "null";
503
+ const char *error_end = error ? "\"" : "";
504
+
505
+ if (!g_json_prefix) {
506
+ free(req_id_esc); free(pv_id_esc); free(rec_sid_esc); free(svc_uuid_esc);
507
+ free(err_esc); free(url_esc); free(method_esc); free(req_data_esc); free(resp_data_esc);
508
+ free(req_hdrs_esc); free(resp_hdrs_esc); free(name_esc); free(pspanid_esc);
509
+ return 0;
510
+ }
511
+
512
+ // Handle name field (could be NULL for non-GraphQL requests)
513
+ const char *name_str = name ? "\"" : "null";
514
+ const char *name_end = name ? "\"" : "";
515
+
516
+ size_t len = strlen(g_json_prefix)
517
+ + strlen(k_req_id) + strlen(req_id_esc)
518
+ + strlen(k_pv_id) + strlen(pv_id_esc)
519
+ + strlen(k_rec_sid) + strlen(rec_sid_esc)
520
+ + strlen(k_svc_uuid) + strlen(svc_uuid_esc)
521
+ + strlen(k_ts_start) + strlen(ts_start_buf)
522
+ + strlen(k_ts_end) + strlen(ts_end_buf)
523
+ + strlen(k_resp_code) + strlen(resp_code_buf)
524
+ + strlen(k_success) + strlen(success_str)
525
+ + strlen(k_error) + strlen(error_str) + strlen(err_esc) + strlen(error_end)
526
+ + strlen(k_url) + strlen(url_esc)
527
+ + strlen(k_method) + strlen(method_esc)
528
+ + strlen(k_req_data) + strlen(req_data_esc)
529
+ + strlen(k_resp_data) + strlen(resp_data_esc)
530
+ + strlen(k_req_hdrs) + strlen(req_hdrs_esc)
531
+ + strlen(k_resp_hdrs) + strlen(resp_hdrs_esc)
532
+ + strlen(k_name) + strlen(name_str) + strlen(name_esc) + strlen(name_end);
533
+
534
+ // Add parentSpanId field (null or "span-id")
535
+ if (pspanid_esc) {
536
+ len += strlen(k_pspanid) + 1 + strlen(pspanid_esc) + 1; // ,"parentSpanId":"span-id"
537
+ } else {
538
+ len += strlen(k_pspanid) + 4; // ,"parentSpanId":null
539
+ }
540
+
541
+ len += strlen(JSON_SUFFIX) + 10;
542
+
543
+ char *body = (char*)malloc(len + 1);
544
+ if (!body) {
545
+ free(req_id_esc); free(pv_id_esc); free(rec_sid_esc); free(svc_uuid_esc);
546
+ free(err_esc); free(url_esc); free(method_esc); free(req_data_esc); free(resp_data_esc);
547
+ free(req_hdrs_esc); free(resp_hdrs_esc); free(name_esc); free(pspanid_esc);
548
+ return 0;
549
+ }
550
+
551
+ char *o = body;
552
+ o += sprintf(o, "%s", g_json_prefix);
553
+ o += sprintf(o, "%s%s", k_req_id, req_id_esc);
554
+ o += sprintf(o, "%s%s", k_pv_id, pv_id_esc);
555
+ o += sprintf(o, "%s%s", k_rec_sid, rec_sid_esc);
556
+ o += sprintf(o, "%s%s", k_svc_uuid, svc_uuid_esc);
557
+ o += sprintf(o, "%s%s", k_ts_start, ts_start_buf);
558
+ o += sprintf(o, "%s%s", k_ts_end, ts_end_buf);
559
+ o += sprintf(o, "%s%s", k_resp_code, resp_code_buf);
560
+ o += sprintf(o, "%s%s", k_success, success_str);
561
+ if (error) {
562
+ o += sprintf(o, "%s\"%s\"", k_error, err_esc);
563
+ } else {
564
+ o += sprintf(o, "%snull", k_error);
565
+ }
566
+ o += sprintf(o, "%s%s", k_url, url_esc);
567
+ o += sprintf(o, "%s%s", k_method, method_esc);
568
+ o += sprintf(o, "%s%s", k_req_data, req_data_esc);
569
+ o += sprintf(o, "%s%s", k_resp_data, resp_data_esc);
570
+ o += sprintf(o, "%s%s", k_req_hdrs, req_hdrs_esc);
571
+ o += sprintf(o, "%s%s", k_resp_hdrs, resp_hdrs_esc);
572
+ // Add name field (handles both GraphQL and non-GraphQL)
573
+ if (name) {
574
+ o += sprintf(o, "%s\"%s\"", k_name, name_esc);
575
+ } else {
576
+ o += sprintf(o, "%snull", k_name);
577
+ }
578
+
579
+ // Add parentSpanId field
580
+ if (pspanid_esc) {
581
+ o += sprintf(o, "%s\"%s\"", k_pspanid, pspanid_esc);
582
+ } else {
583
+ o += sprintf(o, "%snull", k_pspanid);
584
+ }
585
+
586
+ o += sprintf(o, "%s", JSON_SUFFIX);
587
+ *o = '\0';
588
+
589
+ *out_body = body;
590
+ *out_len = (size_t)(o - body);
591
+
592
+ free(req_id_esc); free(pv_id_esc); free(rec_sid_esc); free(svc_uuid_esc);
593
+ free(err_esc); free(url_esc); free(method_esc); free(req_data_esc); free(resp_data_esc);
594
+ free(req_hdrs_esc); free(resp_hdrs_esc); free(name_esc); free(pspanid_esc);
595
+ return 1;
596
+ }
597
+
598
+ // ---------- ring ops ----------
599
+ static inline size_t ring_count(void) {
600
+ size_t h = atomic_load_explicit(&g_head, memory_order_acquire);
601
+ size_t t = atomic_load_explicit(&g_tail, memory_order_acquire);
602
+ return t - h;
603
+ }
604
+ static inline int ring_empty(void) { return ring_count() == 0; }
605
+
606
+ static int ring_push(char *body, size_t len) {
607
+ while (atomic_flag_test_and_set_explicit(&g_push_lock, memory_order_acquire)) {
608
+ // brief spin
609
+ }
610
+ size_t t = atomic_load_explicit(&g_tail, memory_order_relaxed);
611
+ size_t h = atomic_load_explicit(&g_head, memory_order_acquire);
612
+ if ((t - h) >= g_cap) {
613
+ atomic_flag_clear_explicit(&g_push_lock, memory_order_release);
614
+ if (SF_DEBUG) {
615
+ fprintf(stderr, "[DEBUG ring_push] RING FULL! Dropped message (len=%zu)\n", len);
616
+ fflush(stderr);
617
+ }
618
+ return 0; // full (drop)
619
+ }
620
+ size_t idx = t % g_cap;
621
+ g_ring[idx].body = body;
622
+ g_ring[idx].len = len;
623
+ atomic_store_explicit(&g_tail, t + 1, memory_order_release);
624
+ atomic_flag_clear_explicit(&g_push_lock, memory_order_release);
625
+
626
+ if (SF_DEBUG) {
627
+ // Show first 200 chars of body for debugging
628
+ char preview[201];
629
+ size_t preview_len = len < 200 ? len : 200;
630
+ memcpy(preview, body, preview_len);
631
+ preview[preview_len] = '\0';
632
+ fprintf(stderr, "[DEBUG ring_push] PUSHED to ring: len=%zu, head=%zu, tail=%zu, preview=%.200s\n",
633
+ len, h, t + 1, preview);
634
+ fflush(stderr);
635
+ }
636
+
637
+ pthread_mutex_lock(&g_cv_mtx);
638
+ pthread_cond_signal(&g_cv);
639
+ pthread_mutex_unlock(&g_cv_mtx);
640
+ return 1;
641
+ }
642
+
643
+ static int ring_pop(char **body, size_t *len) {
644
+ size_t h = atomic_load_explicit(&g_head, memory_order_relaxed);
645
+ size_t t = atomic_load_explicit(&g_tail, memory_order_acquire);
646
+ if (h == t) return 0;
647
+ size_t idx = h % g_cap;
648
+ *body = g_ring[idx].body;
649
+ *len = g_ring[idx].len;
650
+ g_ring[idx].body = NULL;
651
+ g_ring[idx].len = 0;
652
+ atomic_store_explicit(&g_head, h + 1, memory_order_release);
653
+ return 1;
654
+ }
655
+
656
+ // ---------- curl sink callbacks ----------
657
+ static size_t _sink_write(char *ptr, size_t size, size_t nmemb, void *userdata) {
658
+ (void)ptr; (void)userdata;
659
+ return size * nmemb;
660
+ }
661
+ static size_t _sink_header(char *ptr, size_t size, size_t nmemb, void *userdata) {
662
+ (void)ptr; (void)userdata;
663
+ return size * nmemb;
664
+ }
665
+
666
+ // ---------- sender thread ----------
667
+ static void *sender_main(void *arg) {
668
+ (void)arg;
669
+
670
+ // CRITICAL: Set telemetry guard for this thread (prevents _sfteepreload.c capture)
671
+ sf_guard_enter();
672
+
673
+ // Initialize thread-local curl handle
674
+ g_telem_curl = curl_easy_init();
675
+ if (!g_telem_curl) {
676
+ sf_guard_leave();
677
+ return NULL;
678
+ }
679
+
680
+ // Configure curl handle (copy from global settings)
681
+ curl_easy_setopt(g_telem_curl, CURLOPT_URL, g_url);
682
+ curl_easy_setopt(g_telem_curl, CURLOPT_TCP_KEEPALIVE, 1L);
683
+ curl_easy_setopt(g_telem_curl, CURLOPT_TCP_NODELAY, 1L); // NEW: Eliminate Nagle delay
684
+ curl_easy_setopt(g_telem_curl, CURLOPT_HTTPHEADER, g_hdrs);
685
+ #ifdef CURL_HTTP_VERSION_2TLS
686
+ if (g_http2) {
687
+ curl_easy_setopt(g_telem_curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS);
688
+ }
689
+ #endif
690
+ // Disable SSL verification for local testing with self-signed certificates
691
+ curl_easy_setopt(g_telem_curl, CURLOPT_SSL_VERIFYPEER, 0L);
692
+ curl_easy_setopt(g_telem_curl, CURLOPT_SSL_VERIFYHOST, 0L);
693
+ if (SF_DEBUG) {
694
+ fprintf(stderr, "[DEBUG sender_main] SSL verification DISABLED (for self-signed certs)\n");
695
+ fflush(stderr);
696
+ }
697
+ curl_easy_setopt(g_telem_curl, CURLOPT_WRITEFUNCTION, _sink_write);
698
+ curl_easy_setopt(g_telem_curl, CURLOPT_HEADERFUNCTION, _sink_header);
699
+
700
+ while (atomic_load(&g_running)) {
701
+ if (ring_empty()) {
702
+ pthread_mutex_lock(&g_cv_mtx);
703
+ if (ring_empty() && atomic_load(&g_running))
704
+ pthread_cond_wait(&g_cv, &g_cv_mtx);
705
+ pthread_mutex_unlock(&g_cv_mtx);
706
+ if (!atomic_load(&g_running)) break;
707
+ }
708
+ char *body = NULL; size_t len = 0;
709
+ while (ring_pop(&body, &len)) {
710
+ if (!body) continue;
711
+
712
+ if (SF_DEBUG) {
713
+ // Show first 200 chars of body being sent
714
+ char preview[201];
715
+ size_t preview_len = len < 200 ? len : 200;
716
+ memcpy(preview, body, preview_len);
717
+ preview[preview_len] = '\0';
718
+ fprintf(stderr, "[DEBUG sender_main] POPPED from ring: len=%zu, sending to %s\n", len, g_url);
719
+ fprintf(stderr, "[DEBUG sender_main] Body preview: %.200s\n", preview);
720
+ fflush(stderr);
721
+ }
722
+
723
+ // Use thread-local curl handle (each thread has its own persistent connection)
724
+ curl_easy_setopt(g_telem_curl, CURLOPT_POSTFIELDS, body);
725
+ curl_easy_setopt(g_telem_curl, CURLOPT_POSTFIELDSIZE, (long)len);
726
+
727
+ CURLcode res = curl_easy_perform(g_telem_curl);
728
+
729
+ if (SF_DEBUG) {
730
+ if (res != CURLE_OK) {
731
+ fprintf(stderr, "[DEBUG sender_main] curl_easy_perform FAILED: %s\n", curl_easy_strerror(res));
732
+ } else {
733
+ long response_code = 0;
734
+ curl_easy_getinfo(g_telem_curl, CURLINFO_RESPONSE_CODE, &response_code);
735
+ fprintf(stderr, "[DEBUG sender_main] curl_easy_perform SUCCESS: HTTP %ld\n", response_code);
736
+ }
737
+ fflush(stderr);
738
+ }
739
+
740
+ free(body);
741
+ if (!atomic_load(&g_running)) break;
742
+ }
743
+ }
744
+
745
+ if (g_telem_curl) {
746
+ curl_easy_cleanup(g_telem_curl);
747
+ g_telem_curl = NULL;
748
+ }
749
+ sf_guard_leave();
750
+ return NULL;
751
+ }
752
+
753
+ // ---------- Python API ----------
754
+ static PyObject *py_init(PyObject *self, PyObject *args, PyObject *kw) {
755
+ // Initialize SF_DEBUG from environment variable (once)
756
+ static int initialized = 0;
757
+ if (!initialized) {
758
+ const char *debug_val = getenv("SF_DEBUG");
759
+ SF_DEBUG = (debug_val && (strcmp(debug_val, "True") == 0 || strcmp(debug_val, "true") == 0 || strcmp(debug_val, "1") == 0)) ? 1 : 0;
760
+ initialized = 1;
761
+ }
762
+
763
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG py_init] CALLED!\n");
764
+ fflush(stderr);
765
+
766
+ const char *url, *query, *api_key;
767
+ int http2 = 0;
768
+ static char *kwlist[] = {"url","query","api_key","http2", NULL};
769
+ if (!PyArg_ParseTupleAndKeywords(args, kw, "sss|i",
770
+ kwlist, &url, &query, &api_key, &http2)) {
771
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG py_init] FAIL: arg parse error\n");
772
+ fflush(stderr);
773
+ Py_RETURN_FALSE;
774
+ }
775
+ if (g_running) {
776
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG py_init] Already running, returning True\n");
777
+ fflush(stderr);
778
+ Py_RETURN_TRUE;
779
+ }
780
+
781
+ g_url = str_dup(url);
782
+ g_query_escaped = json_escape_query(query);
783
+ g_api_key = str_dup(api_key);
784
+ g_http2 = http2 ? 1 : 0;
785
+ if (!g_url || !g_query_escaped || !g_api_key) {
786
+ Py_RETURN_FALSE;
787
+ }
788
+ if (!build_prefix()) { Py_RETURN_FALSE; }
789
+
790
+ g_cap = SFN_RING_CAP;
791
+ g_ring = (sfn_msg_t*)calloc(g_cap, sizeof(sfn_msg_t));
792
+ if (!g_ring) { Py_RETURN_FALSE; }
793
+
794
+ // Parse SF_FASTNET_SENDER_THREADS environment variable
795
+ const char *env_threads = getenv("SF_FASTNET_SENDER_THREADS");
796
+ if (env_threads) {
797
+ int t = atoi(env_threads);
798
+ if (t > 0 && t <= MAX_SENDER_THREADS) {
799
+ g_configured_sender_threads = t;
800
+ }
801
+ }
802
+
803
+ // Initialize curl (shared headers only - handles are per-thread)
804
+ curl_global_init(CURL_GLOBAL_DEFAULT);
805
+ g_hdrs = NULL;
806
+ g_hdrs = curl_slist_append(g_hdrs, "Content-Type: application/json");
807
+
808
+ // Start sender thread pool
809
+ atomic_store(&g_running, 1);
810
+ g_num_sender_threads = g_configured_sender_threads;
811
+ for (int i = 0; i < g_num_sender_threads; i++) {
812
+ if (pthread_create(&g_sender_threads[i], NULL, sender_main, NULL) != 0) {
813
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG py_init] FAIL: pthread_create failed for thread %d\n", i);
814
+ fflush(stderr);
815
+ atomic_store(&g_running, 0);
816
+ // Clean up already-started threads
817
+ for (int j = 0; j < i; j++) {
818
+ pthread_join(g_sender_threads[j], NULL);
819
+ }
820
+ Py_RETURN_FALSE;
821
+ }
822
+ }
823
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG py_init] SUCCESS: _sffastnet initialized with %d threads!\n", g_num_sender_threads);
824
+ fflush(stderr);
825
+ Py_RETURN_TRUE;
826
+ }
827
+
828
+ // Ultra-fast path - use s# format for zero-copy bytes access like _sffastlog
829
+ static PyObject *py_network_request(PyObject *self, PyObject *args) {
830
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG py_network_request] CALLED!\n");
831
+ fflush(stderr);
832
+
833
+ // Accept bytes objects with lengths for zero-copy access
834
+ // Order: request_id, page_visit_id, recording_session_id, service_uuid,
835
+ // timestamp_start, timestamp_end, response_code, success,
836
+ // error, url, method, request_data, response_data,
837
+ // request_headers, response_headers
838
+
839
+ if (!g_running || !g_json_prefix) {
840
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG py_network_request] EARLY RETURN: g_running=%d, g_json_prefix=%p\n", g_running, g_json_prefix);
841
+ fflush(stderr);
842
+ Py_RETURN_NONE;
843
+ }
844
+
845
+ const char *req_id, *pv_id, *rec_sid, *svc_uuid, *url, *method, *error;
846
+ const char *req_data, *resp_data, *req_hdrs, *resp_hdrs;
847
+ const char *parent_span_id = NULL;
848
+ Py_ssize_t req_id_len, pv_id_len, rec_sid_len, svc_uuid_len, url_len, method_len;
849
+ Py_ssize_t req_data_len = 0, resp_data_len = 0, req_hdrs_len = 0, resp_hdrs_len = 0;
850
+ unsigned long long ts_start, ts_end;
851
+ int resp_code, success;
852
+ PyObject *o_error = NULL;
853
+
854
+ // Use s# for bytes (zero-copy), y# also works for bytes objects
855
+ // Parse as: str, str, str, str, int, int, int, bool, obj, str, str, bytes, bytes, bytes, bytes, optional str
856
+ if (!PyArg_ParseTuple(args, "ssssKKipOssy#y#y#y#|z",
857
+ &req_id, &pv_id, &rec_sid, &svc_uuid,
858
+ &ts_start, &ts_end, &resp_code, &success,
859
+ &o_error, &url, &method,
860
+ &req_data, &req_data_len,
861
+ &resp_data, &resp_data_len,
862
+ &req_hdrs, &req_hdrs_len,
863
+ &resp_hdrs, &resp_hdrs_len,
864
+ &parent_span_id)) {
865
+ Py_RETURN_NONE;
866
+ }
867
+
868
+ error = (o_error != Py_None) ? PyUnicode_AsUTF8(o_error) : NULL;
869
+
870
+ char *body = NULL;
871
+ size_t len = 0;
872
+ int ok = 0;
873
+
874
+ // Create null-terminated strings from byte buffers for JSON building
875
+ // Allocate on stack for small strings, heap for large
876
+ char req_data_buf[65536]; // 64KB max
877
+ char resp_data_buf[65536];
878
+ char req_hdrs_buf[8192]; // 8KB max
879
+ char resp_hdrs_buf[8192];
880
+
881
+ // Copy and null-terminate (safe truncation)
882
+ size_t req_data_copy = req_data_len > 65535 ? 65535 : req_data_len;
883
+ size_t resp_data_copy = resp_data_len > 65535 ? 65535 : resp_data_len;
884
+ size_t req_hdrs_copy = req_hdrs_len > 8191 ? 8191 : req_hdrs_len;
885
+ size_t resp_hdrs_copy = resp_hdrs_len > 8191 ? 8191 : resp_hdrs_len;
886
+
887
+ memcpy(req_data_buf, req_data, req_data_copy);
888
+ req_data_buf[req_data_copy] = '\0';
889
+
890
+ memcpy(resp_data_buf, resp_data, resp_data_copy);
891
+ resp_data_buf[resp_data_copy] = '\0';
892
+
893
+ memcpy(req_hdrs_buf, req_hdrs, req_hdrs_copy);
894
+ req_hdrs_buf[req_hdrs_copy] = '\0';
895
+
896
+ memcpy(resp_hdrs_buf, resp_hdrs, resp_hdrs_copy);
897
+ resp_hdrs_buf[resp_hdrs_copy] = '\0';
898
+
899
+ // Everything from here runs without GIL
900
+ Py_BEGIN_ALLOW_THREADS
901
+ // Extract GraphQL operation name from request body if present
902
+ // Fast path: 2ns for non-GraphQL, 15-18ns for GraphQL
903
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG py_network_request] Calling extract_graphql_name with req_data_copy=%zu bytes\n", req_data_copy);
904
+ char *graphql_name = extract_graphql_name(req_data_buf, req_data_copy);
905
+ if (SF_DEBUG) fprintf(stderr, "[DEBUG py_network_request] extract_graphql_name returned: %s\n", graphql_name ? graphql_name : "NULL");
906
+
907
+ if (build_body_network_request(
908
+ req_id, pv_id, rec_sid, svc_uuid,
909
+ ts_start, ts_end,
910
+ resp_code, success, error,
911
+ url, method,
912
+ req_data_buf, resp_data_buf,
913
+ req_hdrs_buf, resp_hdrs_buf,
914
+ graphql_name,
915
+ parent_span_id,
916
+ &body, &len)) {
917
+ ok = ring_push(body, len);
918
+ if (SF_DEBUG) {
919
+ fprintf(stderr, "[DEBUG py_network_request] ring_push returned: %s (len=%zu)\n", ok ? "SUCCESS" : "FAILED", len);
920
+ fflush(stderr);
921
+ }
922
+ } else {
923
+ if (SF_DEBUG) {
924
+ fprintf(stderr, "[DEBUG py_network_request] build_body_network_request FAILED\n");
925
+ fflush(stderr);
926
+ }
927
+ }
928
+
929
+ // Free the malloc'd GraphQL name if extracted
930
+ if (graphql_name) free(graphql_name);
931
+ Py_END_ALLOW_THREADS
932
+
933
+ if (!ok && body) free(body);
934
+ Py_RETURN_NONE;
935
+ }
936
+
937
+ static PyObject *py_shutdown(PyObject *self, PyObject *args) {
938
+ if (!g_running) Py_RETURN_NONE;
939
+
940
+ atomic_store(&g_running, 0);
941
+
942
+ // Wake ALL threads with broadcast (not signal)
943
+ pthread_mutex_lock(&g_cv_mtx);
944
+ pthread_cond_broadcast(&g_cv);
945
+ pthread_mutex_unlock(&g_cv_mtx);
946
+
947
+ // Join all sender threads in thread pool
948
+ for (int i = 0; i < g_num_sender_threads; i++) {
949
+ if (g_sender_threads[i]) {
950
+ pthread_join(g_sender_threads[i], NULL);
951
+ g_sender_threads[i] = 0;
952
+ }
953
+ }
954
+ g_num_sender_threads = 0;
955
+
956
+ // Cleanup curl (per-thread handles cleaned by pthread_cleanup_push)
957
+ if (g_hdrs) { curl_slist_free_all(g_hdrs); g_hdrs = NULL; }
958
+ curl_global_cleanup();
959
+
960
+ // Free all config strings and NULL pointers
961
+ free(g_url); g_url = NULL;
962
+ free(g_query_escaped); g_query_escaped = NULL;
963
+ free(g_json_prefix); g_json_prefix = NULL;
964
+ free(g_api_key); g_api_key = NULL;
965
+
966
+ // Drain and free ring buffer
967
+ if (g_ring) {
968
+ char *b; size_t l;
969
+ while (ring_pop(&b, &l)) free(b);
970
+ free(g_ring); g_ring = NULL;
971
+ }
972
+
973
+ Py_RETURN_NONE;
974
+ }
975
+
976
+ // ---------- Module table ----------
977
+ static PyMethodDef SFFastNetMethods[] = {
978
+ {"init", (PyCFunction)py_init, METH_VARARGS | METH_KEYWORDS, "Init network request tracking and start sender"},
979
+ {"network_request", py_network_request, METH_VARARGS, "Send network request (fast positional-only)"},
980
+ {"shutdown", (PyCFunction)py_shutdown, METH_NOARGS, "Shutdown sender and free state"},
981
+ {NULL, NULL, 0, NULL}
982
+ };
983
+
984
+ static struct PyModuleDef sffastnetmodule = {
985
+ PyModuleDef_HEAD_INIT,
986
+ "_sffastnet",
987
+ "sf_veritas ultra-fast network request tracking with request/response data",
988
+ -1,
989
+ SFFastNetMethods
990
+ };
991
+
992
+ PyMODINIT_FUNC PyInit__sffastnet(void) {
993
+ return PyModule_Create(&sffastnetmodule);
994
+ }