sf-veritas 0.9.7__py3-none-any.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.
- sf_veritas/.gitignore +2 -0
- sf_veritas/__init__.py +4 -0
- sf_veritas/app_config.py +49 -0
- sf_veritas/cli.py +336 -0
- sf_veritas/constants.py +3 -0
- sf_veritas/custom_excepthook.py +285 -0
- sf_veritas/custom_log_handler.py +53 -0
- sf_veritas/custom_output_wrapper.py +107 -0
- sf_veritas/custom_print.py +34 -0
- sf_veritas/django_app.py +5 -0
- sf_veritas/env_vars.py +83 -0
- sf_veritas/exception_handling_middleware.py +18 -0
- sf_veritas/exception_metaclass.py +69 -0
- sf_veritas/frame_tools.py +112 -0
- sf_veritas/import_hook.py +62 -0
- sf_veritas/infra_details/__init__.py +3 -0
- sf_veritas/infra_details/get_infra_details.py +24 -0
- sf_veritas/infra_details/kubernetes/__init__.py +3 -0
- sf_veritas/infra_details/kubernetes/get_cluster_name.py +147 -0
- sf_veritas/infra_details/kubernetes/get_details.py +7 -0
- sf_veritas/infra_details/running_on/__init__.py +17 -0
- sf_veritas/infra_details/running_on/kubernetes.py +11 -0
- sf_veritas/interceptors.py +252 -0
- sf_veritas/local_env_detect.py +118 -0
- sf_veritas/package_metadata.py +6 -0
- sf_veritas/patches/__init__.py +0 -0
- sf_veritas/patches/concurrent_futures.py +19 -0
- sf_veritas/patches/constants.py +1 -0
- sf_veritas/patches/exceptions.py +82 -0
- sf_veritas/patches/multiprocessing.py +32 -0
- sf_veritas/patches/network_libraries/__init__.py +51 -0
- sf_veritas/patches/network_libraries/aiohttp.py +100 -0
- sf_veritas/patches/network_libraries/curl_cffi.py +93 -0
- sf_veritas/patches/network_libraries/http_client.py +64 -0
- sf_veritas/patches/network_libraries/httpcore.py +152 -0
- sf_veritas/patches/network_libraries/httplib2.py +76 -0
- sf_veritas/patches/network_libraries/httpx.py +123 -0
- sf_veritas/patches/network_libraries/niquests.py +192 -0
- sf_veritas/patches/network_libraries/pycurl.py +71 -0
- sf_veritas/patches/network_libraries/requests.py +187 -0
- sf_veritas/patches/network_libraries/tornado.py +139 -0
- sf_veritas/patches/network_libraries/treq.py +122 -0
- sf_veritas/patches/network_libraries/urllib_request.py +129 -0
- sf_veritas/patches/network_libraries/utils.py +101 -0
- sf_veritas/patches/os.py +17 -0
- sf_veritas/patches/threading.py +32 -0
- sf_veritas/patches/web_frameworks/__init__.py +45 -0
- sf_veritas/patches/web_frameworks/aiohttp.py +133 -0
- sf_veritas/patches/web_frameworks/async_websocket_consumer.py +132 -0
- sf_veritas/patches/web_frameworks/blacksheep.py +107 -0
- sf_veritas/patches/web_frameworks/bottle.py +142 -0
- sf_veritas/patches/web_frameworks/cherrypy.py +246 -0
- sf_veritas/patches/web_frameworks/django.py +307 -0
- sf_veritas/patches/web_frameworks/eve.py +138 -0
- sf_veritas/patches/web_frameworks/falcon.py +229 -0
- sf_veritas/patches/web_frameworks/fastapi.py +145 -0
- sf_veritas/patches/web_frameworks/flask.py +186 -0
- sf_veritas/patches/web_frameworks/klein.py +40 -0
- sf_veritas/patches/web_frameworks/litestar.py +217 -0
- sf_veritas/patches/web_frameworks/pyramid.py +89 -0
- sf_veritas/patches/web_frameworks/quart.py +155 -0
- sf_veritas/patches/web_frameworks/robyn.py +114 -0
- sf_veritas/patches/web_frameworks/sanic.py +120 -0
- sf_veritas/patches/web_frameworks/starlette.py +144 -0
- sf_veritas/patches/web_frameworks/strawberry.py +269 -0
- sf_veritas/patches/web_frameworks/tornado.py +129 -0
- sf_veritas/patches/web_frameworks/utils.py +55 -0
- sf_veritas/print_override.py +13 -0
- sf_veritas/regular_data_transmitter.py +358 -0
- sf_veritas/request_interceptor.py +399 -0
- sf_veritas/request_utils.py +104 -0
- sf_veritas/server_status.py +1 -0
- sf_veritas/shutdown_flag.py +11 -0
- sf_veritas/subprocess_startup.py +3 -0
- sf_veritas/test_cli.py +145 -0
- sf_veritas/thread_local.py +436 -0
- sf_veritas/timeutil.py +114 -0
- sf_veritas/transmit_exception_to_sailfish.py +28 -0
- sf_veritas/transmitter.py +58 -0
- sf_veritas/types.py +44 -0
- sf_veritas/unified_interceptor.py +323 -0
- sf_veritas/utils.py +39 -0
- sf_veritas-0.9.7.dist-info/METADATA +83 -0
- sf_veritas-0.9.7.dist-info/RECORD +86 -0
- sf_veritas-0.9.7.dist-info/WHEEL +4 -0
- sf_veritas-0.9.7.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import builtins
|
|
2
|
+
import threading
|
|
3
|
+
import uuid
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from contextvars import ContextVar
|
|
6
|
+
from typing import Any, Dict, Optional, Set, Tuple, Union
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
from . import app_config
|
|
10
|
+
from .constants import NONSESSION_APPLOGS
|
|
11
|
+
from .env_vars import SF_DEBUG
|
|
12
|
+
|
|
13
|
+
# Define context variables
|
|
14
|
+
# You CANNOT switch this for another type - this is the ONLY version that works as of October 2024.
|
|
15
|
+
# Thread variables do not work, nor do globals.
|
|
16
|
+
# See https://elshad-karimov.medium.com/pythons-contextvars-the-most-powerful-feature-you-ve-never-heard-of-d636f4d34030
|
|
17
|
+
trace_id_ctx = ContextVar("trace_id", default=None)
|
|
18
|
+
handled_exceptions_ctx = ContextVar("handled_exceptions", default=set())
|
|
19
|
+
reentrancy_guard_logging_active_ctx = ContextVar(
|
|
20
|
+
"reentrancy_guard_logging_active", default=False
|
|
21
|
+
)
|
|
22
|
+
reentrancy_guard_logging_preactive_ctx = ContextVar(
|
|
23
|
+
"reentrancy_guard_logging_preactive", default=False
|
|
24
|
+
)
|
|
25
|
+
reentrancy_guard_print_active_ctx = ContextVar(
|
|
26
|
+
"reentrancy_guard_print_active", default=False
|
|
27
|
+
)
|
|
28
|
+
reentrancy_guard_print_preactive_ctx = ContextVar(
|
|
29
|
+
"reentrancy_guard_print_preactive", default=False
|
|
30
|
+
)
|
|
31
|
+
reentrancy_guard_exception_active_ctx = ContextVar(
|
|
32
|
+
"reentrancy_guard_exception_active", default=False
|
|
33
|
+
)
|
|
34
|
+
reentrancy_guard_exception_preactive_ctx = ContextVar(
|
|
35
|
+
"reentrancy_guard_exception_preactive", default=False
|
|
36
|
+
)
|
|
37
|
+
suppress_network_recording_ctx = ContextVar("suppress_network_recording", default=False)
|
|
38
|
+
reentrancy_guard_sys_stdout_active_ctx = ContextVar(
|
|
39
|
+
"reentrancy_guard_sys_stdout_active", default=False
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Thread-local storage as a fallback
|
|
43
|
+
_thread_locals = threading.local()
|
|
44
|
+
|
|
45
|
+
_shared_trace_registry = {}
|
|
46
|
+
_shared_trace_registry_lock = threading.RLock()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _set_shared_trace_id(trace_id: str) -> None:
|
|
50
|
+
with _shared_trace_registry_lock:
|
|
51
|
+
_shared_trace_registry["trace_id"] = trace_id
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_shared_trace_id() -> Optional[str]:
|
|
55
|
+
with _shared_trace_registry_lock:
|
|
56
|
+
return _shared_trace_registry.get("trace_id")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Generalized get, set, and get_or_set functions for all properties
|
|
60
|
+
def _get_context_or_thread_local(
|
|
61
|
+
ctx_var: ContextVar, attr_name: str, default: Any
|
|
62
|
+
) -> Any:
|
|
63
|
+
return ctx_var.get() or getattr(_thread_locals, attr_name, default)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _set_context_and_thread_local(
|
|
67
|
+
ctx_var: ContextVar, attr_name: str, value: Any
|
|
68
|
+
) -> Any:
|
|
69
|
+
ctx_var.set(value)
|
|
70
|
+
setattr(_thread_locals, attr_name, value)
|
|
71
|
+
return value
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def unset_sf_trace_id() -> None:
|
|
75
|
+
"""
|
|
76
|
+
Fully unsets the Sailfish trace ID from contextvars, thread-local storage,
|
|
77
|
+
and the shared trace registry.
|
|
78
|
+
"""
|
|
79
|
+
_set_shared_trace_id(None)
|
|
80
|
+
_set_context_and_thread_local(trace_id_ctx, "trace_id", None)
|
|
81
|
+
if SF_DEBUG:
|
|
82
|
+
print("[[DEBUG]] unset_sf_trace_id: trace_id cleared", log=False)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _get_or_set_context_and_thread_local(
|
|
86
|
+
ctx_var: ContextVar, attr_name: str, value_if_not_set
|
|
87
|
+
) -> Tuple[bool, Any]:
|
|
88
|
+
value = ctx_var.get() or getattr(_thread_locals, attr_name, None)
|
|
89
|
+
if value is None:
|
|
90
|
+
return _set_context_and_thread_local(ctx_var, attr_name, value_if_not_set)
|
|
91
|
+
return value
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Trace ID functions
|
|
95
|
+
def get_sf_trace_id() -> Optional[Union[str, UUID]]:
|
|
96
|
+
shared_trace_id = _get_shared_trace_id()
|
|
97
|
+
if shared_trace_id:
|
|
98
|
+
return shared_trace_id
|
|
99
|
+
return _get_context_or_thread_local(trace_id_ctx, "trace_id", None)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def set_sf_trace_id(trace_id: Union[str, UUID]) -> Union[str, UUID]:
|
|
103
|
+
_set_shared_trace_id(str(trace_id))
|
|
104
|
+
return _set_context_and_thread_local(trace_id_ctx, "trace_id", trace_id)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_or_set_sf_trace_id(
|
|
108
|
+
new_trace_id_if_not_set: Optional[str] = None,
|
|
109
|
+
is_associated_with_inbound_request: bool = False,
|
|
110
|
+
) -> Tuple[bool, Union[str, UUID]]:
|
|
111
|
+
if new_trace_id_if_not_set:
|
|
112
|
+
if SF_DEBUG:
|
|
113
|
+
print(
|
|
114
|
+
f"[trace_id] Setting new trace_id from argument: {new_trace_id_if_not_set}",
|
|
115
|
+
log=False,
|
|
116
|
+
)
|
|
117
|
+
set_sf_trace_id(new_trace_id_if_not_set)
|
|
118
|
+
return True, new_trace_id_if_not_set
|
|
119
|
+
|
|
120
|
+
trace_id = get_sf_trace_id()
|
|
121
|
+
if trace_id:
|
|
122
|
+
if SF_DEBUG:
|
|
123
|
+
print(f"[trace_id] Returning existing trace_id: {trace_id}", log=False)
|
|
124
|
+
return False, trace_id
|
|
125
|
+
|
|
126
|
+
if SF_DEBUG:
|
|
127
|
+
print("[trace_id] No trace_id found. Generating new trace_id.", log=False)
|
|
128
|
+
unique_id = uuid.uuid4()
|
|
129
|
+
trace_id = f"{NONSESSION_APPLOGS}-v3/{app_config._sailfish_api_key}/{unique_id}"
|
|
130
|
+
set_sf_trace_id(trace_id)
|
|
131
|
+
if SF_DEBUG:
|
|
132
|
+
print(f"[trace_id] Generated and set new trace_id: {trace_id}", log=False)
|
|
133
|
+
return True, trace_id
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# Handled exceptions functions
|
|
137
|
+
def get_handled_exceptions() -> Set[Any]:
|
|
138
|
+
return _get_context_or_thread_local(
|
|
139
|
+
handled_exceptions_ctx, "handled_exceptions", set()
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def set_handled_exceptions(exceptions_set: Set[Any]) -> Set[Any]:
|
|
144
|
+
return _set_context_and_thread_local(
|
|
145
|
+
handled_exceptions_ctx, "handled_exceptions", exceptions_set
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_or_set_handled_exceptions(default: set = None) -> Tuple[bool, Set[Any]]:
|
|
150
|
+
if default is None:
|
|
151
|
+
default = set()
|
|
152
|
+
return _get_or_set_context_and_thread_local(
|
|
153
|
+
handled_exceptions_ctx, "handled_exceptions", default
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def mark_exception_handled(exception) -> None:
|
|
158
|
+
"""
|
|
159
|
+
Marks an exception as handled to avoid duplicate processing.
|
|
160
|
+
"""
|
|
161
|
+
handled_exceptions = get_handled_exceptions()
|
|
162
|
+
handled_exceptions.add(id(exception))
|
|
163
|
+
set_handled_exceptions(handled_exceptions)
|
|
164
|
+
|
|
165
|
+
# Set the `_handled` attribute on the exception if it exists
|
|
166
|
+
if hasattr(exception, "_handled"):
|
|
167
|
+
setattr(exception, "_handled", True)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def has_handled_exception(exception) -> bool:
|
|
171
|
+
"""
|
|
172
|
+
Checks if an exception has been handled.
|
|
173
|
+
"""
|
|
174
|
+
# Check both thread-local context and the `_handled` attribute
|
|
175
|
+
return id(exception) in get_handled_exceptions() or getattr(
|
|
176
|
+
exception, "_handled", False
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def reset_handled_exceptions() -> Set[Any]:
|
|
181
|
+
return set_handled_exceptions(set())
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# Reentrancy guards functions (logging)
|
|
185
|
+
def get_reentrancy_guard_logging_active() -> bool:
|
|
186
|
+
return _get_context_or_thread_local(
|
|
187
|
+
reentrancy_guard_logging_active_ctx, "reentrancy_guard_logging_active", False
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def set_reentrancy_guard_logging_active(value: bool) -> bool:
|
|
192
|
+
return _set_context_and_thread_local(
|
|
193
|
+
reentrancy_guard_logging_active_ctx, "reentrancy_guard_logging_active", value
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def get_or_set_reentrancy_guard_logging_active(
|
|
198
|
+
value_if_not_set: bool,
|
|
199
|
+
) -> Tuple[bool, bool]:
|
|
200
|
+
return _get_or_set_context_and_thread_local(
|
|
201
|
+
reentrancy_guard_logging_active_ctx,
|
|
202
|
+
"reentrancy_guard_logging_active",
|
|
203
|
+
value_if_not_set,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def activate_reentrancy_guards_logging() -> bool:
|
|
208
|
+
set_reentrancy_guard_logging_active(True)
|
|
209
|
+
set_reentrancy_guard_logging_preactive(True)
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def get_reentrancy_guard_logging_preactive() -> bool:
|
|
214
|
+
return _get_context_or_thread_local(
|
|
215
|
+
reentrancy_guard_logging_preactive_ctx,
|
|
216
|
+
"reentrancy_guard_logging_preactive",
|
|
217
|
+
False,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def set_reentrancy_guard_logging_preactive(value: bool) -> bool:
|
|
222
|
+
return _set_context_and_thread_local(
|
|
223
|
+
reentrancy_guard_logging_preactive_ctx,
|
|
224
|
+
"reentrancy_guard_logging_preactive",
|
|
225
|
+
value,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def get_or_set_reentrancy_guard_logging_preactive(
|
|
230
|
+
value_if_not_set: bool,
|
|
231
|
+
) -> Tuple[bool, bool]:
|
|
232
|
+
return _get_or_set_context_and_thread_local(
|
|
233
|
+
reentrancy_guard_logging_preactive_ctx,
|
|
234
|
+
"reentrancy_guard_logging_preactive",
|
|
235
|
+
value_if_not_set,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def activate_reentrancy_guards_logging_preactive() -> bool:
|
|
240
|
+
return set_reentrancy_guard_logging_preactive(True)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# Reentrancy guards functions (stdout)
|
|
244
|
+
def get_reentrancy_guard_sys_stdout_active() -> bool:
|
|
245
|
+
return _get_context_or_thread_local(
|
|
246
|
+
reentrancy_guard_sys_stdout_active_ctx,
|
|
247
|
+
"reentrancy_guard_sys_stdout_active",
|
|
248
|
+
False,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def set_reentrancy_guard_sys_stdout_active(value: bool) -> bool:
|
|
253
|
+
return _set_context_and_thread_local(
|
|
254
|
+
reentrancy_guard_sys_stdout_active_ctx,
|
|
255
|
+
"reentrancy_guard_sys_stdout_active",
|
|
256
|
+
value,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def activate_reentrancy_guards_sys_stdout() -> bool:
|
|
261
|
+
set_reentrancy_guard_sys_stdout_active(True)
|
|
262
|
+
return True
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# Reentrancy guards functions (print)
|
|
266
|
+
def get_reentrancy_guard_print_active() -> bool:
|
|
267
|
+
return _get_context_or_thread_local(
|
|
268
|
+
reentrancy_guard_print_active_ctx, "reentrancy_guard_print_active", False
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def set_reentrancy_guard_print_active(value: bool) -> bool:
|
|
273
|
+
return _set_context_and_thread_local(
|
|
274
|
+
reentrancy_guard_print_active_ctx, "reentrancy_guard_print_active", value
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def get_or_set_reentrancy_guard_print_active(
|
|
279
|
+
value_if_not_set: bool,
|
|
280
|
+
) -> Tuple[bool, bool]:
|
|
281
|
+
return _get_or_set_context_and_thread_local(
|
|
282
|
+
reentrancy_guard_print_active_ctx,
|
|
283
|
+
"reentrancy_guard_print_active",
|
|
284
|
+
value_if_not_set,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def activate_reentrancy_guards_print() -> bool:
|
|
289
|
+
set_reentrancy_guard_print_active(True)
|
|
290
|
+
set_reentrancy_guard_print_preactive(True)
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def get_reentrancy_guard_print_preactive() -> bool:
|
|
295
|
+
return _get_context_or_thread_local(
|
|
296
|
+
reentrancy_guard_print_preactive_ctx, "reentrancy_guard_print_preactive", False
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def set_reentrancy_guard_print_preactive(value: bool) -> bool:
|
|
301
|
+
return _set_context_and_thread_local(
|
|
302
|
+
reentrancy_guard_print_preactive_ctx, "reentrancy_guard_print_preactive", value
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def get_or_set_reentrancy_guard_print_preactive(
|
|
307
|
+
value_if_not_set: bool,
|
|
308
|
+
) -> Tuple[bool, bool]:
|
|
309
|
+
return _get_or_set_context_and_thread_local(
|
|
310
|
+
reentrancy_guard_print_preactive_ctx,
|
|
311
|
+
"reentrancy_guard_print_preactive",
|
|
312
|
+
value_if_not_set,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def activate_reentrancy_guards_print_preactive() -> bool:
|
|
317
|
+
return set_reentrancy_guard_print_preactive(True)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# Reentrancy guards functions (exception)
|
|
321
|
+
def get_reentrancy_guard_exception_active() -> bool:
|
|
322
|
+
return _get_context_or_thread_local(
|
|
323
|
+
reentrancy_guard_exception_active_ctx,
|
|
324
|
+
"reentrancy_guard_exception_active",
|
|
325
|
+
False,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def set_reentrancy_guard_exception_active(value: bool) -> bool:
|
|
330
|
+
return _set_context_and_thread_local(
|
|
331
|
+
reentrancy_guard_exception_active_ctx,
|
|
332
|
+
"reentrancy_guard_exception_active",
|
|
333
|
+
value,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def get_or_set_reentrancy_guard_exception_active(
|
|
338
|
+
value_if_not_set: bool,
|
|
339
|
+
) -> Tuple[bool, bool]:
|
|
340
|
+
return _get_or_set_context_and_thread_local(
|
|
341
|
+
reentrancy_guard_exception_active_ctx,
|
|
342
|
+
"reentrancy_guard_exception_active",
|
|
343
|
+
value_if_not_set,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def activate_reentrancy_guards_exception() -> bool:
|
|
348
|
+
set_reentrancy_guard_exception_active(True)
|
|
349
|
+
set_reentrancy_guard_exception_preactive(True)
|
|
350
|
+
return True
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def get_reentrancy_guard_exception_preactive() -> bool:
|
|
354
|
+
return _get_context_or_thread_local(
|
|
355
|
+
reentrancy_guard_exception_preactive_ctx,
|
|
356
|
+
"reentrancy_guard_exception_preactive",
|
|
357
|
+
False,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def set_reentrancy_guard_exception_preactive(value: bool) -> bool:
|
|
362
|
+
return _set_context_and_thread_local(
|
|
363
|
+
reentrancy_guard_exception_preactive_ctx,
|
|
364
|
+
"reentrancy_guard_exception_preactive",
|
|
365
|
+
value,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def get_or_set_reentrancy_guard_exception_preactive(
|
|
370
|
+
value_if_not_set: bool,
|
|
371
|
+
) -> Tuple[bool, bool]:
|
|
372
|
+
return _get_or_set_context_and_thread_local(
|
|
373
|
+
reentrancy_guard_exception_preactive_ctx,
|
|
374
|
+
"reentrancy_guard_exception_preactive",
|
|
375
|
+
value_if_not_set,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def activate_reentrancy_guards_exception_preactive() -> bool:
|
|
380
|
+
return set_reentrancy_guard_exception_preactive(True)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# Get and set context
|
|
384
|
+
def get_context() -> Dict[str, Any]:
|
|
385
|
+
"""Get the current context values for all properties."""
|
|
386
|
+
return {
|
|
387
|
+
"trace_id": get_sf_trace_id(),
|
|
388
|
+
"handled_exceptions": get_handled_exceptions(),
|
|
389
|
+
"reentrancy_guard_logging_active": get_reentrancy_guard_logging_active(),
|
|
390
|
+
"reentrancy_guard_logging_preactive": get_reentrancy_guard_logging_preactive(),
|
|
391
|
+
"reentrancy_guard_print_active": get_reentrancy_guard_print_active(),
|
|
392
|
+
"reentrancy_guard_print_preactive": get_reentrancy_guard_print_preactive(),
|
|
393
|
+
"reentrancy_guard_exception_active": get_reentrancy_guard_exception_active(),
|
|
394
|
+
"reentrancy_guard_exception_preactive": get_reentrancy_guard_exception_preactive(),
|
|
395
|
+
"reentrancy_guard_sys_stdout_active": get_reentrancy_guard_sys_stdout_active(),
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def set_context(context) -> None:
|
|
400
|
+
"""Set the current context values for all properties."""
|
|
401
|
+
set_sf_trace_id(context.get("trace_id"))
|
|
402
|
+
set_handled_exceptions(context.get("handled_exceptions", set()))
|
|
403
|
+
set_reentrancy_guard_logging_active(
|
|
404
|
+
context.get("reentrancy_guard_logging_active", False)
|
|
405
|
+
)
|
|
406
|
+
set_reentrancy_guard_logging_preactive(
|
|
407
|
+
context.get("reentrancy_guard_logging_preactive", False)
|
|
408
|
+
)
|
|
409
|
+
set_reentrancy_guard_print_active(
|
|
410
|
+
context.get("reentrancy_guard_print_active", False)
|
|
411
|
+
)
|
|
412
|
+
set_reentrancy_guard_print_preactive(
|
|
413
|
+
context.get("reentrancy_guard_print_preactive", False)
|
|
414
|
+
)
|
|
415
|
+
set_reentrancy_guard_exception_active(
|
|
416
|
+
context.get("reentrancy_guard_exception_active", False)
|
|
417
|
+
)
|
|
418
|
+
set_reentrancy_guard_exception_preactive(
|
|
419
|
+
context.get("reentrancy_guard_exception_preactive", False)
|
|
420
|
+
)
|
|
421
|
+
set_reentrancy_guard_sys_stdout_active(
|
|
422
|
+
context.get("reentrancy_guard_sys_stdout_active", False)
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@contextmanager
|
|
427
|
+
def suppress_network_recording():
|
|
428
|
+
token = suppress_network_recording_ctx.set(True)
|
|
429
|
+
try:
|
|
430
|
+
yield
|
|
431
|
+
finally:
|
|
432
|
+
suppress_network_recording_ctx.reset(token)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def is_network_recording_suppressed() -> bool:
|
|
436
|
+
return suppress_network_recording_ctx.get()
|
sf_veritas/timeutil.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from datetime import datetime, timedelta, timezone
|
|
5
|
+
from time import sleep
|
|
6
|
+
from typing import Tuple
|
|
7
|
+
|
|
8
|
+
import ntplib
|
|
9
|
+
import requests
|
|
10
|
+
from dateutil import parser
|
|
11
|
+
from .env_vars import PRINT_CONFIGURATION_STATUSES
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_log_argument_supported():
|
|
15
|
+
try:
|
|
16
|
+
signature = inspect.signature(print)
|
|
17
|
+
return "log" in signature.parameters
|
|
18
|
+
except (TypeError, ValueError) as err:
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Determine if the `log` argument is supported
|
|
23
|
+
PRINT_ARGS = {"log": False} if is_log_argument_supported() else {}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TimeSync:
|
|
27
|
+
_instance = None
|
|
28
|
+
_lock = threading.Lock()
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def get_instance(cls):
|
|
32
|
+
if not cls._instance:
|
|
33
|
+
with cls._lock:
|
|
34
|
+
if not cls._instance:
|
|
35
|
+
cls._instance = cls() # Initialize singleton
|
|
36
|
+
return cls._instance
|
|
37
|
+
|
|
38
|
+
def __init__(self):
|
|
39
|
+
if getattr(self, "_initialized", False): # Avoid reinitialization
|
|
40
|
+
return
|
|
41
|
+
if PRINT_CONFIGURATION_STATUSES:
|
|
42
|
+
print("Configurating global time lock", **PRINT_ARGS)
|
|
43
|
+
self.sync_utc_datetime, self.local_time_ms = self._fetch_utc_time()
|
|
44
|
+
self._initialized = True
|
|
45
|
+
if PRINT_CONFIGURATION_STATUSES:
|
|
46
|
+
print("Configurating global time lock...DONE", **PRINT_ARGS)
|
|
47
|
+
|
|
48
|
+
def _fetch_utc_time(self) -> Tuple[datetime, int]:
|
|
49
|
+
try:
|
|
50
|
+
return self._fetch_with_retries(self._fetch_ntp_time)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
if PRINT_CONFIGURATION_STATUSES:
|
|
53
|
+
print(f"NTP sync failed: {e}. Falling back to HTTP APIs.", **PRINT_ARGS)
|
|
54
|
+
return self._fetch_with_retries(self._fetch_utc_time_from_apis)
|
|
55
|
+
|
|
56
|
+
def _fetch_with_retries(
|
|
57
|
+
self, fetch_func, retries=3, backoff_factor=1
|
|
58
|
+
) -> Tuple[datetime, int]:
|
|
59
|
+
for attempt in range(retries):
|
|
60
|
+
try:
|
|
61
|
+
return fetch_func()
|
|
62
|
+
except Exception as e:
|
|
63
|
+
if PRINT_CONFIGURATION_STATUSES:
|
|
64
|
+
print(f"Attempt {attempt + 1} failed: {e}", **PRINT_ARGS)
|
|
65
|
+
sleep(backoff_factor * (2**attempt))
|
|
66
|
+
raise Exception("All retries failed")
|
|
67
|
+
|
|
68
|
+
def _fetch_ntp_time(self) -> Tuple[datetime, int]:
|
|
69
|
+
client = ntplib.NTPClient()
|
|
70
|
+
response = client.request("pool.ntp.org", version=3)
|
|
71
|
+
utc_datetime = datetime.utcfromtimestamp(response.tx_time).replace(
|
|
72
|
+
tzinfo=timezone.utc
|
|
73
|
+
)
|
|
74
|
+
local_time_ms = int(time.time() * 1000)
|
|
75
|
+
if PRINT_CONFIGURATION_STATUSES:
|
|
76
|
+
print("Time successfully synced using NTP.", **PRINT_ARGS)
|
|
77
|
+
return utc_datetime, local_time_ms
|
|
78
|
+
|
|
79
|
+
def _fetch_utc_time_from_apis(self) -> Tuple[datetime, int]:
|
|
80
|
+
apis = [
|
|
81
|
+
"https://worldtimeapi.org/api/timezone/Etc/UTC",
|
|
82
|
+
"http://worldclockapi.com/api/json/utc/now",
|
|
83
|
+
]
|
|
84
|
+
for api in apis:
|
|
85
|
+
try:
|
|
86
|
+
response = requests.get(api, timeout=5)
|
|
87
|
+
response.raise_for_status()
|
|
88
|
+
data = response.json()
|
|
89
|
+
if "datetime" in data:
|
|
90
|
+
utc_datetime = parser.isoparse(data["datetime"])
|
|
91
|
+
elif "currentDateTime" in data:
|
|
92
|
+
utc_datetime = parser.isoparse(data["currentDateTime"])
|
|
93
|
+
else:
|
|
94
|
+
raise ValueError("Unexpected API response format")
|
|
95
|
+
local_time_ms = int(time.time() * 1000)
|
|
96
|
+
if PRINT_CONFIGURATION_STATUSES:
|
|
97
|
+
print(f"Time successfully synced using {api}.", **PRINT_ARGS)
|
|
98
|
+
return utc_datetime, local_time_ms
|
|
99
|
+
except Exception as e:
|
|
100
|
+
if PRINT_CONFIGURATION_STATUSES:
|
|
101
|
+
print(f"Failed to fetch time from {api}: {e}", **PRINT_ARGS)
|
|
102
|
+
if PRINT_CONFIGURATION_STATUSES:
|
|
103
|
+
print("All time sources failed. Falling back to system time.", **PRINT_ARGS)
|
|
104
|
+
utc_datetime = datetime.now(timezone.utc)
|
|
105
|
+
local_time_ms = int(time.time() * 1000)
|
|
106
|
+
return utc_datetime, local_time_ms
|
|
107
|
+
|
|
108
|
+
def get_utc_time_in_ms(self) -> int:
|
|
109
|
+
current_local_time_ms = int(time.time() * 1000)
|
|
110
|
+
elapsed_ms = current_local_time_ms - self.local_time_ms
|
|
111
|
+
current_utc_datetime = self.sync_utc_datetime + timedelta(
|
|
112
|
+
milliseconds=elapsed_ms
|
|
113
|
+
)
|
|
114
|
+
return int(current_utc_datetime.timestamp() * 1000)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from .custom_excepthook import transmit_exception
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def transmit_exception_to_sailfish(
|
|
5
|
+
exc: BaseException,
|
|
6
|
+
force_transmit: bool = False,
|
|
7
|
+
):
|
|
8
|
+
"""
|
|
9
|
+
Transmit an exception to Sailfish using the original traceback captured at the
|
|
10
|
+
point the exception was raised.
|
|
11
|
+
|
|
12
|
+
:param exc: The exception instance.
|
|
13
|
+
:param force_transmit: If True, will transmit even if the exception might
|
|
14
|
+
have already been handled or flagged.
|
|
15
|
+
"""
|
|
16
|
+
# Get the exception type and traceback from the exception itself
|
|
17
|
+
exc_type = type(exc)
|
|
18
|
+
exc_traceback = exc.__traceback__ # Automatically fetch the original traceback
|
|
19
|
+
|
|
20
|
+
# In some implementations, you might keep a `_handled` attribute to avoid double transmission.
|
|
21
|
+
if not force_transmit and getattr(exc, "_handled", False):
|
|
22
|
+
return # Already transmitted
|
|
23
|
+
|
|
24
|
+
# Actually send it over to Sailfish
|
|
25
|
+
transmit_exception(exc_type, exc, exc_traceback)
|
|
26
|
+
|
|
27
|
+
# Mark as handled to avoid re-transmission
|
|
28
|
+
setattr(exc, "_handled", True)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from .env_vars import SF_DEBUG
|
|
4
|
+
from .interceptors import CollectMetadataTransmitter
|
|
5
|
+
from .thread_local import get_or_set_sf_trace_id
|
|
6
|
+
|
|
7
|
+
collect_metadata_transmitter = CollectMetadataTransmitter()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SailfishTransmitter(object):
|
|
11
|
+
@classmethod
|
|
12
|
+
def identify(
|
|
13
|
+
cls,
|
|
14
|
+
user_id: str,
|
|
15
|
+
traits: Optional[Dict[str, Any]] = None,
|
|
16
|
+
traits_json: Optional[str] = None,
|
|
17
|
+
override: bool = False,
|
|
18
|
+
) -> None:
|
|
19
|
+
if traits is not None or traits_json is not None:
|
|
20
|
+
return cls.add_or_update_metadata(user_id, traits, traits_json, override)
|
|
21
|
+
return cls.add_or_update_metadata(user_id, dict(), override=override)
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def add_or_update_metadata(
|
|
25
|
+
cls,
|
|
26
|
+
user_id: str,
|
|
27
|
+
traits: Optional[Dict[str, Any]] = None,
|
|
28
|
+
traits_json: Optional[str] = None,
|
|
29
|
+
override: bool = False,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Sets traits and sends to the Sailfish AI backend
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
user_id: unique identifier for the user; common uses are username or email
|
|
36
|
+
traits: dictionary of contents to add or update in the user's traits. Defaults to None.
|
|
37
|
+
traits_json: json string of contents to add or update in the user's traits. Defaults to None.
|
|
38
|
+
"""
|
|
39
|
+
if traits is None and traits_json is None:
|
|
40
|
+
raise Exception(
|
|
41
|
+
'Must pass in either traits or traits_json to "add_or_update_traits"'
|
|
42
|
+
)
|
|
43
|
+
if SF_DEBUG:
|
|
44
|
+
print(
|
|
45
|
+
"[[DEBUG - add_or_update_traits]] starting thread [[/DEBUG]]", log=False
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
_, trace_id = get_or_set_sf_trace_id()
|
|
49
|
+
if SF_DEBUG:
|
|
50
|
+
print(
|
|
51
|
+
"add_or_update_metadata...SENDING DATA...args=",
|
|
52
|
+
(user_id, traits, traits_json, override, trace_id),
|
|
53
|
+
trace_id,
|
|
54
|
+
log=False,
|
|
55
|
+
)
|
|
56
|
+
collect_metadata_transmitter.do_send(
|
|
57
|
+
(user_id, traits, traits_json, override, trace_id), trace_id
|
|
58
|
+
)
|
sf_veritas/types.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class FrameInfo:
|
|
8
|
+
file: str
|
|
9
|
+
line: int
|
|
10
|
+
function: str
|
|
11
|
+
code: str
|
|
12
|
+
locals: Optional[Dict[str, Any]] = field(default_factory=dict)
|
|
13
|
+
offender: Optional[bool] = False
|
|
14
|
+
|
|
15
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
16
|
+
frame_info_dict = {
|
|
17
|
+
"file": self.file,
|
|
18
|
+
"line": self.line,
|
|
19
|
+
"function": self.function,
|
|
20
|
+
"code": self.code,
|
|
21
|
+
}
|
|
22
|
+
if self.locals:
|
|
23
|
+
frame_info_dict["locals"] = {k: str(v) for k, v in self.locals.items()}
|
|
24
|
+
if self.offender:
|
|
25
|
+
frame_info_dict["offender"] = self.offender
|
|
26
|
+
return frame_info_dict
|
|
27
|
+
|
|
28
|
+
def to_json(self) -> str:
|
|
29
|
+
return json.dumps(self.to_dict())
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_trace_from_json(data_json) -> List[FrameInfo]:
|
|
33
|
+
data = json.loads(data_json)
|
|
34
|
+
return [FrameInfo(**item) for item in data]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CustomJSONEncoderForFrameInfo(json.JSONEncoder):
|
|
38
|
+
def default(self, obj):
|
|
39
|
+
if isinstance(obj, FrameInfo):
|
|
40
|
+
return obj.to_dict()
|
|
41
|
+
try:
|
|
42
|
+
return super().default(obj)
|
|
43
|
+
except TypeError:
|
|
44
|
+
return str(obj)
|