traceseed 0.1.0__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.
- traceseed/__init__.py +104 -0
- traceseed/api.py +473 -0
- traceseed/cli.py +171 -0
- traceseed/collectors/__init__.py +199 -0
- traceseed/config.py +162 -0
- traceseed/context.py +68 -0
- traceseed/engine.py +322 -0
- traceseed/errors.py +35 -0
- traceseed/fingerprint.py +131 -0
- traceseed/logging.py +24 -0
- traceseed/models.py +104 -0
- traceseed/py.typed +0 -0
- traceseed/redaction.py +235 -0
- traceseed/replay/__init__.py +3 -0
- traceseed/replay/runner.py +121 -0
- traceseed/serialization.py +263 -0
- traceseed/storage/__init__.py +6 -0
- traceseed/storage/archive.py +400 -0
- traceseed/storage/base.py +22 -0
- traceseed/storage/directory.py +80 -0
- traceseed/storage/memory.py +24 -0
- traceseed-0.1.0.dist-info/METADATA +350 -0
- traceseed-0.1.0.dist-info/RECORD +27 -0
- traceseed-0.1.0.dist-info/WHEEL +5 -0
- traceseed-0.1.0.dist-info/entry_points.txt +2 -0
- traceseed-0.1.0.dist-info/licenses/LICENSE +21 -0
- traceseed-0.1.0.dist-info/top_level.txt +1 -0
traceseed/__init__.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""TraceSeed — captura de falhas sem dependências externas."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__version__ = "0.1.0"
|
|
6
|
+
|
|
7
|
+
from .api import (
|
|
8
|
+
capture,
|
|
9
|
+
capture_exception,
|
|
10
|
+
get_last_capture,
|
|
11
|
+
guard,
|
|
12
|
+
install,
|
|
13
|
+
install_asyncio,
|
|
14
|
+
register_codec,
|
|
15
|
+
register_collector,
|
|
16
|
+
uninstall,
|
|
17
|
+
uninstall_asyncio,
|
|
18
|
+
unregister_codec,
|
|
19
|
+
unregister_collector,
|
|
20
|
+
)
|
|
21
|
+
from .config import TraceSeedConfig, configure, get_config, reset_config
|
|
22
|
+
from .context import (
|
|
23
|
+
breadcrumb,
|
|
24
|
+
clear_context,
|
|
25
|
+
context,
|
|
26
|
+
current_breadcrumbs,
|
|
27
|
+
current_context,
|
|
28
|
+
reset_context,
|
|
29
|
+
set_context,
|
|
30
|
+
)
|
|
31
|
+
from .errors import (
|
|
32
|
+
CallbackError,
|
|
33
|
+
ConfigurationError,
|
|
34
|
+
IntegrityError,
|
|
35
|
+
InvalidPackageError,
|
|
36
|
+
ReplayError,
|
|
37
|
+
SerializationError,
|
|
38
|
+
StorageError,
|
|
39
|
+
TraceSeedError,
|
|
40
|
+
)
|
|
41
|
+
from .models import (
|
|
42
|
+
Breadcrumb,
|
|
43
|
+
CallableInfo,
|
|
44
|
+
CaptureContext,
|
|
45
|
+
CaptureResult,
|
|
46
|
+
ExceptionInfo,
|
|
47
|
+
FailureRecord,
|
|
48
|
+
FrameInfo,
|
|
49
|
+
RuntimeInfo,
|
|
50
|
+
)
|
|
51
|
+
from .storage import ArchiveStorage, DirectoryStorage, MemoryStorage, StoredFailure
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"__version__",
|
|
55
|
+
# api
|
|
56
|
+
"capture",
|
|
57
|
+
"capture_exception",
|
|
58
|
+
"get_last_capture",
|
|
59
|
+
"guard",
|
|
60
|
+
"install",
|
|
61
|
+
"install_asyncio",
|
|
62
|
+
"register_codec",
|
|
63
|
+
"register_collector",
|
|
64
|
+
"uninstall",
|
|
65
|
+
"uninstall_asyncio",
|
|
66
|
+
"unregister_codec",
|
|
67
|
+
"unregister_collector",
|
|
68
|
+
# errors
|
|
69
|
+
"TraceSeedError",
|
|
70
|
+
"CallbackError",
|
|
71
|
+
"ConfigurationError",
|
|
72
|
+
"IntegrityError",
|
|
73
|
+
"InvalidPackageError",
|
|
74
|
+
"ReplayError",
|
|
75
|
+
"SerializationError",
|
|
76
|
+
"StorageError",
|
|
77
|
+
# config
|
|
78
|
+
"TraceSeedConfig",
|
|
79
|
+
"configure",
|
|
80
|
+
"get_config",
|
|
81
|
+
"reset_config",
|
|
82
|
+
# context
|
|
83
|
+
"breadcrumb",
|
|
84
|
+
"clear_context",
|
|
85
|
+
"context",
|
|
86
|
+
"current_breadcrumbs",
|
|
87
|
+
"current_context",
|
|
88
|
+
"reset_context",
|
|
89
|
+
"set_context",
|
|
90
|
+
# models
|
|
91
|
+
"Breadcrumb",
|
|
92
|
+
"CallableInfo",
|
|
93
|
+
"CaptureContext",
|
|
94
|
+
"CaptureResult",
|
|
95
|
+
"ExceptionInfo",
|
|
96
|
+
"FailureRecord",
|
|
97
|
+
"FrameInfo",
|
|
98
|
+
"RuntimeInfo",
|
|
99
|
+
# storage
|
|
100
|
+
"ArchiveStorage",
|
|
101
|
+
"DirectoryStorage",
|
|
102
|
+
"MemoryStorage",
|
|
103
|
+
"StoredFailure",
|
|
104
|
+
]
|
traceseed/api.py
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"""API pública: decoradores, context managers, hooks globais e registro.
|
|
2
|
+
|
|
3
|
+
Semântica de strict:
|
|
4
|
+
strict=False (padrão): falhas internas de captura não substituem a exceção
|
|
5
|
+
original; o erro vai para stderr; re_raise continua funcionando normalmente.
|
|
6
|
+
strict=True: falhas internas levantam StorageError (com exceção original como
|
|
7
|
+
__cause__); falha de callback levanta CallbackError; o comportamento é igual
|
|
8
|
+
nos quatro contextos de captura.
|
|
9
|
+
|
|
10
|
+
asyncio hooks:
|
|
11
|
+
install_asyncio() é idempotente por loop: instalar duas vezes não empilha
|
|
12
|
+
wrappers. Usa WeakKeyDictionary para evitar retenção de loops e colisão
|
|
13
|
+
de id() após garbage collection.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import functools
|
|
20
|
+
import inspect
|
|
21
|
+
import sys
|
|
22
|
+
import threading
|
|
23
|
+
import weakref
|
|
24
|
+
from collections.abc import Callable, Generator
|
|
25
|
+
from contextlib import contextmanager, suppress
|
|
26
|
+
from inspect import BoundArguments
|
|
27
|
+
from typing import Any, TypeVar
|
|
28
|
+
|
|
29
|
+
from .collectors import CollectorRegistry
|
|
30
|
+
from .config import TraceSeedConfig, get_config
|
|
31
|
+
from .engine import CaptureEngine
|
|
32
|
+
from .errors import CallbackError, StorageError
|
|
33
|
+
from .models import CallableInfo, CaptureContext, CaptureResult
|
|
34
|
+
from .serialization import SafeSerializer
|
|
35
|
+
from .storage.archive import ArchiveStorage
|
|
36
|
+
|
|
37
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
38
|
+
|
|
39
|
+
_global_registry: CollectorRegistry = CollectorRegistry()
|
|
40
|
+
_global_codecs: dict[str, Any] = {}
|
|
41
|
+
_installed: bool = False
|
|
42
|
+
_old_excepthook: Any = None
|
|
43
|
+
_old_thread_excepthook: Any = None
|
|
44
|
+
_last_capture: CaptureResult | None = None
|
|
45
|
+
|
|
46
|
+
# Asyncio: WeakKeyDictionary usa a referência real do loop como chave.
|
|
47
|
+
# Quando o loop é coletado pelo GC, a entrada some automaticamente.
|
|
48
|
+
# Isso evita colisões de id() entre loops destruídos e novos.
|
|
49
|
+
_asyncio_handlers: weakref.WeakKeyDictionary[asyncio.AbstractEventLoop, Any] = (
|
|
50
|
+
weakref.WeakKeyDictionary()
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
_SKIP_TYPES = (KeyboardInterrupt, SystemExit)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _make_serializer(config: TraceSeedConfig) -> SafeSerializer:
|
|
57
|
+
ser = SafeSerializer(config)
|
|
58
|
+
for codec in _global_codecs.values():
|
|
59
|
+
ser.register_codec(codec)
|
|
60
|
+
return ser
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _default_storage(config: TraceSeedConfig, serializer: SafeSerializer) -> ArchiveStorage:
|
|
64
|
+
return ArchiveStorage(config, serializer)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _is_importable(func: Callable[..., Any]) -> bool:
|
|
68
|
+
module = getattr(func, "__module__", None)
|
|
69
|
+
qualname = getattr(func, "__qualname__", "")
|
|
70
|
+
if module in (None, "__main__"):
|
|
71
|
+
return False
|
|
72
|
+
return "<locals>" not in qualname
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _bind_arguments(func: Callable[..., Any], args: tuple, kwargs: dict) -> dict[str, Any]:
|
|
76
|
+
try:
|
|
77
|
+
sig = inspect.signature(func)
|
|
78
|
+
bound: BoundArguments = sig.bind(*args, **kwargs)
|
|
79
|
+
bound.apply_defaults()
|
|
80
|
+
return dict(bound.arguments)
|
|
81
|
+
except (TypeError, ValueError):
|
|
82
|
+
return {}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _raise_strict(storage_error_msg: str, original: BaseException) -> None:
|
|
86
|
+
"""Levanta StorageError preservando a exceção original como __cause__."""
|
|
87
|
+
raise StorageError(storage_error_msg) from original
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _invoke_callback(
|
|
91
|
+
on_captured: Callable[[CaptureResult], Any],
|
|
92
|
+
result: CaptureResult,
|
|
93
|
+
strict: bool,
|
|
94
|
+
original: BaseException,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Invoca on_captured respeitando strict."""
|
|
97
|
+
if strict:
|
|
98
|
+
try:
|
|
99
|
+
on_captured(result)
|
|
100
|
+
except Exception as cb_err:
|
|
101
|
+
raise CallbackError(str(cb_err)) from original
|
|
102
|
+
else:
|
|
103
|
+
with suppress(Exception):
|
|
104
|
+
on_captured(result)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
# capture_exception
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def capture_exception(
|
|
113
|
+
exception: BaseException,
|
|
114
|
+
*,
|
|
115
|
+
config: TraceSeedConfig | None = None,
|
|
116
|
+
storage: Any = None,
|
|
117
|
+
metadata: dict[str, Any] | None = None,
|
|
118
|
+
operation: str | None = None,
|
|
119
|
+
callable_info: CallableInfo | None = None,
|
|
120
|
+
replay_arguments: tuple | None = None,
|
|
121
|
+
replay_keyword_arguments: dict | None = None,
|
|
122
|
+
strict: bool = False,
|
|
123
|
+
on_captured: Callable[[CaptureResult], Any] | None = None,
|
|
124
|
+
) -> CaptureResult | None:
|
|
125
|
+
if not isinstance(exception, BaseException):
|
|
126
|
+
raise TypeError(f"esperado BaseException, recebeu {type(exception).__name__}")
|
|
127
|
+
|
|
128
|
+
cfg = config or get_config()
|
|
129
|
+
ser = _make_serializer(cfg)
|
|
130
|
+
stor = storage if storage is not None else _default_storage(cfg, ser)
|
|
131
|
+
|
|
132
|
+
ctx = CaptureContext(
|
|
133
|
+
operation=operation,
|
|
134
|
+
metadata=metadata or {},
|
|
135
|
+
arguments={},
|
|
136
|
+
callable_info=callable_info,
|
|
137
|
+
replay_arguments=replay_arguments,
|
|
138
|
+
replay_keyword_arguments=replay_keyword_arguments,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
engine = CaptureEngine(
|
|
142
|
+
config=cfg,
|
|
143
|
+
collectors=_global_registry,
|
|
144
|
+
storage=stor,
|
|
145
|
+
serializer=ser,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
result = engine.capture(exception, ctx)
|
|
149
|
+
|
|
150
|
+
if result.capture_error:
|
|
151
|
+
if strict:
|
|
152
|
+
raise StorageError(result.capture_error) from exception
|
|
153
|
+
print(f"traceseed: {result.capture_error}", file=sys.stderr)
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
global _last_capture
|
|
157
|
+
_last_capture = result
|
|
158
|
+
|
|
159
|
+
if on_captured is not None:
|
|
160
|
+
_invoke_callback(on_captured, result, strict, exception)
|
|
161
|
+
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
# Decorator @capture
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def capture(
|
|
171
|
+
func: F | None = None,
|
|
172
|
+
*,
|
|
173
|
+
storage: Any = None,
|
|
174
|
+
config: TraceSeedConfig | None = None,
|
|
175
|
+
replayable: bool = False,
|
|
176
|
+
operation: str | None = None,
|
|
177
|
+
on_captured: Callable[[CaptureResult], Any] | None = None,
|
|
178
|
+
strict: bool = False,
|
|
179
|
+
) -> Any:
|
|
180
|
+
def decorator(fn: F) -> F:
|
|
181
|
+
if asyncio.iscoroutinefunction(fn):
|
|
182
|
+
|
|
183
|
+
@functools.wraps(fn)
|
|
184
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
185
|
+
cfg = config or get_config()
|
|
186
|
+
try:
|
|
187
|
+
return await fn(*args, **kwargs)
|
|
188
|
+
except _SKIP_TYPES:
|
|
189
|
+
raise
|
|
190
|
+
except BaseException as exc:
|
|
191
|
+
_capture_in_wrapper(
|
|
192
|
+
fn,
|
|
193
|
+
args,
|
|
194
|
+
kwargs,
|
|
195
|
+
exc,
|
|
196
|
+
cfg,
|
|
197
|
+
replayable,
|
|
198
|
+
operation,
|
|
199
|
+
storage,
|
|
200
|
+
on_captured,
|
|
201
|
+
strict,
|
|
202
|
+
)
|
|
203
|
+
if cfg.re_raise:
|
|
204
|
+
raise
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
return async_wrapper # type: ignore
|
|
208
|
+
else:
|
|
209
|
+
|
|
210
|
+
@functools.wraps(fn)
|
|
211
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
212
|
+
cfg = config or get_config()
|
|
213
|
+
try:
|
|
214
|
+
return fn(*args, **kwargs)
|
|
215
|
+
except _SKIP_TYPES:
|
|
216
|
+
raise
|
|
217
|
+
except BaseException as exc:
|
|
218
|
+
_capture_in_wrapper(
|
|
219
|
+
fn,
|
|
220
|
+
args,
|
|
221
|
+
kwargs,
|
|
222
|
+
exc,
|
|
223
|
+
cfg,
|
|
224
|
+
replayable,
|
|
225
|
+
operation,
|
|
226
|
+
storage,
|
|
227
|
+
on_captured,
|
|
228
|
+
strict,
|
|
229
|
+
)
|
|
230
|
+
if cfg.re_raise:
|
|
231
|
+
raise
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
return sync_wrapper # type: ignore
|
|
235
|
+
|
|
236
|
+
if func is not None:
|
|
237
|
+
return decorator(func)
|
|
238
|
+
return decorator
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _capture_in_wrapper(
|
|
242
|
+
fn: Callable[..., Any],
|
|
243
|
+
args: tuple,
|
|
244
|
+
kwargs: dict,
|
|
245
|
+
exc: BaseException,
|
|
246
|
+
cfg: TraceSeedConfig,
|
|
247
|
+
replayable: bool,
|
|
248
|
+
operation: str | None,
|
|
249
|
+
storage: Any,
|
|
250
|
+
on_captured: Callable | None,
|
|
251
|
+
strict: bool,
|
|
252
|
+
) -> None:
|
|
253
|
+
"""Lógica comum de captura para decoradores sync e async."""
|
|
254
|
+
arguments = _bind_arguments(fn, args, kwargs)
|
|
255
|
+
importable = _is_importable(fn) and replayable
|
|
256
|
+
ci = CallableInfo(
|
|
257
|
+
module=getattr(fn, "__module__", ""),
|
|
258
|
+
qualname=getattr(fn, "__qualname__", ""),
|
|
259
|
+
replayable=importable,
|
|
260
|
+
reason=None if importable else ("callable não importável" if replayable else None),
|
|
261
|
+
)
|
|
262
|
+
op = operation or getattr(fn, "__qualname__", None)
|
|
263
|
+
ser = _make_serializer(cfg)
|
|
264
|
+
stor = storage if storage is not None else _default_storage(cfg, ser)
|
|
265
|
+
ctx = CaptureContext(
|
|
266
|
+
operation=op,
|
|
267
|
+
metadata={},
|
|
268
|
+
arguments=arguments,
|
|
269
|
+
callable_info=ci,
|
|
270
|
+
replay_arguments=args if importable else None,
|
|
271
|
+
replay_keyword_arguments=kwargs if importable else None,
|
|
272
|
+
)
|
|
273
|
+
engine = CaptureEngine(cfg, _global_registry, stor, ser)
|
|
274
|
+
result = engine.capture(exc, ctx)
|
|
275
|
+
|
|
276
|
+
if result.capture_error:
|
|
277
|
+
if strict:
|
|
278
|
+
raise StorageError(result.capture_error) from exc
|
|
279
|
+
print(f"traceseed: {result.capture_error}", file=sys.stderr)
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
global _last_capture
|
|
283
|
+
_last_capture = result
|
|
284
|
+
if on_captured:
|
|
285
|
+
_invoke_callback(on_captured, result, strict, exc)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# ---------------------------------------------------------------------------
|
|
289
|
+
# guard context manager
|
|
290
|
+
# ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@contextmanager
|
|
294
|
+
def guard(
|
|
295
|
+
operation: str,
|
|
296
|
+
*,
|
|
297
|
+
storage: Any = None,
|
|
298
|
+
config: TraceSeedConfig | None = None,
|
|
299
|
+
on_captured: Callable[[CaptureResult], Any] | None = None,
|
|
300
|
+
strict: bool = False,
|
|
301
|
+
) -> Generator[None, None, None]:
|
|
302
|
+
cfg = config or get_config()
|
|
303
|
+
try:
|
|
304
|
+
yield
|
|
305
|
+
except _SKIP_TYPES:
|
|
306
|
+
raise
|
|
307
|
+
except BaseException as exc:
|
|
308
|
+
ser = _make_serializer(cfg)
|
|
309
|
+
stor = storage if storage is not None else _default_storage(cfg, ser)
|
|
310
|
+
ctx = CaptureContext(operation=operation, metadata={}, arguments={})
|
|
311
|
+
engine = CaptureEngine(cfg, _global_registry, stor, ser)
|
|
312
|
+
result = engine.capture(exc, ctx)
|
|
313
|
+
|
|
314
|
+
if result.capture_error:
|
|
315
|
+
if strict:
|
|
316
|
+
raise StorageError(result.capture_error) from exc
|
|
317
|
+
print(f"traceseed: {result.capture_error}", file=sys.stderr)
|
|
318
|
+
else:
|
|
319
|
+
global _last_capture
|
|
320
|
+
_last_capture = result
|
|
321
|
+
if on_captured:
|
|
322
|
+
_invoke_callback(on_captured, result, strict, exc)
|
|
323
|
+
|
|
324
|
+
if cfg.re_raise:
|
|
325
|
+
raise
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
# ---------------------------------------------------------------------------
|
|
329
|
+
# Hooks globais
|
|
330
|
+
# ---------------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def install(storage: Any = None, config: TraceSeedConfig | None = None) -> None:
|
|
334
|
+
"""Instala hooks em sys.excepthook e threading.excepthook.
|
|
335
|
+
|
|
336
|
+
É idempotente: chamadas repetidas não instalam hooks duplos.
|
|
337
|
+
O hook anterior (incluindo handlers customizados) é sempre chamado.
|
|
338
|
+
"""
|
|
339
|
+
global _installed, _old_excepthook, _old_thread_excepthook
|
|
340
|
+
|
|
341
|
+
if _installed:
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
cfg = config
|
|
345
|
+
stor = storage
|
|
346
|
+
# Captura os handlers atuais antes de sobrescrever
|
|
347
|
+
prev_sys = sys.excepthook
|
|
348
|
+
prev_thread = threading.excepthook
|
|
349
|
+
|
|
350
|
+
def _sys_excepthook(exc_type: type, exc_value: BaseException, exc_tb: Any) -> None:
|
|
351
|
+
with suppress(Exception):
|
|
352
|
+
capture_exception(exc_value, config=cfg, storage=stor)
|
|
353
|
+
# Chama o handler que estava instalado antes do TraceSeed
|
|
354
|
+
try:
|
|
355
|
+
prev_sys(exc_type, exc_value, exc_tb)
|
|
356
|
+
except Exception:
|
|
357
|
+
sys.__excepthook__(exc_type, exc_value, exc_tb)
|
|
358
|
+
|
|
359
|
+
def _thread_excepthook(hook_args: threading.ExceptHookArgs) -> None:
|
|
360
|
+
if hook_args.exc_value is not None:
|
|
361
|
+
with suppress(Exception):
|
|
362
|
+
capture_exception(hook_args.exc_value, config=cfg, storage=stor)
|
|
363
|
+
# Chama o handler anterior
|
|
364
|
+
with suppress(Exception):
|
|
365
|
+
prev_thread(hook_args)
|
|
366
|
+
|
|
367
|
+
_old_excepthook = prev_sys
|
|
368
|
+
_old_thread_excepthook = prev_thread
|
|
369
|
+
sys.excepthook = _sys_excepthook
|
|
370
|
+
threading.excepthook = _thread_excepthook
|
|
371
|
+
_installed = True
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def uninstall() -> None:
|
|
375
|
+
"""Restaura exatamente os hooks que estavam instalados antes de install()."""
|
|
376
|
+
global _installed, _old_excepthook, _old_thread_excepthook
|
|
377
|
+
|
|
378
|
+
if not _installed:
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
if _old_excepthook is not None:
|
|
382
|
+
sys.excepthook = _old_excepthook
|
|
383
|
+
if _old_thread_excepthook is not None:
|
|
384
|
+
threading.excepthook = _old_thread_excepthook
|
|
385
|
+
|
|
386
|
+
_old_excepthook = None
|
|
387
|
+
_old_thread_excepthook = None
|
|
388
|
+
_installed = False
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def install_asyncio(
|
|
392
|
+
loop: asyncio.AbstractEventLoop | None = None,
|
|
393
|
+
storage: Any = None,
|
|
394
|
+
config: TraceSeedConfig | None = None,
|
|
395
|
+
) -> asyncio.AbstractEventLoop:
|
|
396
|
+
"""Instala handler de exceções no loop asyncio, preservando o handler anterior.
|
|
397
|
+
|
|
398
|
+
É idempotente por loop: instalar duas vezes não empilha wrappers.
|
|
399
|
+
Usa WeakKeyDictionary para evitar retenção do loop e colisão de id().
|
|
400
|
+
|
|
401
|
+
Retorna o loop configurado (útil para chamar uninstall_asyncio depois).
|
|
402
|
+
"""
|
|
403
|
+
cfg = config
|
|
404
|
+
stor = storage
|
|
405
|
+
|
|
406
|
+
try:
|
|
407
|
+
lp = loop or asyncio.get_event_loop()
|
|
408
|
+
except RuntimeError:
|
|
409
|
+
lp = asyncio.new_event_loop()
|
|
410
|
+
|
|
411
|
+
# Idempotência: se já instalado neste loop, retorna sem re-instalar
|
|
412
|
+
if lp in _asyncio_handlers:
|
|
413
|
+
return lp
|
|
414
|
+
|
|
415
|
+
old_handler = lp.get_exception_handler()
|
|
416
|
+
_asyncio_handlers[lp] = old_handler
|
|
417
|
+
|
|
418
|
+
def _asyncio_handler(lp2: asyncio.AbstractEventLoop, context: dict[str, Any]) -> None:
|
|
419
|
+
exc = context.get("exception")
|
|
420
|
+
if exc is not None:
|
|
421
|
+
with suppress(Exception):
|
|
422
|
+
capture_exception(exc, config=cfg, storage=stor)
|
|
423
|
+
if old_handler is not None:
|
|
424
|
+
try:
|
|
425
|
+
old_handler(lp2, context)
|
|
426
|
+
except Exception:
|
|
427
|
+
lp2.default_exception_handler(context)
|
|
428
|
+
else:
|
|
429
|
+
lp2.default_exception_handler(context)
|
|
430
|
+
|
|
431
|
+
lp.set_exception_handler(_asyncio_handler)
|
|
432
|
+
return lp
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def uninstall_asyncio(loop: asyncio.AbstractEventLoop | None = None) -> None:
|
|
436
|
+
"""Restaura o handler asyncio anterior ao install_asyncio().
|
|
437
|
+
|
|
438
|
+
É seguro chamar mais de uma vez (idempotente).
|
|
439
|
+
"""
|
|
440
|
+
try:
|
|
441
|
+
lp = loop or asyncio.get_event_loop()
|
|
442
|
+
except RuntimeError:
|
|
443
|
+
return
|
|
444
|
+
|
|
445
|
+
if lp not in _asyncio_handlers:
|
|
446
|
+
return
|
|
447
|
+
|
|
448
|
+
lp.set_exception_handler(_asyncio_handlers.pop(lp))
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
# ---------------------------------------------------------------------------
|
|
452
|
+
# Registro global
|
|
453
|
+
# ---------------------------------------------------------------------------
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def get_last_capture() -> CaptureResult | None:
|
|
457
|
+
return _last_capture
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def register_collector(collector: Any) -> None:
|
|
461
|
+
_global_registry.register(collector)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def unregister_collector(name: str) -> None:
|
|
465
|
+
_global_registry.unregister(name)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def register_codec(codec: Any) -> None:
|
|
469
|
+
_global_codecs[codec.type_name] = codec
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def unregister_codec(name: str) -> None:
|
|
473
|
+
_global_codecs.pop(name, None)
|