logxpy 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.
- logxpy/__init__.py +126 -0
- logxpy/_action.py +958 -0
- logxpy/_async.py +186 -0
- logxpy/_base.py +80 -0
- logxpy/_compat.py +71 -0
- logxpy/_config.py +45 -0
- logxpy/_dest.py +88 -0
- logxpy/_errors.py +58 -0
- logxpy/_fmt.py +68 -0
- logxpy/_generators.py +136 -0
- logxpy/_mask.py +23 -0
- logxpy/_message.py +195 -0
- logxpy/_output.py +517 -0
- logxpy/_pool.py +93 -0
- logxpy/_traceback.py +126 -0
- logxpy/_types.py +71 -0
- logxpy/_util.py +56 -0
- logxpy/_validation.py +486 -0
- logxpy/_version.py +21 -0
- logxpy/cli.py +61 -0
- logxpy/dask.py +172 -0
- logxpy/decorators.py +268 -0
- logxpy/filter.py +124 -0
- logxpy/journald.py +88 -0
- logxpy/json.py +149 -0
- logxpy/loggerx.py +253 -0
- logxpy/logwriter.py +84 -0
- logxpy/parse.py +191 -0
- logxpy/prettyprint.py +173 -0
- logxpy/serializers.py +36 -0
- logxpy/stdlib.py +23 -0
- logxpy/tai64n.py +45 -0
- logxpy/testing.py +472 -0
- logxpy/tests/__init__.py +9 -0
- logxpy/tests/common.py +36 -0
- logxpy/tests/strategies.py +231 -0
- logxpy/tests/test_action.py +1751 -0
- logxpy/tests/test_api.py +86 -0
- logxpy/tests/test_async.py +67 -0
- logxpy/tests/test_compat.py +13 -0
- logxpy/tests/test_config.py +21 -0
- logxpy/tests/test_coroutines.py +105 -0
- logxpy/tests/test_dask.py +211 -0
- logxpy/tests/test_decorators.py +54 -0
- logxpy/tests/test_filter.py +122 -0
- logxpy/tests/test_fmt.py +42 -0
- logxpy/tests/test_generators.py +292 -0
- logxpy/tests/test_journald.py +246 -0
- logxpy/tests/test_json.py +208 -0
- logxpy/tests/test_loggerx.py +44 -0
- logxpy/tests/test_logwriter.py +262 -0
- logxpy/tests/test_message.py +334 -0
- logxpy/tests/test_output.py +921 -0
- logxpy/tests/test_parse.py +309 -0
- logxpy/tests/test_pool.py +55 -0
- logxpy/tests/test_prettyprint.py +303 -0
- logxpy/tests/test_pyinstaller.py +35 -0
- logxpy/tests/test_serializers.py +36 -0
- logxpy/tests/test_stdlib.py +73 -0
- logxpy/tests/test_tai64n.py +66 -0
- logxpy/tests/test_testing.py +1051 -0
- logxpy/tests/test_traceback.py +251 -0
- logxpy/tests/test_twisted.py +814 -0
- logxpy/tests/test_util.py +45 -0
- logxpy/tests/test_validation.py +989 -0
- logxpy/twisted.py +265 -0
- logxpy-0.1.0.dist-info/METADATA +100 -0
- logxpy-0.1.0.dist-info/RECORD +72 -0
- logxpy-0.1.0.dist-info/WHEEL +5 -0
- logxpy-0.1.0.dist-info/entry_points.txt +2 -0
- logxpy-0.1.0.dist-info/licenses/LICENSE +201 -0
- logxpy-0.1.0.dist-info/top_level.txt +1 -0
logxpy/_traceback.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logging of tracebacks and L{twisted.python.failure.Failure} instances,
|
|
3
|
+
as well as common utilities for handling exception logging.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import traceback
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from ._message import EXCEPTION_FIELD, REASON_FIELD
|
|
10
|
+
from ._util import safeunicode, load_module
|
|
11
|
+
from ._validation import MessageType, Field
|
|
12
|
+
from ._errors import _error_extraction
|
|
13
|
+
|
|
14
|
+
TRACEBACK_MESSAGE = MessageType(
|
|
15
|
+
"eliot:traceback",
|
|
16
|
+
[
|
|
17
|
+
Field(REASON_FIELD, safeunicode, "The exception's value."),
|
|
18
|
+
Field("traceback", safeunicode, "The traceback."),
|
|
19
|
+
Field(
|
|
20
|
+
EXCEPTION_FIELD,
|
|
21
|
+
lambda typ: "%s.%s" % (typ.__module__, typ.__name__),
|
|
22
|
+
"The exception type's FQPN.",
|
|
23
|
+
),
|
|
24
|
+
],
|
|
25
|
+
"An unexpected exception indicating a bug.",
|
|
26
|
+
)
|
|
27
|
+
# The fields here are actually subset of what you might get in practice,
|
|
28
|
+
# due to exception extraction, so we hackily modify the serializer:
|
|
29
|
+
TRACEBACK_MESSAGE._serializer.allow_additional_fields = True
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _writeTracebackMessage(logger, typ, exception, traceback):
|
|
33
|
+
"""
|
|
34
|
+
Write a traceback to the log.
|
|
35
|
+
|
|
36
|
+
@param typ: The class of the exception.
|
|
37
|
+
|
|
38
|
+
@param exception: The L{Exception} instance.
|
|
39
|
+
|
|
40
|
+
@param traceback: The traceback, a C{str}.
|
|
41
|
+
"""
|
|
42
|
+
msg = TRACEBACK_MESSAGE(reason=exception, traceback=traceback, exception=typ)
|
|
43
|
+
msg = msg.bind(**_error_extraction.get_fields_for_exception(logger, exception))
|
|
44
|
+
msg.write(logger)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# The default Python standard library traceback.py formatting functions
|
|
48
|
+
# involving reading source from disk. This is a potential performance hit
|
|
49
|
+
# since disk I/O can block. We therefore format the tracebacks with in-memory
|
|
50
|
+
# information only.
|
|
51
|
+
#
|
|
52
|
+
# Unfortunately, the easiest way to do this is... exciting.
|
|
53
|
+
def _get_traceback_no_io():
|
|
54
|
+
"""
|
|
55
|
+
Return a version of L{traceback} that doesn't do I/O.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
module = load_module(str("_traceback_no_io"), traceback)
|
|
59
|
+
except NotImplementedError:
|
|
60
|
+
# Can't fix the I/O problem, oh well:
|
|
61
|
+
return traceback
|
|
62
|
+
|
|
63
|
+
class FakeLineCache(object):
|
|
64
|
+
def checkcache(self, *args, **kwargs):
|
|
65
|
+
None
|
|
66
|
+
|
|
67
|
+
def getline(self, *args, **kwargs):
|
|
68
|
+
return ""
|
|
69
|
+
|
|
70
|
+
def lazycache(self, *args, **kwargs):
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
module.linecache = FakeLineCache()
|
|
74
|
+
return module
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
_traceback_no_io = _get_traceback_no_io()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def write_traceback(logger=None, exc_info=None):
|
|
81
|
+
"""
|
|
82
|
+
Write the latest traceback to the log.
|
|
83
|
+
|
|
84
|
+
This should be used inside an C{except} block. For example:
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
dostuff()
|
|
88
|
+
except:
|
|
89
|
+
write_traceback(logger)
|
|
90
|
+
|
|
91
|
+
Or you can pass the result of C{sys.exc_info()} to the C{exc_info}
|
|
92
|
+
parameter.
|
|
93
|
+
"""
|
|
94
|
+
if exc_info is None:
|
|
95
|
+
exc_info = sys.exc_info()
|
|
96
|
+
typ, exception, tb = exc_info
|
|
97
|
+
traceback = "".join(_traceback_no_io.format_exception(typ, exception, tb))
|
|
98
|
+
_writeTracebackMessage(logger, typ, exception, traceback)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def writeFailure(failure, logger=None):
|
|
102
|
+
"""
|
|
103
|
+
Write a L{twisted.python.failure.Failure} to the log.
|
|
104
|
+
|
|
105
|
+
This is for situations where you got an unexpected exception and want to
|
|
106
|
+
log a traceback. For example, if you have C{Deferred} that might error,
|
|
107
|
+
you'll want to wrap it with a L{eliot.twisted.DeferredContext} and then add
|
|
108
|
+
C{writeFailure} as the error handler to get the traceback logged:
|
|
109
|
+
|
|
110
|
+
d = DeferredContext(dostuff())
|
|
111
|
+
d.addCallback(process)
|
|
112
|
+
# Final error handler.
|
|
113
|
+
d.addErrback(writeFailure)
|
|
114
|
+
|
|
115
|
+
@param failure: L{Failure} to write to the log.
|
|
116
|
+
|
|
117
|
+
@type logger: L{eliot.ILogger}. Will be deprecated at some point, so just
|
|
118
|
+
ignore it.
|
|
119
|
+
|
|
120
|
+
@return: None
|
|
121
|
+
"""
|
|
122
|
+
# Failure.getBriefTraceback does not include source code, so does not do
|
|
123
|
+
# I/O.
|
|
124
|
+
_writeTracebackMessage(
|
|
125
|
+
logger, failure.value.__class__, failure.value, failure.getBriefTraceback()
|
|
126
|
+
)
|
logxpy/_types.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Core types and protocols - eliot-compatible field names."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import IntEnum
|
|
7
|
+
from typing import Any, Protocol, runtime_checkable
|
|
8
|
+
|
|
9
|
+
# Eliot-compatible field names (from eliot/_message.py, eliot/_action.py)
|
|
10
|
+
TASK_UUID = "task_uuid"
|
|
11
|
+
TASK_LEVEL = "task_level"
|
|
12
|
+
TIMESTAMP = "timestamp"
|
|
13
|
+
MESSAGE_TYPE = "message_type"
|
|
14
|
+
ACTION_TYPE = "action_type"
|
|
15
|
+
ACTION_STATUS = "action_status"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Level(IntEnum):
|
|
19
|
+
DEBUG = 10
|
|
20
|
+
INFO = 20
|
|
21
|
+
SUCCESS = 25
|
|
22
|
+
NOTE = 26
|
|
23
|
+
WARNING = 30
|
|
24
|
+
ERROR = 40
|
|
25
|
+
CRITICAL = 50
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class Record:
|
|
30
|
+
"""Immutable log record with eliot-compatible field names."""
|
|
31
|
+
|
|
32
|
+
timestamp: float # eliot: timestamp
|
|
33
|
+
level: Level
|
|
34
|
+
message: str
|
|
35
|
+
fields: dict[str, Any]
|
|
36
|
+
context: dict[str, Any]
|
|
37
|
+
task_uuid: str # eliot: task_uuid
|
|
38
|
+
task_level: tuple[int, ...] # eliot: task_level
|
|
39
|
+
action_type: str | None = None
|
|
40
|
+
action_status: str | None = None
|
|
41
|
+
message_type: str | None = None
|
|
42
|
+
|
|
43
|
+
def to_dict(self) -> dict[str, Any]:
|
|
44
|
+
"""Convert to eliot-compatible dict format."""
|
|
45
|
+
d = {
|
|
46
|
+
TIMESTAMP: self.timestamp,
|
|
47
|
+
TASK_UUID: self.task_uuid,
|
|
48
|
+
TASK_LEVEL: list(self.task_level),
|
|
49
|
+
**self.context,
|
|
50
|
+
**self.fields,
|
|
51
|
+
}
|
|
52
|
+
if self.message_type:
|
|
53
|
+
d[MESSAGE_TYPE] = self.message_type
|
|
54
|
+
d["message"] = self.message
|
|
55
|
+
if self.action_type:
|
|
56
|
+
d[ACTION_TYPE] = self.action_type
|
|
57
|
+
d[ACTION_STATUS] = self.action_status
|
|
58
|
+
return d
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@runtime_checkable
|
|
62
|
+
class Destination(Protocol):
|
|
63
|
+
async def write(self, record: Record) -> None: ...
|
|
64
|
+
async def flush(self) -> None: ...
|
|
65
|
+
async def close(self) -> None: ...
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@runtime_checkable
|
|
69
|
+
class Formatter(Protocol):
|
|
70
|
+
def supports(self, obj: Any) -> bool: ...
|
|
71
|
+
def format(self, obj: Any, **opts: Any) -> dict[str, Any]: ...
|
logxpy/_util.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities that don't go anywhere else.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from types import ModuleType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def safeunicode(o):
|
|
9
|
+
"""
|
|
10
|
+
Like C{str()}, but catches and swallows any raised exceptions.
|
|
11
|
+
|
|
12
|
+
@param o: An object of some sort.
|
|
13
|
+
|
|
14
|
+
@return: C{str(o)}, or an error message if that failed.
|
|
15
|
+
@rtype: C{str}
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
return str(o)
|
|
19
|
+
except:
|
|
20
|
+
# Not much we can do about this...
|
|
21
|
+
return "eliot: unknown, str() raised exception"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def saferepr(o):
|
|
25
|
+
"""
|
|
26
|
+
Like C{str(repr())}, but catches and swallows any raised exceptions.
|
|
27
|
+
|
|
28
|
+
@param o: An object of some sort.
|
|
29
|
+
|
|
30
|
+
@return: C{str(repr(o))}, or an error message if that failed.
|
|
31
|
+
@rtype: C{str}
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
return str(repr(o))
|
|
35
|
+
except:
|
|
36
|
+
# Not much we can do about this...
|
|
37
|
+
return "eliot: unknown, str() raised exception"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_module(name, original_module):
|
|
41
|
+
"""
|
|
42
|
+
Load a copy of a module, distinct from what you'd get if you imported
|
|
43
|
+
it directly.
|
|
44
|
+
|
|
45
|
+
@param str name: The name of the new module.
|
|
46
|
+
@param original_module: The original module we're recreating.
|
|
47
|
+
|
|
48
|
+
@return: A new, distinct module.
|
|
49
|
+
"""
|
|
50
|
+
import importlib.util
|
|
51
|
+
|
|
52
|
+
module = ModuleType(name)
|
|
53
|
+
spec = importlib.util.find_spec(original_module.__name__)
|
|
54
|
+
source = spec.loader.get_code(original_module.__name__)
|
|
55
|
+
exec(source, module.__dict__, module.__dict__)
|
|
56
|
+
return module
|