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,543 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import re
5
+ import threading
6
+ import time
7
+ import uuid
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ from . import app_config, transmit_exception_to_sailfish
11
+ from .env_vars import PRINT_CONFIGURATION_STATUSES, SF_DEBUG, SF_LOG_IGNORE_REGEX
12
+ from .package_metadata import PACKAGE_LIBRARY_TYPE, __version__
13
+ from .regular_data_transmitter import ServiceIdentifier
14
+ from .request_utils import non_blocking_post
15
+ from .thread_local import ( # reentrancy_guard, activate_reentrancy_guards_logging_preactive,
16
+ activate_reentrancy_guards_logging,
17
+ get_current_function_span_id,
18
+ get_or_set_sf_trace_id,
19
+ )
20
+ from .timeutil import TimeSync
21
+ from .types import CustomJSONEncoderForFrameInfo, FrameInfo
22
+ from .utils import serialize_json_with_exclusions, strtobool
23
+
24
+ # Precompile once (was re.match(pattern,..) per log)
25
+ # Loaded from SF_LOG_IGNORE_REGEX environment variable (default: suppress /healthz and /graphql/ 2xx)
26
+ _IGNORE_RE = re.compile(SF_LOG_IGNORE_REGEX)
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class OutputInterceptor(object):
32
+ def __init__(self, api_key: str = None):
33
+ self.api_key = api_key or app_config._sailfish_api_key
34
+ self.endpoint = app_config._sailfish_graphql_endpoint
35
+ self.operation_name: Optional[str] = ""
36
+ self.query_type = "mutation"
37
+ self.service_identifier = ServiceIdentifier()
38
+
39
+ @property
40
+ def query_name(self) -> str:
41
+ return (
42
+ self.operation_name[0].lower() + self.operation_name[1:]
43
+ if self.operation_name
44
+ else ""
45
+ )
46
+
47
+ def get_default_variables(self, session_id: Optional[str] = None):
48
+ trace_id = session_id
49
+ if not session_id:
50
+ _, trace_id = get_or_set_sf_trace_id(session_id)
51
+ timestamp_ms = TimeSync.get_instance().get_utc_time_in_ms()
52
+ return {
53
+ "apiKey": self.api_key,
54
+ "serviceUuid": app_config._service_uuid,
55
+ "library": PACKAGE_LIBRARY_TYPE,
56
+ "sessionId": trace_id,
57
+ "timestampMs": str(timestamp_ms),
58
+ "version": __version__,
59
+ }
60
+
61
+ def get_variables(
62
+ self,
63
+ additional_variables: Optional[Dict[str, Any]] = None,
64
+ session_id: Optional[str] = None,
65
+ ) -> Dict[str, Any]:
66
+ additional_variables = additional_variables or {}
67
+ return {**additional_variables, **self.get_default_variables(session_id)}
68
+
69
+ def check_if_contents_should_be_ignored(
70
+ self, contents
71
+ ): # pylint: disable=unused-argument
72
+ return False
73
+
74
+ def _send_app_identifier(self, session_id: str) -> None:
75
+ if SF_DEBUG:
76
+ print("_send_app_identifier...SENDING DATA...args=", set(), log=False)
77
+ self.service_identifier.do_send(set())
78
+
79
+ def do_send(self, args, session_id: str) -> None:
80
+ self._send_app_identifier(session_id)
81
+ if SF_DEBUG:
82
+ print(f"[[OutputInterceptor.do_send]] session_id={session_id}", log=False)
83
+ try:
84
+ self.send(*args)
85
+ except RuntimeError:
86
+ return
87
+
88
+
89
+ # sf_veritas/interceptors.py (excerpt: LogInterceptor)
90
+ import time
91
+ from typing import Callable, Tuple
92
+
93
+ from . import app_config
94
+ from .env_vars import SF_DEBUG
95
+ from .request_utils import non_blocking_post_deferred # Python fallback
96
+ from .thread_local import get_reentrancy_guard_logging_preactive
97
+
98
+ # Try native fast path (compiled C extension)
99
+ try:
100
+ from . import _sffastlog
101
+
102
+ if SF_DEBUG:
103
+ print("[[interceptors.py]] Imported _sffastlog")
104
+
105
+ _FAST_OK = True
106
+ except Exception:
107
+ if SF_DEBUG:
108
+ print("[[interceptors.py]] UNABLE to import _sffastlog")
109
+ _sffastlog = None
110
+ _FAST_OK = False
111
+
112
+ # GraphQL mutation (camelCase variables) — keep identical to your server schema
113
+ _COLLECT_LOGS_OP = "CollectLogs"
114
+ _COLLECT_LOGS_MUTATION = """
115
+ mutation CollectLogs(
116
+ $apiKey: String!,
117
+ $serviceUuid: String!,
118
+ $sessionId: String!,
119
+ $level: String!,
120
+ $contents: String!,
121
+ $reentrancyGuardPreactive: Boolean!,
122
+ $library: String!,
123
+ $timestampMs: String!,
124
+ $version: String!,
125
+ $parentSpanId: String
126
+ ) {
127
+ collectLogs(
128
+ apiKey: $apiKey,
129
+ serviceUuid: $serviceUuid,
130
+ sessionId: $sessionId,
131
+ level: $level,
132
+ contents: $contents,
133
+ reentrancyGuardPreactive: $reentrancyGuardPreactive,
134
+ library: $library,
135
+ timestampMs: $timestampMs,
136
+ version: $version,
137
+ parentSpanId: $parentSpanId
138
+ )
139
+ }
140
+ """.strip()
141
+
142
+ # ---------- Prints (GraphQL identical to your current schema) ----------
143
+ _COLLECT_PRINT_OP = "CollectPrintStatements"
144
+ _COLLECT_PRINT_MUTATION = """
145
+ mutation CollectPrintStatements(
146
+ $apiKey: String!,
147
+ $serviceUuid: String!,
148
+ $sessionId: String!,
149
+ $contents: String!,
150
+ $reentrancyGuardPreactive: Boolean!,
151
+ $library: String!,
152
+ $timestampMs: String!,
153
+ $version: String!,
154
+ $parentSpanId: String
155
+ ) {
156
+ collectPrintStatements(
157
+ apiKey: $apiKey,
158
+ serviceUuid: $serviceUuid,
159
+ sessionId: $sessionId,
160
+ contents: $contents,
161
+ reentrancyGuardPreactive: $reentrancyGuardPreactive,
162
+ library: $library,
163
+ timestampMs: $timestampMs,
164
+ version: $version,
165
+ parentSpanId: $parentSpanId
166
+ )
167
+ }
168
+ """.strip()
169
+
170
+
171
+ class LogInterceptor:
172
+ """
173
+ Uses native _sffastlog if present; otherwise falls back to Python deferred sender.
174
+ """
175
+
176
+ def __init__(self, api_key: str):
177
+ self.api_key = api_key
178
+ # Use app_config instead of os.environ to avoid KeyError
179
+ self.endpoint = getattr(app_config, "_sailfish_graphql_endpoint", None)
180
+ self.service_uuid = (
181
+ getattr(app_config, "_service_uuid", None)
182
+ or getattr(app_config, "service_uuid", None)
183
+ or "unknown"
184
+ )
185
+ self.library = getattr(app_config, "library", "sailfish-python")
186
+ self.version = getattr(app_config, "version", "0.0.0")
187
+
188
+ if _FAST_OK and self.endpoint:
189
+ try:
190
+ http2 = 1 if os.getenv("SF_NBPOST_HTTP2", "0") == "1" else 0
191
+ if SF_DEBUG:
192
+ print(
193
+ f"[[LogInterceptor.__init__]] Calling _sffastlog.init() with url={self.endpoint}"
194
+ )
195
+ ok = _sffastlog.init(
196
+ url=self.endpoint,
197
+ query=_COLLECT_LOGS_MUTATION,
198
+ api_key=self.api_key,
199
+ service_uuid=str(self.service_uuid),
200
+ library=str(self.library),
201
+ version=str(self.version),
202
+ http2=http2,
203
+ )
204
+ if ok and PRINT_CONFIGURATION_STATUSES:
205
+ print("[_sffastlog] initialized (libcurl sender for logs)")
206
+ elif PRINT_CONFIGURATION_STATUSES:
207
+ print(f"[_sffastlog] init returned {ok}")
208
+ except Exception as e:
209
+ if PRINT_CONFIGURATION_STATUSES:
210
+ print(f"[_sffastlog] init failed; falling back: {e}")
211
+
212
+ def check_if_contents_should_be_ignored(self, contents: str) -> bool:
213
+ """
214
+ Check if log contents should be ignored (not sent to Sailfish).
215
+ Uses SF_LOG_IGNORE_REGEX environment variable (default: suppress /healthz and /graphql/ 2xx).
216
+
217
+ Returns:
218
+ True if the log should be ignored, False otherwise
219
+ """
220
+ return _IGNORE_RE.match(contents or "") is not None
221
+
222
+ def do_send(self, payload: Tuple[str, str, str], trace_id: str):
223
+ """
224
+ payload: (log_level, log_entry, session_id)
225
+ """
226
+ if SF_DEBUG:
227
+ print(
228
+ f"[[LogInterceptor.do_send]] ...start...",
229
+ log=False,
230
+ )
231
+ level, contents, session_id = payload
232
+ preactive = bool(get_reentrancy_guard_logging_preactive())
233
+
234
+ # Capture parent_span_id IMMEDIATELY for async-safety
235
+ parent_span_id = get_current_function_span_id()
236
+
237
+ if SF_DEBUG:
238
+ print(
239
+ f"[[LogInterceptor.do_send]] level={level}, session_id={session_id}, _FAST_OK={_FAST_OK}, parent_span_id={parent_span_id}",
240
+ log=False,
241
+ )
242
+
243
+ if _FAST_OK:
244
+ try:
245
+ if SF_DEBUG:
246
+ print(
247
+ f"[[LogInterceptor.do_send]] Calling _sffastlog.log()",
248
+ log=False,
249
+ )
250
+ _sffastlog.log(
251
+ level=level or "UNKNOWN",
252
+ contents=contents,
253
+ session_id=str(session_id),
254
+ preactive=preactive,
255
+ parent_span_id=parent_span_id,
256
+ )
257
+ if SF_DEBUG:
258
+ print(
259
+ f"[[LogInterceptor.do_send]] _sffastlog.log() succeeded",
260
+ log=False,
261
+ )
262
+ return
263
+ except Exception as e:
264
+ logger.exception(e)
265
+ transmit_exception_to_sailfish(e)
266
+ if SF_DEBUG:
267
+ print(f"[_sffastlog] log failed; fallback path: {e}", log=False)
268
+
269
+ # --- Python fallback (deferred) ---
270
+ ts_ms = time.time_ns() // 1_000_000
271
+ endpoint = self.endpoint
272
+ op = _COLLECT_LOGS_OP
273
+ query = _COLLECT_LOGS_MUTATION
274
+ api_key = self.api_key
275
+ service_uuid = self.service_uuid
276
+ library = self.library
277
+ version = self.version
278
+
279
+ def _builder():
280
+ vars = {
281
+ "apiKey": api_key,
282
+ "serviceUuid": str(service_uuid),
283
+ "sessionId": str(session_id),
284
+ "level": level or "UNKNOWN",
285
+ "contents": contents,
286
+ "reentrancyGuardPreactive": preactive,
287
+ "library": str(library),
288
+ "timestampMs": str(ts_ms),
289
+ "version": str(version),
290
+ "parentSpanId": parent_span_id,
291
+ }
292
+ return endpoint, op, query, vars
293
+
294
+ non_blocking_post_deferred(_builder)
295
+
296
+ def shutdown(self):
297
+ if _FAST_OK:
298
+ try:
299
+ _sffastlog.shutdown()
300
+ except Exception:
301
+ pass
302
+
303
+
304
+ # ---------------- Prints (NEW native fast path) ----------------
305
+ class PrintInterceptor(OutputInterceptor):
306
+ def __init__(self, api_key: str = None):
307
+ if api_key is None:
308
+ api_key = app_config._sailfish_api_key
309
+ super().__init__(api_key)
310
+ self.operation_name = _COLLECT_PRINT_OP
311
+
312
+ # Cache the query string
313
+ self._QUERY = _COLLECT_PRINT_MUTATION
314
+
315
+ # Native fast path for print, if available
316
+ self._fast_print_ok = False
317
+ if _FAST_OK:
318
+ try:
319
+ http2 = 1 if os.getenv("SF_NBPOST_HTTP2", "0") == "1" else 0
320
+ ok = _sffastlog.init_print(
321
+ url=self.endpoint,
322
+ query=self._QUERY,
323
+ api_key=self.api_key,
324
+ service_uuid=str(app_config._service_uuid),
325
+ library=PACKAGE_LIBRARY_TYPE,
326
+ version=__version__,
327
+ http2=http2,
328
+ )
329
+ self._fast_print_ok = bool(ok)
330
+ if PRINT_CONFIGURATION_STATUSES:
331
+ print("[_sffastlog] initialized (prints)") # , log=False)
332
+ if self._fast_print_ok and PRINT_CONFIGURATION_STATUSES:
333
+ print("[_sffastlog] initialized (prints)") # , log=False)
334
+ except Exception as e:
335
+ logger.exception(e)
336
+ transmit_exception_to_sailfish(e)
337
+ if PRINT_CONFIGURATION_STATUSES:
338
+ print(
339
+ "[_sffastlog] init_print failed; fallback:", e
340
+ ) # , log=False)
341
+
342
+ def send(self, contents: str, session_id: str):
343
+ # Drop obvious noise early (cheap)
344
+ if _IGNORE_RE.match(contents or ""):
345
+ return
346
+
347
+ # Capture parent_span_id IMMEDIATELY for async-safety
348
+ parent_span_id = get_current_function_span_id()
349
+
350
+ preactive = False # printing path uses preactive only if you need it later
351
+ if self._fast_print_ok:
352
+ try:
353
+ _sffastlog.print_( # exposed as print_ to avoid name clash
354
+ contents=contents,
355
+ session_id=str(session_id),
356
+ preactive=preactive,
357
+ parent_span_id=parent_span_id,
358
+ )
359
+ return
360
+ except Exception as e:
361
+ logger.exception(e)
362
+ transmit_exception_to_sailfish(e)
363
+ if SF_DEBUG:
364
+ print("[_sffastlog] print_ failed; fallback:", e, log=False)
365
+
366
+ # Python fallback: fast minimal dict and post
367
+ d = self.get_default_variables(session_id)
368
+ variables = {
369
+ "apiKey": d["apiKey"],
370
+ "serviceUuid": d["serviceUuid"],
371
+ "sessionId": d["sessionId"],
372
+ "library": d["library"],
373
+ "timestampMs": d["timestampMs"],
374
+ "version": d["version"],
375
+ "contents": contents,
376
+ "reentrancyGuardPreactive": False,
377
+ "parentSpanId": parent_span_id,
378
+ }
379
+ non_blocking_post(self.endpoint, self.operation_name, self._QUERY, variables)
380
+
381
+
382
+ _COLLECT_EXCEPTION_OP = "CollectExceptions"
383
+ _COLLECT_EXCEPTION_MUTATION = """
384
+ mutation CollectExceptions(
385
+ $apiKey: String!,
386
+ $serviceUuid: String!,
387
+ $sessionId: String!,
388
+ $exceptionMessage: String!,
389
+ $wasCaught: Boolean!,
390
+ $traceJson: String!,
391
+ $reentrancyGuardPreactive: Boolean!,
392
+ $library: String!,
393
+ $timestampMs: String!,
394
+ $version: String!,
395
+ $isFromLocalService: Boolean!,
396
+ $parentSpanId: String
397
+ ) {
398
+ collectExceptions(
399
+ apiKey: $apiKey,
400
+ serviceUuid: $serviceUuid,
401
+ sessionId: $sessionId,
402
+ exceptionMessage: $exceptionMessage,
403
+ wasCaught: $wasCaught,
404
+ traceJson: $traceJson,
405
+ reentrancyGuardPreactive: $reentrancyGuardPreactive,
406
+ library: $library,
407
+ timestampMs: $timestampMs,
408
+ version: $version,
409
+ isFromLocalService: $isFromLocalService,
410
+ parentSpanId: $parentSpanId
411
+ )
412
+ }
413
+ """.strip()
414
+
415
+
416
+ class ExceptionInterceptor(OutputInterceptor):
417
+ def __init__(self, api_key: str = app_config._sailfish_api_key):
418
+ super().__init__(api_key)
419
+ self.operation_name = _COLLECT_EXCEPTION_OP
420
+ self._QUERY = _COLLECT_EXCEPTION_MUTATION
421
+
422
+ # Native fast path for exceptions, if available
423
+ self._fast_exception_ok = False
424
+ if _FAST_OK:
425
+ try:
426
+ http2 = 1 if os.getenv("SF_NBPOST_HTTP2", "0") == "1" else 0
427
+ ok = _sffastlog.init_exception(
428
+ url=self.endpoint,
429
+ query=self._QUERY,
430
+ api_key=self.api_key,
431
+ service_uuid=str(app_config._service_uuid),
432
+ library=PACKAGE_LIBRARY_TYPE,
433
+ version=__version__,
434
+ http2=http2,
435
+ )
436
+ self._fast_exception_ok = bool(ok)
437
+ if self._fast_exception_ok and PRINT_CONFIGURATION_STATUSES:
438
+ print("[_sffastlog] initialized (exceptions)", log=False)
439
+ if PRINT_CONFIGURATION_STATUSES:
440
+ print(
441
+ f"[_sffastlog] exception initialization status={ok}", log=False
442
+ )
443
+ except Exception as e:
444
+ logger.exception(e)
445
+ transmit_exception_to_sailfish(e)
446
+ if PRINT_CONFIGURATION_STATUSES:
447
+ print("[_sffastlog] init_exception failed; fallback:", e, log=False)
448
+
449
+ def send(
450
+ self,
451
+ exception_message: str,
452
+ trace: List[FrameInfo],
453
+ session_id: str,
454
+ was_caught: bool = True,
455
+ is_from_local_service: bool = False,
456
+ ):
457
+ trace_json = json.dumps(trace, cls=CustomJSONEncoderForFrameInfo)
458
+
459
+ # Capture parent_span_id IMMEDIATELY for async-safety
460
+ parent_span_id = get_current_function_span_id()
461
+
462
+ if self._fast_exception_ok:
463
+ try:
464
+ _sffastlog.exception(
465
+ exception_message=exception_message,
466
+ trace_json=trace_json,
467
+ session_id=str(session_id),
468
+ was_caught=was_caught,
469
+ is_from_local_service=is_from_local_service,
470
+ parent_span_id=parent_span_id,
471
+ )
472
+ return
473
+ except Exception as e:
474
+ logger.exception(e)
475
+ transmit_exception_to_sailfish(e)
476
+ if SF_DEBUG:
477
+ print("[_sffastlog] exception failed; fallback:", e, log=False)
478
+
479
+ # Python fallback
480
+ query = f"""
481
+ {self.query_type} {self.operation_name}($apiKey: String!, $serviceUuid: String!, $sessionId: String!, $exceptionMessage: String!, $wasCaught: Boolean!, $traceJson: String!, $reentrancyGuardPreactive: Boolean!, $library: String!, $timestampMs: String!, $version: String!, $isFromLocalService: Boolean!) {{
482
+ {self.query_name}(apiKey: $apiKey, serviceUuid: $serviceUuid, sessionId: $sessionId, exceptionMessage: $exceptionMessage, wasCaught: $wasCaught, traceJson: $traceJson, reentrancyGuardPreactive: $reentrancyGuardPreactive, library: $library, timestampMs: $timestampMs, version: $version, isFromLocalService: $isFromLocalService)
483
+ }}
484
+ """
485
+
486
+ if SF_DEBUG:
487
+ print("SENDING EXCEPTION...", log=False)
488
+ non_blocking_post(
489
+ self.endpoint,
490
+ self.operation_name,
491
+ query,
492
+ self.get_variables(
493
+ {
494
+ "apiKey": self.api_key,
495
+ "exceptionMessage": exception_message,
496
+ "traceJson": trace_json,
497
+ "reentrancyGuardPreactive": False,
498
+ "wasCaught": was_caught,
499
+ "isFromLocalService": is_from_local_service,
500
+ },
501
+ session_id,
502
+ ),
503
+ )
504
+
505
+
506
+ class CollectMetadataTransmitter(OutputInterceptor):
507
+ def __init__(self, api_key: str = app_config._sailfish_api_key):
508
+ super().__init__(api_key)
509
+ self.operation_name = "CollectMetadata"
510
+
511
+ def send(
512
+ self,
513
+ user_id: str,
514
+ traits: Optional[Dict[str, Any]],
515
+ traits_json: Optional[str],
516
+ override: bool,
517
+ session_id: str,
518
+ ):
519
+ if traits is None and traits_json is None:
520
+ raise Exception(
521
+ 'Must pass in either traits or traits_json to "add_or_update_traits"'
522
+ )
523
+ query = f"""
524
+ {self.query_type} {self.operation_name}($apiKey: String!, $serviceUuid: String!, $sessionId: String!, $userId: String!, $traitsJson: String!, $excludedFields: [String!]!, $library: String!, $timestampMs: String!, $version: String!, $override: Boolean!) {{
525
+ {self.query_name}(apiKey: $apiKey, serviceUuid: $serviceUuid, sessionId: $sessionId, userId: $userId, traitsJson: $traitsJson, excludedFields: $excludedFields, library: $library, timestampMs: $timestampMs, version: $version, override: $override)
526
+ }}
527
+ """
528
+
529
+ excluded_fields = []
530
+ if traits_json is None:
531
+ traits_json, excluded_fields = serialize_json_with_exclusions(traits)
532
+
533
+ variables = self.get_variables(
534
+ {
535
+ "userId": user_id,
536
+ "traitsJson": traits_json,
537
+ "excludedFields": excluded_fields,
538
+ "override": override,
539
+ },
540
+ session_id,
541
+ )
542
+
543
+ non_blocking_post(self.endpoint, self.operation_name, query, variables)
Binary file
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+ import os, sys, socket, urllib.request, urllib.error
3
+
4
+ DEFAULT_TIMEOUT_S = 0.15
5
+
6
+ def _quick_http(url: str, headers: dict[str, str] | None = None, timeout: float = DEFAULT_TIMEOUT_S) -> tuple[int | None, str]:
7
+ req = urllib.request.Request(url, headers=headers or {}, method="GET")
8
+ try:
9
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
10
+ return resp.getcode(), "ok"
11
+ except urllib.error.HTTPError as e:
12
+ return e.code, "http_error"
13
+ except Exception as e:
14
+ return None, str(e)
15
+
16
+ def _is_cloud_instance() -> tuple[bool, str]:
17
+ try:
18
+ import urllib.request as _u
19
+ tok_req = _u.Request(
20
+ "http://169.254.169.254/latest/api/token",
21
+ headers={"X-aws-ec2-metadata-token-ttl-seconds": "60"},
22
+ method="PUT",
23
+ )
24
+ with _u.urlopen(tok_req, timeout=DEFAULT_TIMEOUT_S) as r:
25
+ if r.getcode() == 200:
26
+ return True, "aws-imdsv2"
27
+ except urllib.error.HTTPError as e:
28
+ if e.code in (401, 403, 404, 405):
29
+ return True, f"aws-imds({e.code})"
30
+ except Exception:
31
+ pass
32
+
33
+ code, _ = _quick_http("http://169.254.169.254/latest/meta-data/")
34
+ if code == 200:
35
+ return True, "aws-imdsv1"
36
+
37
+ code, _ = _quick_http(
38
+ "http://169.254.169.254/computeMetadata/v1/instance/id",
39
+ headers={"Metadata-Flavor": "Google"},
40
+ )
41
+ if code == 200:
42
+ return True, "gcp-metadata"
43
+
44
+ code, _ = _quick_http(
45
+ "http://169.254.169.254/metadata/instance?api-version=2021-02-01",
46
+ headers={"Metadata": "true"},
47
+ )
48
+ if code == 200:
49
+ return True, "azure-imds"
50
+
51
+ return False, "no-cloud-metadata"
52
+
53
+ def _resolves_host_docker_internal() -> bool:
54
+ try:
55
+ socket.gethostbyname("host.docker.internal")
56
+ return True
57
+ except Exception:
58
+ return False
59
+
60
+ # ---- globals to hold state ----
61
+ SF_IS_LOCAL_ENV: bool | None = None
62
+ SF_LOCAL_ENV_REASON: str | None = None
63
+
64
+
65
+ def _detect() -> tuple[bool, str]:
66
+ """Detect environment once. Raise nothing; always return a tuple."""
67
+ try:
68
+ if any(os.getenv(k) for k in (
69
+ "CI", "GITHUB_ACTIONS", "GITLAB_CI", "CIRCLECI",
70
+ "BUILDkite", "TEAMCITY_VERSION", "JENKINS_URL", "DRONE"
71
+ )):
72
+ return (False, "ci-env-detected")
73
+
74
+ on_cloud, cloud_reason = _is_cloud_instance()
75
+ if on_cloud:
76
+ return (False, cloud_reason)
77
+
78
+ if sys.platform in ("darwin", "win32"):
79
+ return (True, f"desktop-os:{sys.platform}")
80
+ try:
81
+ if "microsoft" in os.uname().release.lower() \
82
+ or "microsoft" in open("/proc/version", "rt", errors="ignore").read().lower():
83
+ return (True, "wsl-kernel")
84
+ except OSError:
85
+ pass
86
+
87
+ if _resolves_host_docker_internal():
88
+ return (True, "docker-desktop-dns")
89
+
90
+ return (True, "no-cloud-metadata-and-no-ci")
91
+
92
+ except Exception as e:
93
+ # fallback: treat as local if detection fails
94
+ return (True, f"detect-error:{type(e).__name__}")
95
+
96
+
97
+ def set_sf_is_local_flag() -> None:
98
+ """
99
+ Run detection once and store results in global variables.
100
+ Call this at app startup. Never raises.
101
+ """
102
+ global SF_IS_LOCAL_ENV, SF_LOCAL_ENV_REASON
103
+ try:
104
+ SF_IS_LOCAL_ENV, SF_LOCAL_ENV_REASON = _detect()
105
+ except Exception as e:
106
+ # absolute fallback, so setup never fails
107
+ SF_IS_LOCAL_ENV, SF_LOCAL_ENV_REASON = True, f"setup-error:{type(e).__name__}"
108
+
109
+
110
+ def sf_is_local_dev_environment() -> tuple[bool, str]:
111
+ """
112
+ Return cached values if sf_set_is_local_flag() has been called,
113
+ otherwise run detection on the fly. Never raises.
114
+ """
115
+ global SF_IS_LOCAL_ENV, SF_LOCAL_ENV_REASON
116
+ if SF_IS_LOCAL_ENV is None or SF_LOCAL_ENV_REASON is None:
117
+ set_sf_is_local_flag()
118
+ return SF_IS_LOCAL_ENV, SF_LOCAL_ENV_REASON
@@ -0,0 +1,6 @@
1
+ import importlib.metadata
2
+
3
+ PACKAGE_LIBRARY_TYPE = "PYTHON"
4
+ PACKAGE_NAME = "sf-veritas"
5
+
6
+ __version__ = importlib.metadata.version(PACKAGE_NAME)
File without changes