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,304 @@
1
+ import inspect
2
+
3
+ # import linecache
4
+ import logging
5
+ import re
6
+ import sys
7
+ import threading
8
+ from types import FrameType, TracebackType
9
+ from typing import List, Optional
10
+
11
+ from . import app_config
12
+ from .env_vars import (
13
+ SAILFISH_EXCEPTION_FETCH_BEYOND_OFFENDER_DEPTH,
14
+ SAILFISH_EXCEPTION_FETCH_LOCALS_BEYOND_OFFENDER_DEPTH,
15
+ SF_DEBUG,
16
+ SF_DEBUG_TRACES,
17
+ )
18
+
19
+ # from .frame_tools import get_locals
20
+ from .interceptors import ExceptionInterceptor
21
+ from .local_env_detect import sf_is_local_dev_environment
22
+ from .thread_local import (
23
+ get_or_set_sf_trace_id,
24
+ has_handled_exception,
25
+ mark_exception_handled,
26
+ )
27
+ from .types import FrameInfo
28
+
29
+ REGEX_EVERYTHING_BEFORE_PYTHON_PACKAGE_NAME_WHEN_EXAMINING_FILENAME = (
30
+ r".*\/(site|dist)-packages\/"
31
+ )
32
+ REGEX_EVERYTHING_AFTER_NEXT_SLASH = r"\/.*"
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+ # Profiling data structure to collect function call information
37
+ call_hierarchy = []
38
+
39
+ # Lock to manage thread-safe access to profiling data
40
+ call_hierarchy_lock = threading.Lock()
41
+
42
+ _original_excepthook = sys.excepthook
43
+
44
+
45
+ def custom_thread_excepthook(args):
46
+ sys.excepthook(args.exc_type, args.exc_value, args.exc_traceback)
47
+
48
+
49
+ def process_traceback_only(
50
+ exc_value: Exception, exc_traceback: TracebackType, was_caught: bool = True
51
+ ):
52
+ """
53
+ Processes only the traceback information for an exception.
54
+
55
+ Args:
56
+ exc_value (Exception): The exception value.
57
+ exc_traceback (TracebackType): The traceback object for the exception.
58
+ """
59
+ trace = extract_trace(exc_traceback)
60
+ except_interceptor = ExceptionInterceptor()
61
+ _, trace_id = get_or_set_sf_trace_id()
62
+ if SF_DEBUG:
63
+ print(
64
+ "process_traceback_only...SENDING DATA...do_send args=",
65
+ (str(exc_value), trace, trace_id, was_caught),
66
+ trace_id,
67
+ log=False,
68
+ )
69
+ except_interceptor.do_send(
70
+ (str(exc_value), trace, trace_id, was_caught, sf_is_local_dev_environment()[0]),
71
+ trace_id,
72
+ )
73
+
74
+
75
+ # Usage example in the custom_excepthook function:
76
+ def custom_excepthook(
77
+ exc_type, exc_value, exc_traceback: TracebackType, raise_original: bool = True
78
+ ):
79
+ if has_handled_exception(exc_value):
80
+ return
81
+ mark_exception_handled(exc_value)
82
+
83
+ if SF_DEBUG:
84
+ print(f"custom_excepthook -> raise_original={raise_original}", log=False)
85
+
86
+ transmit_exception(exc_type, exc_value, exc_traceback)
87
+
88
+ if raise_original:
89
+ _original_excepthook(exc_type, exc_value, exc_traceback)
90
+
91
+
92
+ def transmit_exception(exc_type, exc_value, exc_traceback, was_caught: bool = True):
93
+ """
94
+ Handles transmitting exception information based on profiling mode.
95
+
96
+ Args:
97
+ exc_type: The type of the exception.
98
+ exc_value: The exception instance.
99
+ exc_traceback: The traceback object for the exception.
100
+ """
101
+ if app_config._profiling_mode_enabled:
102
+ threading.Thread(
103
+ target=process_profiling_data,
104
+ args=(exc_type, exc_value, exc_traceback, was_caught),
105
+ ).start()
106
+ return
107
+ process_traceback_only(exc_value, exc_traceback, was_caught)
108
+
109
+
110
+ def profiling_function(frame: FrameType, event: str, arg):
111
+ if not app_config._profiling_mode_enabled:
112
+ return
113
+
114
+ if event in ["call", "return"]:
115
+ code = frame.f_code
116
+ function_name = code.co_name
117
+ filename = code.co_filename
118
+ line_no = frame.f_lineno
119
+ class_name = None
120
+
121
+ # Try to determine if we're inside a method of a class
122
+ if "self" in frame.f_locals:
123
+ class_name = type(frame.f_locals["self"]).__name__
124
+
125
+ call_info = {
126
+ "event": event,
127
+ "function": function_name,
128
+ "class": class_name,
129
+ "file": filename,
130
+ "line_no": line_no,
131
+ }
132
+
133
+ # Add profiling data to call hierarchy with a lock for thread safety
134
+ with call_hierarchy_lock:
135
+ if len(call_hierarchy) < app_config._profiling_max_depth:
136
+ call_hierarchy.append(call_info)
137
+
138
+
139
+ def process_profiling_data(exc_type, exc_value, exc_traceback, was_caught: bool = True):
140
+ # Extract the traceback and process detailed information
141
+ trace = extract_trace(exc_traceback)
142
+
143
+ # Process profiling data
144
+ with call_hierarchy_lock:
145
+ if SF_DEBUG and SF_DEBUG_TRACES:
146
+ print("=" * 40, log=False)
147
+ print("Printing the complete call hierarchy after exception...", log=False)
148
+ print("=" * 40, log=False)
149
+
150
+ for entry in call_hierarchy:
151
+ if entry["event"] == "call":
152
+ class_name = f"{entry['class']}." if entry["class"] else ""
153
+ if SF_DEBUG and SF_DEBUG_TRACES:
154
+ print(
155
+ f"Called: {class_name}{entry['function']} (File: {entry['file']}, Line: {entry['line_no']})",
156
+ log=False,
157
+ )
158
+ elif entry["event"] == "return":
159
+ if SF_DEBUG and SF_DEBUG_TRACES:
160
+ print(f"Return from: {entry['function']}", log=False)
161
+
162
+ if SF_DEBUG and SF_DEBUG_TRACES:
163
+ print("-" * 40, log=False)
164
+
165
+ # Clear profiling data after processing
166
+ call_hierarchy.clear()
167
+
168
+ # Send the extracted traceback data to the exception interceptor
169
+ except_interceptor = ExceptionInterceptor()
170
+ _, trace_id = get_or_set_sf_trace_id()
171
+ if SF_DEBUG:
172
+ print(
173
+ "process_traceback_only...SENDING DATA...do_send args=",
174
+ (str(exc_value), trace, trace_id, was_caught),
175
+ trace_id,
176
+ log=False,
177
+ )
178
+ except_interceptor.do_send(
179
+ (str(exc_value), trace, trace_id, was_caught, sf_is_local_dev_environment()[0]),
180
+ trace_id,
181
+ )
182
+
183
+
184
+ def safe_repr(value):
185
+ try:
186
+ return repr(value)
187
+ except Exception:
188
+ return f"<unrepresentable: {type(value).__name__}>"
189
+
190
+
191
+ def extract_trace(tb: Optional[TracebackType] = None) -> List[FrameInfo]:
192
+ tb_list = []
193
+
194
+ # Collect all traceback frames in natural order (most recent last)
195
+ while tb is not None:
196
+ tb_list.append(tb)
197
+ tb = tb.tb_next
198
+
199
+ if not tb_list:
200
+ return []
201
+
202
+ frame_stack = []
203
+
204
+ # Iterate through the traceback, capturing frame information
205
+ for tb in tb_list:
206
+ frame = tb.tb_frame
207
+ frame_info = inspect.getframeinfo(frame)
208
+ filename = frame_info.filename
209
+ function_name = frame_info.function
210
+ line_no = frame_info.lineno
211
+ code_context = (
212
+ frame_info.code_context[0].strip()
213
+ if frame_info.code_context
214
+ else "No code context"
215
+ )
216
+
217
+ frame_info_obj = FrameInfo(
218
+ file=filename,
219
+ line=line_no,
220
+ function=function_name,
221
+ code=code_context,
222
+ )
223
+
224
+ # Capture ALL local variables at each frame - no depth or package restrictions
225
+ frame_info_obj.locals = {k: safe_repr(v) for k, v in frame.f_locals.items()}
226
+
227
+ frame_stack.append(frame_info_obj)
228
+
229
+ frame_stack[-1].offender = True
230
+ return frame_stack
231
+
232
+
233
+ def start_profiling():
234
+ """Start profiling to collect call hierarchy information if profiling is enabled."""
235
+ from .env_vars import SF_DEBUG
236
+
237
+ if SF_DEBUG:
238
+ try:
239
+ print(
240
+ f"[[DEBUG]] start_profiling() called, _profiling_mode_enabled={app_config._profiling_mode_enabled}",
241
+ log=False,
242
+ )
243
+ except:
244
+ print(
245
+ f"[[DEBUG]] start_profiling() called, _profiling_mode_enabled={app_config._profiling_mode_enabled}"
246
+ )
247
+ if app_config._profiling_mode_enabled:
248
+ try:
249
+ print(
250
+ f"[[DEBUG]] Setting sys.setprofile to profiling_function, current={sys.getprofile()}",
251
+ log=False,
252
+ )
253
+ except:
254
+ print(
255
+ f"[[DEBUG]] Setting sys.setprofile to profiling_function, current={sys.getprofile()}"
256
+ )
257
+ sys.setprofile(profiling_function)
258
+ try:
259
+ print(
260
+ f"[[DEBUG]] After setting, sys.getprofile()={sys.getprofile()}",
261
+ log=False,
262
+ )
263
+ except:
264
+ print(f"[[DEBUG]] After setting, sys.getprofile()={sys.getprofile()}")
265
+
266
+
267
+ def stop_profiling():
268
+ """Stop profiling to prevent collecting more call hierarchy data."""
269
+ sys.setprofile(None)
270
+
271
+
272
+ def should_collect_local_variables_for_stack_item(filename: str) -> bool:
273
+ if "__all__" in app_config._site_and_dist_packages_to_collect_local_variables_on:
274
+ return True
275
+ return is_allowed_package(filename)
276
+
277
+
278
+ def is_installed_package(filename: str) -> bool:
279
+ return bool(
280
+ re.search(
281
+ REGEX_EVERYTHING_BEFORE_PYTHON_PACKAGE_NAME_WHEN_EXAMINING_FILENAME,
282
+ filename,
283
+ )
284
+ )
285
+
286
+
287
+ def get_package_name_from_site_or_dist_package(filename: str):
288
+ site_dist_packages_removed = re.sub(
289
+ REGEX_EVERYTHING_BEFORE_PYTHON_PACKAGE_NAME_WHEN_EXAMINING_FILENAME,
290
+ "",
291
+ filename,
292
+ )
293
+ package_installed_name = re.sub(
294
+ REGEX_EVERYTHING_AFTER_NEXT_SLASH, "", site_dist_packages_removed
295
+ )
296
+ return package_installed_name
297
+
298
+
299
+ def is_allowed_package(filename: str) -> bool:
300
+ if not is_installed_package(filename):
301
+ return True
302
+
303
+ package = get_package_name_from_site_or_dist_package(filename)
304
+ return package in app_config._site_and_dist_packages_to_collect_local_variables_on
@@ -0,0 +1,146 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ from . import app_config
5
+ from .env_vars import PRINT_CONFIGURATION_STATUSES, SF_DEBUG
6
+ from .interceptors import LogInterceptor
7
+ from .thread_local import (
8
+ get_current_function_span_id,
9
+ get_or_set_sf_trace_id,
10
+ get_reentrancy_guard_logging_preactive,
11
+ )
12
+
13
+ # Try native fast path (compiled C extension). If missing, we fall back.
14
+ try:
15
+ from . import _sffastlog as _FASTMOD
16
+
17
+ if SF_DEBUG:
18
+ print("[[custom_log_handler]] _FASTMOD is okay")
19
+
20
+ _FAST_OK = True
21
+ except Exception:
22
+ if SF_DEBUG:
23
+ print("[[custom_log_handler]] _FASTMOD != OK")
24
+ _FASTMOD = None
25
+ _FAST_OK = False
26
+
27
+ # Small constant table avoids attribute access for levelname on hot path
28
+ _LEVEL_NAME = {
29
+ 50: "CRITICAL",
30
+ 40: "ERROR",
31
+ 30: "WARNING",
32
+ 20: "INFO",
33
+ 10: "DEBUG",
34
+ 0: "NOTSET",
35
+ }
36
+
37
+
38
+ class CustomLogHandler(logging.Handler, LogInterceptor):
39
+ """
40
+ Ultra-light log handler:
41
+ - Avoid Formatter unless explicitly set.
42
+ - Avoid %-formatting if record.args is empty.
43
+ - Prefer levelno→name map (micro faster than attribute lookup).
44
+ - Fetch/generate trace_id once per emit (unchanged).
45
+ - DIRECT native send via _sffastlog when available; Python fallback otherwise.
46
+ """
47
+
48
+ def __init__(self):
49
+ logging.Handler.__init__(self)
50
+ LogInterceptor.__init__(self, api_key=app_config._sailfish_api_key)
51
+ if PRINT_CONFIGURATION_STATUSES:
52
+ print("Intercepting log statements")
53
+
54
+ def emit(self, record: logging.LogRecord, trace_id: Optional[str] = None):
55
+ # Bind frequently used symbols to locals to reduce attribute/global lookups
56
+ _formatter = self.formatter
57
+ _get_tid = get_or_set_sf_trace_id
58
+ _get_preactive = get_reentrancy_guard_logging_preactive
59
+ _debug = SF_DEBUG
60
+ _level_table = _LEVEL_NAME
61
+ _fast = _FASTMOD
62
+ _fast_ok = _FAST_OK
63
+ _do_send = self.do_send # Python fallback
64
+
65
+ try:
66
+ # FAST PATH message extraction:
67
+ # - If a formatter is attached, defer to it (caller explicitly wants decorations)
68
+ # - Otherwise:
69
+ # - If there are args, we must perform %-format (getMessage)
70
+ # - If no args and msg is already a str, reuse it directly (no str() allocation)
71
+ # - Else fall back to str(msg)
72
+ if _formatter is None:
73
+ if record.args:
74
+ log_entry = record.getMessage()
75
+ else:
76
+ msg = record.msg
77
+ log_entry = msg if (msg.__class__ is str) else str(msg)
78
+ else:
79
+ log_entry = self.format(record)
80
+
81
+ # Cheap ignore check (lives on LogInterceptor); skip everything if ignored
82
+ # Keeps semantics identical while saving work on hot path
83
+ try:
84
+ if self.check_if_contents_should_be_ignored(log_entry):
85
+ return
86
+ except AttributeError:
87
+ # If not defined for some reason, proceed (older builds)
88
+ pass
89
+
90
+ # CHEAP LEVEL NAME:
91
+ lvlno = record.levelno
92
+ log_level = _level_table.get(lvlno)
93
+ if log_level is None: # uncommon/3rd-party custom levels
94
+ log_level = record.levelname
95
+
96
+ # TRACE ID ONCE (unchanged contract)
97
+ if trace_id is None:
98
+ _, trace_id = _get_tid(None)
99
+
100
+ # Debug printing (rarely enabled)
101
+ if _debug:
102
+ # Avoid f-strings to skip extra work when disabled
103
+ print(
104
+ "[[DEBUG custom_log_handler]]",
105
+ "trace_id=" + str(trace_id),
106
+ "[[" + log_level + "]]",
107
+ log_entry,
108
+ "[[/DEBUG]]",
109
+ log=False,
110
+ )
111
+
112
+ # ---------- Native fast path ----------
113
+ if _fast_ok and _fast is not None:
114
+ try:
115
+ preactive = bool(_get_preactive())
116
+ # Capture parent_span_id IMMEDIATELY for async-safety
117
+ parent_span_id = get_current_function_span_id()
118
+
119
+ # DEBUG: Log what parent_span_id we captured (only for TEST_PARENT_SPAN logs)
120
+ if "TEST_PARENT_SPAN" in log_entry:
121
+ print(f"[LOG_HANDLER_DEBUG] parent_span_id={parent_span_id} for log: {log_entry[:80]}", log=False)
122
+
123
+ # Single C call → copy → enqueue → return; HTTP happens on native thread
124
+ _fast.log(
125
+ level=log_level or "UNKNOWN",
126
+ contents=log_entry,
127
+ session_id=str(trace_id),
128
+ preactive=preactive,
129
+ parent_span_id=parent_span_id,
130
+ )
131
+ return
132
+ except Exception as _e:
133
+ print(
134
+ "[_sffastlog] fast-path failed; falling back:",
135
+ _e,
136
+ log=False,
137
+ )
138
+ # fall through to Python path
139
+
140
+ # ---------- Python fallback (deferred batching/async) ----------
141
+ _do_send((log_level, log_entry, trace_id), trace_id)
142
+
143
+ except Exception as e: # keep lean; avoid storms on storms
144
+ if _debug:
145
+ print("CustomLogHandler.emit error:", e)
146
+ self.handleError(record)
@@ -0,0 +1,153 @@
1
+ # sf_veritas/custom_output_wrapper.py
2
+ import os
3
+ import sys
4
+
5
+ from . import app_config
6
+ from .env_vars import PRINT_CONFIGURATION_STATUSES
7
+ from .interceptors import PrintInterceptor
8
+ from .thread_local import (
9
+ _thread_locals,
10
+ get_current_function_span_id,
11
+ get_or_set_sf_trace_id,
12
+ )
13
+
14
+ # ---- optional native fast path (C extension) ----
15
+ try:
16
+ from . import _sffastlog # compiled extension with ring + libcurl sender
17
+
18
+ _FAST_OK = True
19
+ except Exception: # pragma: no cover
20
+ _sffastlog = None
21
+ _FAST_OK = False
22
+
23
+ _FAST_PRINT_READY = False # one-time init guard
24
+
25
+
26
+ def _ensure_fast_print_initialized() -> bool:
27
+ """Idempotent, cheap check to init native print path once."""
28
+ global _FAST_PRINT_READY
29
+ if not _FAST_OK or _FAST_PRINT_READY:
30
+ return _FAST_PRINT_READY
31
+
32
+ endpoint = getattr(app_config, "_sailfish_graphql_endpoint", None)
33
+ api_key = getattr(app_config, "_sailfish_api_key", None)
34
+ service_uuid = getattr(app_config, "_service_uuid", None)
35
+ library = getattr(app_config, "library", "sailfish-python")
36
+ version = getattr(app_config, "version", "0.0.0")
37
+ http2 = 1 if os.getenv("SF_NBPOST_HTTP2", "0") == "1" else 0
38
+
39
+ if not (endpoint and api_key and service_uuid):
40
+ return False # not configured yet
41
+
42
+ # GraphQL mutation for print statements (must match server)
43
+ query = (
44
+ "mutation CollectPrintStatements("
45
+ "$apiKey: String!,"
46
+ "$serviceUuid: String!,"
47
+ "$sessionId: String!,"
48
+ "$contents: String!,"
49
+ "$reentrancyGuardPreactive: Boolean!,"
50
+ "$library: String!,"
51
+ "$timestampMs: String!,"
52
+ "$version: String!,"
53
+ "$parentSpanId: String"
54
+ "){collectPrintStatements("
55
+ "apiKey:$apiKey,serviceUuid:$serviceUuid,sessionId:$sessionId,"
56
+ "contents:$contents,reentrancyGuardPreactive:$reentrancyGuardPreactive,"
57
+ "library:$library,timestampMs:$timestampMs,version:$version,"
58
+ "parentSpanId:$parentSpanId)}"
59
+ )
60
+
61
+ try:
62
+ ok = _sffastlog.init_print(
63
+ url=endpoint,
64
+ query=query,
65
+ api_key=str(api_key),
66
+ service_uuid=str(service_uuid),
67
+ library=str(library),
68
+ version=str(version),
69
+ http2=http2,
70
+ )
71
+ _FAST_PRINT_READY = bool(ok)
72
+ except Exception:
73
+ _FAST_PRINT_READY = False
74
+
75
+ return _FAST_PRINT_READY
76
+
77
+
78
+ class CustomOutputWrapper:
79
+ """
80
+ Ultra-thin sys.stdout wrapper:
81
+ - Writes straight to the real stream to preserve console output.
82
+ - If native fast path is ready, send to C ring immediately.
83
+ - Otherwise, fall back to PrintInterceptor (Python path).
84
+ - No locks, no regex, no unconditional flush.
85
+ """
86
+
87
+ __slots__ = ("original", "print_interceptor")
88
+
89
+ def __init__(self, original):
90
+ self.original = original
91
+ self.print_interceptor = PrintInterceptor()
92
+
93
+ def write(self, msg):
94
+ # Respect your reentrancy guard quickly
95
+ if getattr(_thread_locals, "reentrancy_guard_logging_active", False):
96
+ self.original.write(msg)
97
+ return
98
+
99
+ # Always write to the real stream first (don’t flush unless caller asks)
100
+ self.original.write(msg)
101
+
102
+ # Cheap ignore cases (optional): skip totally empty or pure newline
103
+ if not msg or msg == "\n":
104
+ return
105
+
106
+ # Build once
107
+ message = msg
108
+ _, trace_id = get_or_set_sf_trace_id()
109
+
110
+ # Native fast path (C) — lowest overhead
111
+ if _ensure_fast_print_initialized():
112
+ try:
113
+ # Capture parent_span_id IMMEDIATELY for async-safety
114
+ parent_span_id = get_current_function_span_id()
115
+
116
+ _sffastlog.print_(
117
+ contents=message, session_id=str(trace_id), preactive=0, parent_span_id=parent_span_id
118
+ )
119
+ return
120
+ except Exception:
121
+ pass # fall through to Python fallback
122
+
123
+ # Python fallback path
124
+ self.print_interceptor.do_send((message, trace_id), trace_id)
125
+
126
+ def flush(self):
127
+ # Only flush when caller flushes (keeps latency down)
128
+ self.original.flush()
129
+
130
+ def __getattr__(self, attr):
131
+ return getattr(self.original, attr)
132
+
133
+
134
+ def setup_custom_output_wrappers():
135
+ # Import here to avoid circular dependency
136
+ from .print_override import override_print
137
+
138
+ if PRINT_CONFIGURATION_STATUSES:
139
+ sys.__stdout__.write("setup_custom_output_wrappers\n")
140
+
141
+ # First override print to support log=True/False parameter
142
+ override_print()
143
+
144
+ # Then wrap stdout for interception
145
+ sys.stdout = CustomOutputWrapper(sys.stdout)
146
+
147
+ if PRINT_CONFIGURATION_STATUSES:
148
+ sys.__stdout__.write("setup_custom_output_wrappers...DONE\n")
149
+
150
+
151
+ def get_custom_output_wrapper_django():
152
+ """Django-specific wrapper setup function called by Django patch."""
153
+ setup_custom_output_wrappers()