sf-veritas 0.10.3__cp311-cp311-manylinux_2_28_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sf-veritas might be problematic. Click here for more details.

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