getfaultline 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.
- faultline/__init__.py +405 -0
- faultline/_version.py +1 -0
- faultline/application/__init__.py +33 -0
- faultline/application/client.py +377 -0
- faultline/application/hub.py +210 -0
- faultline/application/ports.py +81 -0
- faultline/domain/__init__.py +107 -0
- faultline/domain/event.py +109 -0
- faultline/domain/limits.py +41 -0
- faultline/domain/normalize.py +117 -0
- faultline/domain/options.py +192 -0
- faultline/domain/processing.py +403 -0
- faultline/domain/scope.py +158 -0
- faultline/domain/scrub.py +85 -0
- faultline/infrastructure/__init__.py +21 -0
- faultline/infrastructure/runtime.py +98 -0
- faultline/infrastructure/source.py +48 -0
- faultline/infrastructure/stack.py +142 -0
- faultline/infrastructure/transport.py +414 -0
- faultline/integrations/__init__.py +18 -0
- faultline/integrations/_common.py +138 -0
- faultline/integrations/asgi.py +53 -0
- faultline/integrations/django.py +101 -0
- faultline/integrations/excepthook.py +86 -0
- faultline/integrations/fastapi.py +28 -0
- faultline/integrations/flask.py +43 -0
- faultline/integrations/logging.py +118 -0
- faultline/integrations/stdlib_http.py +137 -0
- faultline/integrations/wsgi.py +81 -0
- faultline/py.typed +0 -0
- getfaultline-0.1.0.dist-info/METADATA +383 -0
- getfaultline-0.1.0.dist-info/RECORD +34 -0
- getfaultline-0.1.0.dist-info/WHEEL +4 -0
- getfaultline-0.1.0.dist-info/licenses/LICENSE +21 -0
faultline/__init__.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"""Faultline Python SDK: public API and composition root.
|
|
2
|
+
|
|
3
|
+
This is the only module that wires the application, infrastructure and integration layers
|
|
4
|
+
together. Every public function is exception-safe: the SDK never raises into the host app
|
|
5
|
+
(except for exceptions raised by your own code inside ``with_scope()``).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import atexit
|
|
11
|
+
import sys
|
|
12
|
+
import threading
|
|
13
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
14
|
+
from contextlib import contextmanager
|
|
15
|
+
from typing import Any, Callable, Dict, List, Optional, TypeVar
|
|
16
|
+
|
|
17
|
+
from ._version import __version__
|
|
18
|
+
from .application import CaptureHint, ClientDeps, FaultlineClient, Hub, Integration, get_hub
|
|
19
|
+
from .domain import (
|
|
20
|
+
BeforeSend,
|
|
21
|
+
Breadcrumb,
|
|
22
|
+
Event,
|
|
23
|
+
ExceptionPayload,
|
|
24
|
+
IgnorePattern,
|
|
25
|
+
Level,
|
|
26
|
+
RequestData,
|
|
27
|
+
Scope,
|
|
28
|
+
StackFrame,
|
|
29
|
+
User,
|
|
30
|
+
is_level,
|
|
31
|
+
resolve_options,
|
|
32
|
+
safe_str,
|
|
33
|
+
)
|
|
34
|
+
from .infrastructure import (
|
|
35
|
+
HttpTransport,
|
|
36
|
+
LinecacheSourceReader,
|
|
37
|
+
PythonRuntimeInfo,
|
|
38
|
+
StderrLogger,
|
|
39
|
+
StdRandom,
|
|
40
|
+
SystemClock,
|
|
41
|
+
TracebackStackExtractor,
|
|
42
|
+
UuidGenerator,
|
|
43
|
+
)
|
|
44
|
+
from .integrations import (
|
|
45
|
+
ExcepthookIntegration,
|
|
46
|
+
LoggingIntegration,
|
|
47
|
+
StdlibHttpIntegration,
|
|
48
|
+
ThreadingIntegration,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"BeforeSend",
|
|
53
|
+
"Breadcrumb",
|
|
54
|
+
"Event",
|
|
55
|
+
"ExceptionPayload",
|
|
56
|
+
"Level",
|
|
57
|
+
"LoggingIntegration",
|
|
58
|
+
"RequestData",
|
|
59
|
+
"Scope",
|
|
60
|
+
"StackFrame",
|
|
61
|
+
"User",
|
|
62
|
+
"__version__",
|
|
63
|
+
"add_breadcrumb",
|
|
64
|
+
"capture_exception",
|
|
65
|
+
"capture_message",
|
|
66
|
+
"close",
|
|
67
|
+
"flush",
|
|
68
|
+
"get_current_scope",
|
|
69
|
+
"init",
|
|
70
|
+
"is_enabled",
|
|
71
|
+
"set_context",
|
|
72
|
+
"set_extra",
|
|
73
|
+
"set_tag",
|
|
74
|
+
"set_tags",
|
|
75
|
+
"set_user",
|
|
76
|
+
"with_scope",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
SDK_VERSION = __version__
|
|
80
|
+
ATEXIT_TIMEOUT = 2.0
|
|
81
|
+
|
|
82
|
+
_T = TypeVar("_T")
|
|
83
|
+
|
|
84
|
+
_INTEGRATION_FACTORIES: Dict[str, Callable[[], Integration]] = {
|
|
85
|
+
"excepthook": ExcepthookIntegration,
|
|
86
|
+
"threading": ThreadingIntegration,
|
|
87
|
+
"logging": LoggingIntegration,
|
|
88
|
+
"http": StdlibHttpIntegration,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_init_lock = threading.Lock()
|
|
92
|
+
_atexit_registered = False
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _hub() -> Hub:
|
|
96
|
+
return get_hub()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _guard(fallback: _T, fn: Callable[[], _T]) -> _T:
|
|
100
|
+
try:
|
|
101
|
+
return fn()
|
|
102
|
+
except Exception as err:
|
|
103
|
+
try:
|
|
104
|
+
_hub().logger.error("internal SDK error", err)
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
return fallback
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _resolve_integrations(settings: Mapping[str, Any], logger: StderrLogger) -> List[Integration]:
|
|
111
|
+
out: List[Integration] = []
|
|
112
|
+
for name, factory in _INTEGRATION_FACTORIES.items():
|
|
113
|
+
setting = settings.get(name, False)
|
|
114
|
+
if setting is True:
|
|
115
|
+
out.append(factory())
|
|
116
|
+
elif setting is False:
|
|
117
|
+
continue
|
|
118
|
+
elif hasattr(setting, "setup") and hasattr(setting, "teardown"):
|
|
119
|
+
out.append(setting)
|
|
120
|
+
else:
|
|
121
|
+
logger.warn(f'integrations["{name}"] must be a bool or an integration instance')
|
|
122
|
+
return out
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _on_exit() -> None:
|
|
126
|
+
try:
|
|
127
|
+
_hub().close(ATEXIT_TIMEOUT)
|
|
128
|
+
except Exception:
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def init(
|
|
133
|
+
api_key: Optional[str] = None,
|
|
134
|
+
*,
|
|
135
|
+
endpoint: Optional[str] = None,
|
|
136
|
+
environment: Optional[str] = None,
|
|
137
|
+
release: Optional[str] = None,
|
|
138
|
+
server_name: Optional[str] = None,
|
|
139
|
+
sample_rate: Optional[float] = None,
|
|
140
|
+
max_breadcrumbs: Optional[int] = None,
|
|
141
|
+
send_default_pii: Optional[bool] = None,
|
|
142
|
+
before_send: Optional[BeforeSend] = None,
|
|
143
|
+
ignore_errors: Optional[Sequence[IgnorePattern]] = None,
|
|
144
|
+
integrations: Optional[Mapping[str, Any]] = None,
|
|
145
|
+
enabled: Optional[bool] = None,
|
|
146
|
+
debug: Optional[bool] = None,
|
|
147
|
+
**unknown: Any,
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Initialize the SDK. Calling it again replaces the previous client. Never raises.
|
|
150
|
+
|
|
151
|
+
``integrations`` maps ``"excepthook"``, ``"threading"``, ``"logging"`` and ``"http"`` to
|
|
152
|
+
``True`` / ``False`` or to a configured instance, e.g.
|
|
153
|
+
``{"logging": LoggingIntegration(capture_errors=True)}``.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
def run() -> None:
|
|
157
|
+
global _atexit_registered
|
|
158
|
+
with _init_lock:
|
|
159
|
+
hub = _hub()
|
|
160
|
+
previous = hub.client
|
|
161
|
+
hub.teardown_integrations()
|
|
162
|
+
hub.bind_client(None)
|
|
163
|
+
if previous is not None:
|
|
164
|
+
# Let the old client deliver what it already queued, without blocking init.
|
|
165
|
+
threading.Thread(
|
|
166
|
+
target=previous.close,
|
|
167
|
+
args=(ATEXIT_TIMEOUT,),
|
|
168
|
+
name="faultline-close",
|
|
169
|
+
daemon=True,
|
|
170
|
+
).start()
|
|
171
|
+
|
|
172
|
+
raw: Dict[str, Any] = {
|
|
173
|
+
"api_key": api_key,
|
|
174
|
+
"endpoint": endpoint,
|
|
175
|
+
"environment": environment,
|
|
176
|
+
"release": release,
|
|
177
|
+
"server_name": server_name,
|
|
178
|
+
"sample_rate": sample_rate,
|
|
179
|
+
"max_breadcrumbs": max_breadcrumbs,
|
|
180
|
+
"send_default_pii": send_default_pii,
|
|
181
|
+
"before_send": before_send,
|
|
182
|
+
"ignore_errors": ignore_errors,
|
|
183
|
+
"integrations": integrations,
|
|
184
|
+
"enabled": enabled,
|
|
185
|
+
"debug": debug,
|
|
186
|
+
}
|
|
187
|
+
result = resolve_options(raw)
|
|
188
|
+
logger = StderrLogger(result.debug)
|
|
189
|
+
for key in unknown:
|
|
190
|
+
logger.warn(f'unknown option "{key}"; ignored')
|
|
191
|
+
for w in result.warnings:
|
|
192
|
+
logger.warn(f"invalid option: {w}")
|
|
193
|
+
opts = result.options
|
|
194
|
+
if opts is None:
|
|
195
|
+
logger.error("SDK disabled: " + "; ".join(result.errors))
|
|
196
|
+
return
|
|
197
|
+
if not opts.enabled:
|
|
198
|
+
logger.debug("SDK disabled (enabled=False)")
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
transport = HttpTransport(opts.endpoint, opts.api_key, logger)
|
|
202
|
+
client = FaultlineClient(
|
|
203
|
+
opts,
|
|
204
|
+
ClientDeps(
|
|
205
|
+
transport=transport,
|
|
206
|
+
stack=TracebackStackExtractor(),
|
|
207
|
+
source=LinecacheSourceReader(),
|
|
208
|
+
clock=SystemClock(),
|
|
209
|
+
ids=UuidGenerator(),
|
|
210
|
+
random=StdRandom(),
|
|
211
|
+
logger=logger,
|
|
212
|
+
runtime=PythonRuntimeInfo(),
|
|
213
|
+
sdk_version=SDK_VERSION,
|
|
214
|
+
),
|
|
215
|
+
)
|
|
216
|
+
hub.bind_client(client)
|
|
217
|
+
hub.install_integrations(_resolve_integrations(opts.integrations, logger))
|
|
218
|
+
if not _atexit_registered:
|
|
219
|
+
atexit.register(_on_exit)
|
|
220
|
+
_atexit_registered = True
|
|
221
|
+
logger.debug(f"initialized (endpoint {opts.endpoint}, sdk {SDK_VERSION})")
|
|
222
|
+
|
|
223
|
+
_guard(None, run)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _hint(
|
|
227
|
+
tags: Optional[Mapping[str, Any]],
|
|
228
|
+
extra: Optional[Mapping[str, Any]],
|
|
229
|
+
user: Optional[User],
|
|
230
|
+
level: Optional[str],
|
|
231
|
+
fingerprint: Optional[Sequence[str]],
|
|
232
|
+
contexts: Optional[Mapping[str, Mapping[str, Any]]],
|
|
233
|
+
) -> CaptureHint:
|
|
234
|
+
return CaptureHint(
|
|
235
|
+
tags={str(k): safe_str(v) for k, v in tags.items()} if isinstance(tags, Mapping) else None,
|
|
236
|
+
extra=extra if isinstance(extra, Mapping) else None,
|
|
237
|
+
user=user if isinstance(user, Mapping) else None,
|
|
238
|
+
level=level if level is not None and is_level(level) else None,
|
|
239
|
+
fingerprint=[safe_str(x) for x in fingerprint]
|
|
240
|
+
if isinstance(fingerprint, (list, tuple))
|
|
241
|
+
else None,
|
|
242
|
+
contexts=contexts if isinstance(contexts, Mapping) else None,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def capture_exception(
|
|
247
|
+
exc: Any = None,
|
|
248
|
+
*,
|
|
249
|
+
tags: Optional[Mapping[str, Any]] = None,
|
|
250
|
+
extra: Optional[Mapping[str, Any]] = None,
|
|
251
|
+
user: Optional[User] = None,
|
|
252
|
+
level: Optional[Level] = None,
|
|
253
|
+
fingerprint: Optional[Sequence[str]] = None,
|
|
254
|
+
contexts: Optional[Mapping[str, Mapping[str, Any]]] = None,
|
|
255
|
+
**unknown: Any,
|
|
256
|
+
) -> Optional[str]:
|
|
257
|
+
"""Capture an exception and return its event id (``None`` if dropped or disabled).
|
|
258
|
+
|
|
259
|
+
``exc`` defaults to the exception currently being handled (``sys.exc_info()``). It may
|
|
260
|
+
also be an ``exc_info`` tuple or any non-exception value (captured as a synthetic error).
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
def run() -> Optional[str]:
|
|
264
|
+
value: Any = exc
|
|
265
|
+
if value is None:
|
|
266
|
+
value = sys.exc_info()[1]
|
|
267
|
+
if value is None:
|
|
268
|
+
_hub().logger.debug("capture_exception called without an active exception")
|
|
269
|
+
return None
|
|
270
|
+
elif isinstance(value, tuple) and len(value) == 3:
|
|
271
|
+
value = value[1]
|
|
272
|
+
hint = _hint(tags, extra, user, level, fingerprint, contexts)
|
|
273
|
+
return _hub().capture_exception(value, hint)
|
|
274
|
+
|
|
275
|
+
return _guard(None, run)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def capture_message(
|
|
279
|
+
message: str,
|
|
280
|
+
level: Level = "info",
|
|
281
|
+
*,
|
|
282
|
+
tags: Optional[Mapping[str, Any]] = None,
|
|
283
|
+
extra: Optional[Mapping[str, Any]] = None,
|
|
284
|
+
user: Optional[User] = None,
|
|
285
|
+
fingerprint: Optional[Sequence[str]] = None,
|
|
286
|
+
contexts: Optional[Mapping[str, Mapping[str, Any]]] = None,
|
|
287
|
+
**unknown: Any,
|
|
288
|
+
) -> Optional[str]:
|
|
289
|
+
"""Capture a plain message (default level ``"info"``) and return its event id."""
|
|
290
|
+
|
|
291
|
+
def run() -> Optional[str]:
|
|
292
|
+
lvl = level if is_level(level) else "info"
|
|
293
|
+
hint = _hint(tags, extra, user, lvl, fingerprint, contexts)
|
|
294
|
+
return _hub().capture_message(message, lvl, hint)
|
|
295
|
+
|
|
296
|
+
return _guard(None, run)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def set_user(user: Optional[User]) -> None:
|
|
300
|
+
"""Set (or, with ``None``, clear) the user on the current scope."""
|
|
301
|
+
_guard(None, lambda: _hub().set_user(user if isinstance(user, Mapping) and user else None))
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def set_tag(key: str, value: Any) -> None:
|
|
305
|
+
def run() -> None:
|
|
306
|
+
if isinstance(key, str):
|
|
307
|
+
_hub().set_tag(key, safe_str(value))
|
|
308
|
+
|
|
309
|
+
_guard(None, run)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def set_tags(tags: Mapping[str, Any]) -> None:
|
|
313
|
+
def run() -> None:
|
|
314
|
+
if isinstance(tags, Mapping):
|
|
315
|
+
_hub().set_tags({str(k): safe_str(v) for k, v in tags.items()})
|
|
316
|
+
|
|
317
|
+
_guard(None, run)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def set_context(name: str, context: Optional[Mapping[str, Any]]) -> None:
|
|
321
|
+
"""Set a named structured context; ``None`` removes it."""
|
|
322
|
+
|
|
323
|
+
def run() -> None:
|
|
324
|
+
if isinstance(name, str):
|
|
325
|
+
_hub().set_context(name, context if isinstance(context, Mapping) else None)
|
|
326
|
+
|
|
327
|
+
_guard(None, run)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def set_extra(key: str, value: Any) -> None:
|
|
331
|
+
def run() -> None:
|
|
332
|
+
if isinstance(key, str):
|
|
333
|
+
_hub().set_extra(key, value)
|
|
334
|
+
|
|
335
|
+
_guard(None, run)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def add_breadcrumb(crumb: Optional[Mapping[str, Any]] = None, **fields: Any) -> None:
|
|
339
|
+
"""Add a breadcrumb to the current scope.
|
|
340
|
+
|
|
341
|
+
Pass a mapping or keyword arguments: ``type`` (``default``, ``http``, ``log``, ``query``,
|
|
342
|
+
``navigation``), ``category``, ``message``, ``level`` and ``data``.
|
|
343
|
+
"""
|
|
344
|
+
|
|
345
|
+
def run() -> None:
|
|
346
|
+
merged: Dict[str, Any] = dict(crumb) if isinstance(crumb, Mapping) else {}
|
|
347
|
+
merged.update(fields)
|
|
348
|
+
_hub().add_breadcrumb(merged)
|
|
349
|
+
|
|
350
|
+
_guard(None, run)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@contextmanager
|
|
354
|
+
def with_scope() -> Iterator[Scope]:
|
|
355
|
+
"""Fork the current scope for the duration of the ``with`` block.
|
|
356
|
+
|
|
357
|
+
Changes made to the yielded scope (or through ``set_tag`` and friends) apply only inside
|
|
358
|
+
the block, including threads started with ``contextvars.copy_context()`` and asyncio tasks
|
|
359
|
+
created inside it. Exceptions raised by your code propagate unchanged.
|
|
360
|
+
"""
|
|
361
|
+
try:
|
|
362
|
+
cm = _hub().with_scope()
|
|
363
|
+
scope = cm.__enter__()
|
|
364
|
+
except Exception:
|
|
365
|
+
yield Scope() # detached scope: the SDK must never break the caller
|
|
366
|
+
return
|
|
367
|
+
try:
|
|
368
|
+
yield scope
|
|
369
|
+
except BaseException:
|
|
370
|
+
if not cm.__exit__(*sys.exc_info()):
|
|
371
|
+
raise
|
|
372
|
+
else:
|
|
373
|
+
try:
|
|
374
|
+
cm.__exit__(None, None, None)
|
|
375
|
+
except Exception:
|
|
376
|
+
pass
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def get_current_scope() -> Scope:
|
|
380
|
+
"""The scope that ``set_tag`` & co. write to right now."""
|
|
381
|
+
return _guard(Scope(), lambda: _hub().get_scope())
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _timeout(value: Any, default: float = 2.0) -> float:
|
|
385
|
+
"""Invalid timeouts (``None``, strings, NaN, negative) fall back to the default."""
|
|
386
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
387
|
+
return default
|
|
388
|
+
if value != value or value < 0:
|
|
389
|
+
return default
|
|
390
|
+
return float(min(value, 3600.0))
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def flush(timeout: float = 2.0) -> bool:
|
|
394
|
+
"""Block until queued events are sent. Returns ``False`` on timeout. Never raises."""
|
|
395
|
+
return _guard(False, lambda: _hub().flush(_timeout(timeout)))
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def close(timeout: float = 2.0) -> bool:
|
|
399
|
+
"""Flush, remove the integrations and disable the SDK. Never raises."""
|
|
400
|
+
return _guard(False, lambda: _hub().close(_timeout(timeout)))
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def is_enabled() -> bool:
|
|
404
|
+
"""``True`` once ``init`` succeeded and the SDK is sending events."""
|
|
405
|
+
return _guard(False, lambda: bool(_hub().client and _hub().client.is_enabled)) # type: ignore[union-attr]
|
faultline/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Application layer: the client, the hub and the ports they depend on."""
|
|
2
|
+
|
|
3
|
+
from .client import CaptureHint, ClientDeps, FaultlineClient
|
|
4
|
+
from .hub import Hub, Integration, get_hub
|
|
5
|
+
from .ports import (
|
|
6
|
+
Clock,
|
|
7
|
+
IdGenerator,
|
|
8
|
+
Logger,
|
|
9
|
+
RandomSource,
|
|
10
|
+
RuntimeInfo,
|
|
11
|
+
SilentLogger,
|
|
12
|
+
SourceContextReader,
|
|
13
|
+
StackExtractor,
|
|
14
|
+
Transport,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"CaptureHint",
|
|
19
|
+
"ClientDeps",
|
|
20
|
+
"Clock",
|
|
21
|
+
"FaultlineClient",
|
|
22
|
+
"Hub",
|
|
23
|
+
"IdGenerator",
|
|
24
|
+
"Integration",
|
|
25
|
+
"Logger",
|
|
26
|
+
"RandomSource",
|
|
27
|
+
"RuntimeInfo",
|
|
28
|
+
"SilentLogger",
|
|
29
|
+
"SourceContextReader",
|
|
30
|
+
"StackExtractor",
|
|
31
|
+
"Transport",
|
|
32
|
+
"get_hub",
|
|
33
|
+
]
|