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/_generators.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Support for maintaining an action context across generator suspension.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from sys import exc_info
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from contextvars import copy_context
|
|
9
|
+
from weakref import WeakKeyDictionary
|
|
10
|
+
|
|
11
|
+
from . import log_message
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _GeneratorContext(object):
|
|
15
|
+
"""Generator sub-context for C{_ExecutionContext}."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, execution_context):
|
|
18
|
+
self._execution_context = execution_context
|
|
19
|
+
self._contexts = WeakKeyDictionary()
|
|
20
|
+
self._current_generator = None
|
|
21
|
+
|
|
22
|
+
def init_stack(self, generator):
|
|
23
|
+
"""Create a new stack for the given generator."""
|
|
24
|
+
self._contexts[generator] = copy_context()
|
|
25
|
+
|
|
26
|
+
@contextmanager
|
|
27
|
+
def in_generator(self, generator):
|
|
28
|
+
"""Context manager: set the given generator as the current generator."""
|
|
29
|
+
previous_generator = self._current_generator
|
|
30
|
+
try:
|
|
31
|
+
self._current_generator = generator
|
|
32
|
+
yield
|
|
33
|
+
finally:
|
|
34
|
+
self._current_generator = previous_generator
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class GeneratorSupportNotEnabled(Exception):
|
|
38
|
+
"""
|
|
39
|
+
An attempt was made to use a decorated generator without first turning on
|
|
40
|
+
the generator context manager.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def eliot_friendly_generator_function(original):
|
|
45
|
+
"""
|
|
46
|
+
Decorate a generator function so that the Eliot action context is
|
|
47
|
+
preserved across ``yield`` expressions.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
@wraps(original)
|
|
51
|
+
def wrapper(*a, **kw):
|
|
52
|
+
# Keep track of whether the next value to deliver to the generator is
|
|
53
|
+
# a non-exception or an exception.
|
|
54
|
+
ok = True
|
|
55
|
+
|
|
56
|
+
# Keep track of the next value to deliver to the generator.
|
|
57
|
+
value_in = None
|
|
58
|
+
|
|
59
|
+
# Create the generator with a call to the generator function. This
|
|
60
|
+
# happens with whatever Eliot action context happens to be active,
|
|
61
|
+
# which is fine and correct and also irrelevant because no code in the
|
|
62
|
+
# generator function can run until we call send or throw on it.
|
|
63
|
+
gen = original(*a, **kw)
|
|
64
|
+
|
|
65
|
+
# Initialize the per-generator context to a copy of the current context.
|
|
66
|
+
context = copy_context()
|
|
67
|
+
while True:
|
|
68
|
+
try:
|
|
69
|
+
# Whichever way we invoke the generator, we will do it
|
|
70
|
+
# with the Eliot action context stack we've saved for it.
|
|
71
|
+
# Then the context manager will re-save it and restore the
|
|
72
|
+
# "outside" stack for us.
|
|
73
|
+
#
|
|
74
|
+
# Regarding the support of Twisted's inlineCallbacks-like
|
|
75
|
+
# functionality (see eliot.twisted.inline_callbacks):
|
|
76
|
+
#
|
|
77
|
+
# The invocation may raise the inlineCallbacks internal
|
|
78
|
+
# control flow exception _DefGen_Return. It is not wrong to
|
|
79
|
+
# just let that propagate upwards here but inlineCallbacks
|
|
80
|
+
# does think it is wrong. The behavior triggers a
|
|
81
|
+
# DeprecationWarning to try to get us to fix our code. We
|
|
82
|
+
# could explicitly handle and re-raise the _DefGen_Return but
|
|
83
|
+
# only at the expense of depending on a private Twisted API.
|
|
84
|
+
# For now, I'm opting to try to encourage Twisted to fix the
|
|
85
|
+
# situation (or at least not worsen it):
|
|
86
|
+
# https://twistedmatrix.com/trac/ticket/9590
|
|
87
|
+
#
|
|
88
|
+
# Alternatively, _DefGen_Return is only required on Python 2.
|
|
89
|
+
# When Python 2 support is dropped, this concern can be
|
|
90
|
+
# eliminated by always using `return value` instead of
|
|
91
|
+
# `returnValue(value)` (and adding the necessary logic to the
|
|
92
|
+
# StopIteration handler below).
|
|
93
|
+
def go():
|
|
94
|
+
if ok:
|
|
95
|
+
value_out = gen.send(value_in)
|
|
96
|
+
else:
|
|
97
|
+
value_out = gen.throw(*value_in)
|
|
98
|
+
# We have obtained a value from the generator. In
|
|
99
|
+
# giving it to us, it has given up control. Note this
|
|
100
|
+
# fact here. Importantly, this is within the
|
|
101
|
+
# generator's action context so that we get a good
|
|
102
|
+
# indication of where the yield occurred.
|
|
103
|
+
#
|
|
104
|
+
# This is noisy, enable only for debugging:
|
|
105
|
+
if wrapper.debug:
|
|
106
|
+
log_message(message_type="yielded")
|
|
107
|
+
return value_out
|
|
108
|
+
|
|
109
|
+
value_out = context.run(go)
|
|
110
|
+
except StopIteration:
|
|
111
|
+
# When the generator raises this, it is signaling
|
|
112
|
+
# completion. Leave the loop.
|
|
113
|
+
break
|
|
114
|
+
else:
|
|
115
|
+
try:
|
|
116
|
+
# Pass the generator's result along to whoever is
|
|
117
|
+
# driving. Capture the result as the next value to
|
|
118
|
+
# send inward.
|
|
119
|
+
value_in = yield value_out
|
|
120
|
+
except:
|
|
121
|
+
# Or capture the exception if that's the flavor of the
|
|
122
|
+
# next value. This could possibly include GeneratorExit
|
|
123
|
+
# which turns out to be just fine because throwing it into
|
|
124
|
+
# the inner generator effectively propagates the close
|
|
125
|
+
# (and with the right context!) just as you would want.
|
|
126
|
+
# True, the GeneratorExit does get re-throwing out of the
|
|
127
|
+
# gen.throw call and hits _the_generator_context's
|
|
128
|
+
# contextmanager. But @contextmanager extremely
|
|
129
|
+
# conveniently eats it for us! Thanks, @contextmanager!
|
|
130
|
+
ok = False
|
|
131
|
+
value_in = exc_info()
|
|
132
|
+
else:
|
|
133
|
+
ok = True
|
|
134
|
+
|
|
135
|
+
wrapper.debug = False
|
|
136
|
+
return wrapper
|
logxpy/_mask.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Field masking."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
class Masker:
|
|
7
|
+
def __init__(self, fields: list[str], patterns: list[str]):
|
|
8
|
+
self._fields = {f.lower() for f in fields}
|
|
9
|
+
self._patterns = [re.compile(p) for p in patterns]
|
|
10
|
+
|
|
11
|
+
def mask(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
12
|
+
return {k: self._mask(k, v) for k, v in data.items()}
|
|
13
|
+
|
|
14
|
+
def _mask(self, key: str, val: Any) -> Any:
|
|
15
|
+
if key.lower() in self._fields: return "***"
|
|
16
|
+
if isinstance(val, str):
|
|
17
|
+
for p in self._patterns:
|
|
18
|
+
val = p.sub("***", val)
|
|
19
|
+
elif isinstance(val, dict):
|
|
20
|
+
return self.mask(val)
|
|
21
|
+
elif isinstance(val, list):
|
|
22
|
+
return [self._mask(key, v) for v in val]
|
|
23
|
+
return val
|
logxpy/_message.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Log messages and related utilities.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from warnings import warn
|
|
7
|
+
|
|
8
|
+
from pyrsistent import PClass, pmap_field
|
|
9
|
+
|
|
10
|
+
MESSAGE_TYPE_FIELD = "message_type"
|
|
11
|
+
TASK_UUID_FIELD = "task_uuid"
|
|
12
|
+
TASK_LEVEL_FIELD = "task_level"
|
|
13
|
+
TIMESTAMP_FIELD = "timestamp"
|
|
14
|
+
|
|
15
|
+
EXCEPTION_FIELD = "exception"
|
|
16
|
+
REASON_FIELD = "reason"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Message(object):
|
|
20
|
+
"""
|
|
21
|
+
A log message.
|
|
22
|
+
|
|
23
|
+
Messages are basically dictionaries, mapping "fields" to "values". Field
|
|
24
|
+
names should not start with C{'_'}, as those are reserved for system use
|
|
25
|
+
(e.g. C{"_id"} is used by Elasticsearch for unique message identifiers and
|
|
26
|
+
may be auto-populated by logstash).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
# Overrideable for testing purposes:
|
|
30
|
+
_time = time.time
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def new(_class, _serializer=None, **fields):
|
|
34
|
+
"""
|
|
35
|
+
Create a new L{Message}.
|
|
36
|
+
|
|
37
|
+
The keyword arguments will become the initial contents of the L{Message}.
|
|
38
|
+
|
|
39
|
+
@param _serializer: A positional argument, either C{None} or a
|
|
40
|
+
L{eliot._validation._MessageSerializer} with which a
|
|
41
|
+
L{eliot.ILogger} may choose to serialize the message. If you're
|
|
42
|
+
using L{eliot.MessageType} this will be populated for you.
|
|
43
|
+
|
|
44
|
+
@return: The new L{Message}
|
|
45
|
+
"""
|
|
46
|
+
warn(
|
|
47
|
+
"Message.new() is deprecated since 1.11.0, "
|
|
48
|
+
"use eliot.log_message() instead.",
|
|
49
|
+
DeprecationWarning,
|
|
50
|
+
stacklevel=2,
|
|
51
|
+
)
|
|
52
|
+
return _class(fields, _serializer)
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def log(_class, **fields):
|
|
56
|
+
"""
|
|
57
|
+
Write a new L{Message} to the default L{Logger}.
|
|
58
|
+
|
|
59
|
+
The keyword arguments will become contents of the L{Message}.
|
|
60
|
+
"""
|
|
61
|
+
warn(
|
|
62
|
+
"Message.log() is deprecated since 1.11.0, "
|
|
63
|
+
"use Action.log() or eliot.log_message() instead.",
|
|
64
|
+
DeprecationWarning,
|
|
65
|
+
stacklevel=2,
|
|
66
|
+
)
|
|
67
|
+
_class(fields).write()
|
|
68
|
+
|
|
69
|
+
def __init__(self, contents, serializer=None):
|
|
70
|
+
"""
|
|
71
|
+
You can also use L{Message.new} to create L{Message} objects.
|
|
72
|
+
|
|
73
|
+
@param contents: The contents of this L{Message}, a C{dict} whose keys
|
|
74
|
+
must be C{str}, or text that has been UTF-8 encoded to
|
|
75
|
+
C{bytes}.
|
|
76
|
+
|
|
77
|
+
@param serializer: Either C{None}, or
|
|
78
|
+
L{eliot._validation._MessageSerializer} with which a
|
|
79
|
+
L{eliot.Logger} may choose to serialize the message. If you're
|
|
80
|
+
using L{eliot.MessageType} this will be populated for you.
|
|
81
|
+
"""
|
|
82
|
+
self._contents = contents.copy()
|
|
83
|
+
self._serializer = serializer
|
|
84
|
+
|
|
85
|
+
def bind(self, **fields):
|
|
86
|
+
"""
|
|
87
|
+
Return a new L{Message} with this message's contents plus the
|
|
88
|
+
additional given bindings.
|
|
89
|
+
"""
|
|
90
|
+
contents = self._contents.copy()
|
|
91
|
+
contents.update(fields)
|
|
92
|
+
return Message(contents, self._serializer)
|
|
93
|
+
|
|
94
|
+
def contents(self):
|
|
95
|
+
"""
|
|
96
|
+
Return a copy of L{Message} contents.
|
|
97
|
+
"""
|
|
98
|
+
return self._contents.copy()
|
|
99
|
+
|
|
100
|
+
def _timestamp(self):
|
|
101
|
+
"""
|
|
102
|
+
Return the current time.
|
|
103
|
+
"""
|
|
104
|
+
return self._time()
|
|
105
|
+
|
|
106
|
+
def write(self, logger=None, action=None):
|
|
107
|
+
"""
|
|
108
|
+
Write the message to the given logger.
|
|
109
|
+
|
|
110
|
+
This will additionally include a timestamp, the action context if any,
|
|
111
|
+
and any other fields.
|
|
112
|
+
|
|
113
|
+
Byte field names will be converted to Unicode.
|
|
114
|
+
|
|
115
|
+
@type logger: L{eliot.ILogger} or C{None} indicating the default one.
|
|
116
|
+
Should not be set if the action is also set.
|
|
117
|
+
|
|
118
|
+
@param action: The L{Action} which is the context for this message. If
|
|
119
|
+
C{None}, the L{Action} will be deduced from the current call stack.
|
|
120
|
+
"""
|
|
121
|
+
fields = dict(self._contents)
|
|
122
|
+
if "message_type" not in fields:
|
|
123
|
+
fields["message_type"] = ""
|
|
124
|
+
if self._serializer is not None:
|
|
125
|
+
fields["__eliot_serializer__"] = self._serializer
|
|
126
|
+
if action is None:
|
|
127
|
+
if logger is not None:
|
|
128
|
+
fields["__eliot_logger__"] = logger
|
|
129
|
+
log_message(**fields)
|
|
130
|
+
else:
|
|
131
|
+
action.log(**fields)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class WrittenMessage(PClass):
|
|
135
|
+
"""
|
|
136
|
+
A L{Message} that has been logged.
|
|
137
|
+
|
|
138
|
+
@ivar _logged_dict: The originally logged dictionary.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
_logged_dict = pmap_field((str, str), object)
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def timestamp(self):
|
|
145
|
+
"""
|
|
146
|
+
The Unix timestamp of when the message was logged.
|
|
147
|
+
"""
|
|
148
|
+
return self._logged_dict[TIMESTAMP_FIELD]
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def task_uuid(self):
|
|
152
|
+
"""
|
|
153
|
+
The UUID of the task in which the message was logged.
|
|
154
|
+
"""
|
|
155
|
+
return self._logged_dict[TASK_UUID_FIELD]
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def task_level(self):
|
|
159
|
+
"""
|
|
160
|
+
The L{TaskLevel} of this message appears within the task.
|
|
161
|
+
"""
|
|
162
|
+
return TaskLevel(level=self._logged_dict[TASK_LEVEL_FIELD])
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def contents(self):
|
|
166
|
+
"""
|
|
167
|
+
A C{PMap}, the message contents without Eliot metadata.
|
|
168
|
+
"""
|
|
169
|
+
return (
|
|
170
|
+
self._logged_dict.discard(TIMESTAMP_FIELD)
|
|
171
|
+
.discard(TASK_UUID_FIELD)
|
|
172
|
+
.discard(TASK_LEVEL_FIELD)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
@classmethod
|
|
176
|
+
def from_dict(cls, logged_dictionary):
|
|
177
|
+
"""
|
|
178
|
+
Reconstruct a L{WrittenMessage} from a logged dictionary.
|
|
179
|
+
|
|
180
|
+
@param logged_dictionary: A C{PMap} representing a parsed log entry.
|
|
181
|
+
@return: A L{WrittenMessage} for that dictionary.
|
|
182
|
+
"""
|
|
183
|
+
return cls(_logged_dict=logged_dictionary)
|
|
184
|
+
|
|
185
|
+
def as_dict(self):
|
|
186
|
+
"""
|
|
187
|
+
Return the dictionary that was used to write this message.
|
|
188
|
+
|
|
189
|
+
@return: A C{dict}, as might be logged by Eliot.
|
|
190
|
+
"""
|
|
191
|
+
return self._logged_dict
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# Import at end to deal with circular imports:
|
|
195
|
+
from ._action import log_message, TaskLevel
|