omlish 0.0.0.dev430__py3-none-any.whl → 0.0.0.dev432__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.
- omlish/__about__.py +2 -2
- omlish/asyncs/ioproxy/proxy.py +1 -1
- omlish/concurrent/__init__.py +17 -11
- omlish/funcs/match.py +1 -1
- omlish/lang/imports/proxyinit.py +4 -3
- omlish/logs/all.py +10 -12
- omlish/logs/base.py +0 -30
- omlish/logs/contexts.py +71 -177
- omlish/logs/formatters.py +13 -0
- omlish/logs/infos.py +347 -90
- omlish/logs/standard.py +10 -6
- omlish/logs/std/formatters.py +25 -0
- omlish/logs/std/loggers.py +9 -9
- omlish/logs/std/records.py +571 -211
- omlish/logs/typed/values.py +2 -2
- omlish/math/fixed.py +2 -2
- omlish/secrets/secrets.py +3 -0
- {omlish-0.0.0.dev430.dist-info → omlish-0.0.0.dev432.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev430.dist-info → omlish-0.0.0.dev432.dist-info}/RECORD +23 -23
- omlish/logs/callers.py +0 -75
- omlish/logs/times.py +0 -89
- {omlish-0.0.0.dev430.dist-info → omlish-0.0.0.dev432.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev430.dist-info → omlish-0.0.0.dev432.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev430.dist-info → omlish-0.0.0.dev432.dist-info}/licenses/LICENSE +0 -0
- {omlish-0.0.0.dev430.dist-info → omlish-0.0.0.dev432.dist-info}/top_level.txt +0 -0
omlish/logs/std/records.py
CHANGED
@@ -1,152 +1,574 @@
|
|
1
1
|
# ruff: noqa: UP006 UP007 UP045
|
2
2
|
# @omlish-lite
|
3
|
+
"""
|
4
|
+
TODO:
|
5
|
+
- TypedDict?
|
6
|
+
"""
|
7
|
+
import abc
|
3
8
|
import collections.abc
|
4
9
|
import logging
|
5
|
-
import sys
|
6
10
|
import typing as ta
|
7
11
|
|
8
|
-
from ...lite.
|
12
|
+
from ...lite.abstract import Abstract
|
9
13
|
from ..contexts import LoggingContext
|
10
|
-
from ..contexts import
|
14
|
+
from ..contexts import LoggingContextInfoT
|
15
|
+
from ..infos import LoggingContextInfo
|
16
|
+
from ..infos import LoggingContextInfos
|
17
|
+
from ..infos import LoggingExcInfoTuple
|
11
18
|
from ..warnings import LoggingSetupWarning
|
12
19
|
|
13
20
|
|
21
|
+
T = ta.TypeVar('T')
|
22
|
+
|
23
|
+
|
14
24
|
##
|
15
25
|
|
16
26
|
|
17
|
-
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
# - https://github.com/python/cpython/blob/
|
23
|
-
#
|
24
|
-
#
|
25
|
-
# - name: str
|
26
|
-
# - level: int
|
27
|
-
# - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
|
28
|
-
# - lineno: int - May be 0.
|
29
|
-
# - msg: str
|
30
|
-
# - args: tuple | dict | 1-tuple[dict]
|
31
|
-
# - exc_info: LoggingExcInfoTuple | None
|
32
|
-
# - func: str | None = None -> funcName
|
33
|
-
# - sinfo: str | None = None -> stack_info
|
34
|
-
#
|
35
|
-
KNOWN_STD_LOGGING_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
|
36
|
-
# Name of the logger used to log the call. Unmodified by ctor.
|
37
|
-
name=str,
|
27
|
+
class LoggingContextInfoRecordAdapters:
|
28
|
+
# Ref:
|
29
|
+
# - https://docs.python.org/3/library/logging.html#logrecord-attributes
|
30
|
+
#
|
31
|
+
# LogRecord:
|
32
|
+
# - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L276 (3.8) # noqa
|
33
|
+
# - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L286 (~3.14) # noqa
|
34
|
+
#
|
38
35
|
|
39
|
-
|
40
|
-
|
41
|
-
|
36
|
+
def __new__(cls, *args, **kwargs): # noqa
|
37
|
+
raise TypeError
|
38
|
+
|
39
|
+
class Adapter(Abstract, ta.Generic[T]):
|
40
|
+
@property
|
41
|
+
@abc.abstractmethod
|
42
|
+
def info_cls(self) -> ta.Type[LoggingContextInfo]:
|
43
|
+
raise NotImplementedError
|
44
|
+
|
45
|
+
#
|
46
|
+
|
47
|
+
@ta.final
|
48
|
+
class NOT_SET: # noqa
|
49
|
+
def __new__(cls, *args, **kwargs): # noqa
|
50
|
+
raise TypeError
|
51
|
+
|
52
|
+
class RecordAttr(ta.NamedTuple):
|
53
|
+
name: str
|
54
|
+
type: ta.Any
|
55
|
+
default: ta.Any
|
56
|
+
|
57
|
+
# @abc.abstractmethod
|
58
|
+
record_attrs: ta.ClassVar[ta.Mapping[str, RecordAttr]]
|
59
|
+
|
60
|
+
@property
|
61
|
+
@abc.abstractmethod
|
62
|
+
def _record_attrs(self) -> ta.Union[
|
63
|
+
ta.Mapping[str, ta.Any],
|
64
|
+
ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]],
|
65
|
+
]:
|
66
|
+
raise NotImplementedError
|
67
|
+
|
68
|
+
#
|
69
|
+
|
70
|
+
@abc.abstractmethod
|
71
|
+
def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
|
72
|
+
raise NotImplementedError
|
73
|
+
|
74
|
+
#
|
75
|
+
|
76
|
+
@abc.abstractmethod
|
77
|
+
def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[T]:
|
78
|
+
raise NotImplementedError
|
79
|
+
|
80
|
+
#
|
81
|
+
|
82
|
+
def __init_subclass__(cls, **kwargs: ta.Any) -> None:
|
83
|
+
super().__init_subclass__(**kwargs)
|
84
|
+
|
85
|
+
if Abstract in cls.__bases__:
|
86
|
+
return
|
87
|
+
|
88
|
+
if 'record_attrs' in cls.__dict__:
|
89
|
+
raise TypeError(cls)
|
90
|
+
if not isinstance(ra := cls.__dict__['_record_attrs'], collections.abc.Mapping):
|
91
|
+
raise TypeError(ra)
|
92
|
+
|
93
|
+
rd: ta.Dict[str, LoggingContextInfoRecordAdapters.Adapter.RecordAttr] = {}
|
94
|
+
for n, v in ra.items():
|
95
|
+
if not n or not isinstance(n, str) or n in rd:
|
96
|
+
raise AttributeError(n)
|
97
|
+
if isinstance(v, tuple):
|
98
|
+
t, d = v
|
99
|
+
else:
|
100
|
+
t, d = v, cls.NOT_SET
|
101
|
+
rd[n] = cls.RecordAttr(
|
102
|
+
name=n,
|
103
|
+
type=t,
|
104
|
+
default=d,
|
105
|
+
)
|
106
|
+
cls.record_attrs = rd
|
107
|
+
|
108
|
+
class RequiredAdapter(Adapter[T], Abstract):
|
109
|
+
@property
|
110
|
+
@abc.abstractmethod
|
111
|
+
def _record_attrs(self) -> ta.Mapping[str, ta.Any]:
|
112
|
+
raise NotImplementedError
|
113
|
+
|
114
|
+
#
|
115
|
+
|
116
|
+
@ta.final
|
117
|
+
def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
|
118
|
+
if (info := ctx.get_info(self.info_cls)) is not None:
|
119
|
+
return self._info_to_record(info)
|
120
|
+
else:
|
121
|
+
raise TypeError # FIXME: fallback?
|
42
122
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
args=ta.Union[tuple, dict],
|
123
|
+
@abc.abstractmethod
|
124
|
+
def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
|
125
|
+
raise NotImplementedError
|
47
126
|
|
48
|
-
|
127
|
+
#
|
49
128
|
|
50
|
-
|
51
|
-
|
52
|
-
|
129
|
+
@abc.abstractmethod
|
130
|
+
def record_to_info(self, rec: logging.LogRecord) -> T:
|
131
|
+
raise NotImplementedError
|
53
132
|
|
54
|
-
|
55
|
-
levelno=int,
|
133
|
+
#
|
56
134
|
|
57
|
-
|
135
|
+
def __init_subclass__(cls, **kwargs: ta.Any) -> None:
|
136
|
+
super().__init_subclass__(**kwargs)
|
58
137
|
|
59
|
-
|
60
|
-
|
61
|
-
pathname=str,
|
138
|
+
if any(a.default is not cls.NOT_SET for a in cls.record_attrs.values()):
|
139
|
+
raise TypeError(cls.record_attrs)
|
62
140
|
|
63
|
-
|
64
|
-
|
141
|
+
class OptionalAdapter(Adapter[T], Abstract, ta.Generic[T]):
|
142
|
+
@property
|
143
|
+
@abc.abstractmethod
|
144
|
+
def _record_attrs(self) -> ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]:
|
145
|
+
raise NotImplementedError
|
65
146
|
|
66
|
-
|
67
|
-
# "Unknown module".
|
68
|
-
module=str,
|
147
|
+
record_defaults: ta.ClassVar[ta.Mapping[str, ta.Any]]
|
69
148
|
|
70
|
-
|
149
|
+
#
|
150
|
+
|
151
|
+
@ta.final
|
152
|
+
def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
|
153
|
+
if (info := ctx.get_info(self.info_cls)) is not None:
|
154
|
+
return self._info_to_record(info)
|
155
|
+
else:
|
156
|
+
return self.record_defaults
|
71
157
|
|
72
|
-
|
73
|
-
|
158
|
+
@abc.abstractmethod
|
159
|
+
def _info_to_record(self, info: T) -> ta.Mapping[str, ta.Any]:
|
160
|
+
raise NotImplementedError
|
74
161
|
|
75
|
-
|
76
|
-
|
162
|
+
#
|
163
|
+
|
164
|
+
def __init_subclass__(cls, **kwargs: ta.Any) -> None:
|
165
|
+
super().__init_subclass__(**kwargs)
|
166
|
+
|
167
|
+
dd: ta.Dict[str, ta.Any] = {a.name: a.default for a in cls.record_attrs.values()}
|
168
|
+
if any(d is cls.NOT_SET for d in dd.values()):
|
169
|
+
raise TypeError(cls.record_attrs)
|
170
|
+
cls.record_defaults = dd
|
77
171
|
|
78
172
|
#
|
79
173
|
|
80
|
-
|
81
|
-
|
82
|
-
# unmodified. Mostly set, if requested, by `Logger.findCaller`, to `traceback.print_stack(f)`, but prepended with
|
83
|
-
# the literal "Stack (most recent call last):\n", and stripped of exactly one trailing `\n` if present.
|
84
|
-
stack_info=ta.Optional[str],
|
174
|
+
class Name(RequiredAdapter[LoggingContextInfos.Name]):
|
175
|
+
info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Name]] = LoggingContextInfos.Name
|
85
176
|
|
86
|
-
|
87
|
-
|
88
|
-
|
177
|
+
_record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
|
178
|
+
# Name of the logger used to log the call. Unmodified by ctor.
|
179
|
+
name=str,
|
180
|
+
)
|
89
181
|
|
90
|
-
|
91
|
-
|
92
|
-
|
182
|
+
def _info_to_record(self, info: LoggingContextInfos.Name) -> ta.Mapping[str, ta.Any]:
|
183
|
+
return dict(
|
184
|
+
name=info.name,
|
185
|
+
)
|
93
186
|
|
94
|
-
|
187
|
+
def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Name:
|
188
|
+
return LoggingContextInfos.Name(
|
189
|
+
name=rec.name,
|
190
|
+
)
|
95
191
|
|
96
|
-
|
97
|
-
|
98
|
-
# See:
|
99
|
-
# - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
|
100
|
-
# - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
|
101
|
-
#
|
102
|
-
created=float,
|
192
|
+
class Level(RequiredAdapter[LoggingContextInfos.Level]):
|
193
|
+
info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Level]] = LoggingContextInfos.Level
|
103
194
|
|
104
|
-
|
105
|
-
|
195
|
+
_record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
|
196
|
+
# Text logging level for the message ('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). Set to
|
197
|
+
# `getLevelName(level)`.
|
198
|
+
levelname=str,
|
106
199
|
|
107
|
-
|
108
|
-
|
200
|
+
# Numeric logging level for the message (DEBUG, INFO, WARNING, ERROR, CRITICAL). Unmodified by ctor.
|
201
|
+
levelno=int,
|
202
|
+
)
|
109
203
|
|
110
|
-
|
204
|
+
def _info_to_record(self, info: LoggingContextInfos.Level) -> ta.Mapping[str, ta.Any]:
|
205
|
+
return dict(
|
206
|
+
levelname=info.name,
|
207
|
+
levelno=int(info.level),
|
208
|
+
)
|
111
209
|
|
112
|
-
|
113
|
-
|
210
|
+
def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Level:
|
211
|
+
return LoggingContextInfos.Level.build(rec.levelno)
|
114
212
|
|
115
|
-
|
116
|
-
|
213
|
+
class Msg(RequiredAdapter[LoggingContextInfos.Msg]):
|
214
|
+
info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Msg]] = LoggingContextInfos.Msg
|
117
215
|
|
118
|
-
|
216
|
+
_record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
|
217
|
+
# The format string passed in the original logging call. Merged with args to produce message, or an
|
218
|
+
# arbitrary object (see Using arbitrary objects as messages). Unmodified by ctor.
|
219
|
+
msg=str,
|
119
220
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
221
|
+
# The tuple of arguments merged into msg to produce message, or a dict whose values are used for the merge
|
222
|
+
# (when there is only one argument, and it is a dictionary). Ctor will transform a 1-tuple containing a
|
223
|
+
# Mapping into just the mapping, but is otherwise unmodified.
|
224
|
+
args=ta.Union[tuple, dict, None],
|
225
|
+
)
|
226
|
+
|
227
|
+
def _info_to_record(self, info: LoggingContextInfos.Msg) -> ta.Mapping[str, ta.Any]:
|
228
|
+
return dict(
|
229
|
+
msg=info.msg,
|
230
|
+
args=info.args,
|
231
|
+
)
|
232
|
+
|
233
|
+
def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Msg:
|
234
|
+
return LoggingContextInfos.Msg(
|
235
|
+
msg=rec.msg,
|
236
|
+
args=rec.args,
|
237
|
+
)
|
238
|
+
|
239
|
+
# FIXME: handled specially - all unknown attrs on LogRecord
|
240
|
+
# class Extra(Adapter[LoggingContextInfos.Extra]):
|
241
|
+
# _record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict()
|
125
242
|
#
|
126
|
-
#
|
127
|
-
#
|
243
|
+
# def info_to_record(self, info: ta.Optional[LoggingContextInfos.Extra]) -> ta.Mapping[str, ta.Any]:
|
244
|
+
# # FIXME:
|
245
|
+
# # if extra is not None:
|
246
|
+
# # for key in extra:
|
247
|
+
# # if (key in ["message", "asctime"]) or (key in rv.__dict__):
|
248
|
+
# # raise KeyError("Attempt to overwrite %r in LogRecord" % key)
|
249
|
+
# # rv.__dict__[key] = extra[key]
|
250
|
+
# return dict()
|
128
251
|
#
|
129
|
-
|
252
|
+
# def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Extra]:
|
253
|
+
# return None
|
254
|
+
|
255
|
+
class Time(RequiredAdapter[LoggingContextInfos.Time]):
|
256
|
+
info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Time]] = LoggingContextInfos.Time
|
257
|
+
|
258
|
+
_record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
|
259
|
+
# Time when the LogRecord was created. Set to `time.time_ns() / 1e9` for >=3.13.0b1, otherwise simply
|
260
|
+
# `time.time()`.
|
261
|
+
#
|
262
|
+
# See:
|
263
|
+
# - https://github.com/python/cpython/commit/1316692e8c7c1e1f3b6639e51804f9db5ed892ea
|
264
|
+
# - https://github.com/python/cpython/commit/1500a23f33f5a6d052ff1ef6383d9839928b8ff1
|
265
|
+
#
|
266
|
+
created=float,
|
267
|
+
|
268
|
+
# Millisecond portion of the time when the LogRecord was created.
|
269
|
+
msecs=float,
|
270
|
+
|
271
|
+
# Time in milliseconds when the LogRecord was created, relative to the time the logging module was loaded.
|
272
|
+
relativeCreated=float,
|
273
|
+
)
|
130
274
|
|
131
|
-
|
132
|
-
|
133
|
-
|
275
|
+
def _info_to_record(self, info: LoggingContextInfos.Time) -> ta.Mapping[str, ta.Any]:
|
276
|
+
return dict(
|
277
|
+
created=info.secs,
|
278
|
+
msecs=info.msecs,
|
279
|
+
relativeCreated=info.relative_secs,
|
280
|
+
)
|
134
281
|
|
135
|
-
|
282
|
+
def record_to_info(self, rec: logging.LogRecord) -> LoggingContextInfos.Time:
|
283
|
+
return LoggingContextInfos.Time.build(
|
284
|
+
int(rec.created * 1e9),
|
285
|
+
)
|
136
286
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
287
|
+
class Exc(OptionalAdapter[LoggingContextInfos.Exc]):
|
288
|
+
info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Exc]] = LoggingContextInfos.Exc
|
289
|
+
|
290
|
+
_record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
|
291
|
+
# Exception tuple (à la sys.exc_info) or, if no exception has occurred, None. Unmodified by ctor.
|
292
|
+
exc_info=(ta.Optional[LoggingExcInfoTuple], None),
|
293
|
+
|
294
|
+
# Used to cache the traceback text. Simply set to None by ctor, later set by Formatter.format.
|
295
|
+
exc_text=(ta.Optional[str], None),
|
296
|
+
)
|
297
|
+
|
298
|
+
def _info_to_record(self, info: LoggingContextInfos.Exc) -> ta.Mapping[str, ta.Any]:
|
299
|
+
return dict(
|
300
|
+
exc_info=info.info_tuple,
|
301
|
+
exc_text=None,
|
302
|
+
)
|
303
|
+
|
304
|
+
def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Exc]:
|
305
|
+
# FIXME:
|
306
|
+
# error: Argument 1 to "build" of "Exc" has incompatible type
|
307
|
+
# "tuple[type[BaseException], BaseException, TracebackType | None] | tuple[None, None, None] | None"; expected # noqa
|
308
|
+
# "BaseException | tuple[type[BaseException], BaseException, TracebackType | None] | bool | None" [arg-type] # noqa
|
309
|
+
return LoggingContextInfos.Exc.build(rec.exc_info) # type: ignore[arg-type]
|
310
|
+
|
311
|
+
class Caller(OptionalAdapter[LoggingContextInfos.Caller]):
|
312
|
+
info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Caller]] = LoggingContextInfos.Caller
|
313
|
+
|
314
|
+
_UNKNOWN_PATH_NAME: ta.ClassVar[str] = '(unknown file)'
|
315
|
+
_UNKNOWN_FUNC_NAME: ta.ClassVar[str] = '(unknown function)'
|
316
|
+
|
317
|
+
_STACK_INFO_PREFIX: ta.ClassVar[str] = 'Stack (most recent call last):\n'
|
318
|
+
|
319
|
+
_record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
|
320
|
+
# Full pathname of the source file where the logging call was issued (if available). Unmodified by ctor. May
|
321
|
+
# default to "(unknown file)" by Logger.findCaller / Logger._log.
|
322
|
+
pathname=(str, _UNKNOWN_PATH_NAME),
|
323
|
+
|
324
|
+
# Source line number where the logging call was issued (if available). Unmodified by ctor. May default to 0
|
325
|
+
# y Logger.findCaller / Logger._log.
|
326
|
+
lineno=(int, 0),
|
327
|
+
|
328
|
+
# Name of function containing the logging call. Set by ctor to `func` arg, unmodified. May default to
|
329
|
+
# "(unknown function)" by Logger.findCaller / Logger._log.
|
330
|
+
funcName=(str, _UNKNOWN_FUNC_NAME),
|
331
|
+
|
332
|
+
# Stack frame information (where available) from the bottom of the stack in the current thread, up to and
|
333
|
+
# including the stack frame of the logging call which resulted in the creation of this record. Set by ctor
|
334
|
+
# to `sinfo` arg, unmodified. Mostly set, if requested, by `Logger.findCaller`, to
|
335
|
+
# `traceback.print_stack(f)`, but prepended with the literal "Stack (most recent call last):\n", and
|
336
|
+
# stripped of exactly one trailing `\n` if present.
|
337
|
+
stack_info=(ta.Optional[str], None),
|
338
|
+
)
|
339
|
+
|
340
|
+
def _info_to_record(self, caller: LoggingContextInfos.Caller) -> ta.Mapping[str, ta.Any]:
|
341
|
+
if (sinfo := caller.stack_info) is not None:
|
342
|
+
stack_info: ta.Optional[str] = '\n'.join([
|
343
|
+
self._STACK_INFO_PREFIX,
|
344
|
+
sinfo[1:] if sinfo.endswith('\n') else sinfo,
|
345
|
+
])
|
346
|
+
else:
|
347
|
+
stack_info = None
|
348
|
+
|
349
|
+
return dict(
|
350
|
+
pathname=caller.file_path,
|
351
|
+
|
352
|
+
lineno=caller.line_no,
|
353
|
+
funcName=caller.func_name,
|
354
|
+
|
355
|
+
stack_info=stack_info,
|
356
|
+
)
|
357
|
+
|
358
|
+
def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Caller]:
|
359
|
+
# FIXME: piecemeal?
|
360
|
+
if (
|
361
|
+
rec.pathname != self._UNKNOWN_PATH_NAME and
|
362
|
+
rec.lineno != 0 and
|
363
|
+
rec.funcName != self._UNKNOWN_FUNC_NAME
|
364
|
+
):
|
365
|
+
if (sinfo := rec.stack_info) is not None and sinfo.startswith(self._STACK_INFO_PREFIX):
|
366
|
+
sinfo = sinfo[len(self._STACK_INFO_PREFIX):]
|
367
|
+
return LoggingContextInfos.Caller(
|
368
|
+
file_path=rec.pathname,
|
369
|
+
|
370
|
+
line_no=rec.lineno,
|
371
|
+
func_name=rec.funcName,
|
372
|
+
|
373
|
+
stack_info=sinfo,
|
374
|
+
)
|
375
|
+
|
376
|
+
return None
|
377
|
+
|
378
|
+
class SourceFile(Adapter[LoggingContextInfos.SourceFile]):
|
379
|
+
info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.SourceFile]] = LoggingContextInfos.SourceFile
|
141
380
|
|
142
|
-
|
381
|
+
_record_attrs: ta.ClassVar[ta.Mapping[str, ta.Any]] = dict(
|
382
|
+
# Filename portion of pathname. Set to `os.path.basename(pathname)` if successful, otherwise defaults to
|
383
|
+
# pathname.
|
384
|
+
filename=str,
|
385
|
+
|
386
|
+
# Module (name portion of filename). Set to `os.path.splitext(filename)[0]`, otherwise defaults to
|
387
|
+
# "Unknown module".
|
388
|
+
module=str,
|
389
|
+
)
|
390
|
+
|
391
|
+
_UNKNOWN_MODULE: ta.ClassVar[str] = 'Unknown module'
|
392
|
+
|
393
|
+
def context_to_record(self, ctx: LoggingContext) -> ta.Mapping[str, ta.Any]:
|
394
|
+
if (info := ctx.get_info(LoggingContextInfos.SourceFile)) is not None:
|
395
|
+
return dict(
|
396
|
+
filename=info.file_name,
|
397
|
+
module=info.module,
|
398
|
+
)
|
399
|
+
|
400
|
+
if (caller := ctx.get_info(LoggingContextInfos.Caller)) is not None:
|
401
|
+
return dict(
|
402
|
+
filename=caller.file_path,
|
403
|
+
module=self._UNKNOWN_MODULE,
|
404
|
+
)
|
405
|
+
|
406
|
+
return dict(
|
407
|
+
filename=LoggingContextInfoRecordAdapters.Caller._UNKNOWN_PATH_NAME, # noqa
|
408
|
+
module=self._UNKNOWN_MODULE,
|
409
|
+
)
|
410
|
+
|
411
|
+
def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.SourceFile]:
|
412
|
+
if (
|
413
|
+
rec.module is not None and
|
414
|
+
rec.module != self._UNKNOWN_MODULE
|
415
|
+
):
|
416
|
+
return LoggingContextInfos.SourceFile(
|
417
|
+
file_name=rec.filename,
|
418
|
+
module=rec.module, # FIXME: piecemeal?
|
419
|
+
)
|
420
|
+
|
421
|
+
return None
|
422
|
+
|
423
|
+
class Thread(OptionalAdapter[LoggingContextInfos.Thread]):
|
424
|
+
info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Thread]] = LoggingContextInfos.Thread
|
425
|
+
|
426
|
+
_record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
|
427
|
+
# Thread ID if available, and `logging.logThreads` is truthy.
|
428
|
+
thread=(ta.Optional[int], None),
|
429
|
+
|
430
|
+
# Thread name if available, and `logging.logThreads` is truthy.
|
431
|
+
threadName=(ta.Optional[str], None),
|
432
|
+
)
|
433
|
+
|
434
|
+
def _info_to_record(self, info: LoggingContextInfos.Thread) -> ta.Mapping[str, ta.Any]:
|
435
|
+
if logging.logThreads:
|
436
|
+
return dict(
|
437
|
+
thread=info.ident,
|
438
|
+
threadName=info.name,
|
439
|
+
)
|
440
|
+
|
441
|
+
return self.record_defaults
|
442
|
+
|
443
|
+
def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Thread]:
|
444
|
+
if (
|
445
|
+
(ident := rec.thread) is not None and
|
446
|
+
(name := rec.threadName) is not None
|
447
|
+
):
|
448
|
+
return LoggingContextInfos.Thread(
|
449
|
+
ident=ident,
|
450
|
+
native_id=None,
|
451
|
+
name=name,
|
452
|
+
)
|
453
|
+
|
454
|
+
return None
|
455
|
+
|
456
|
+
class Process(OptionalAdapter[LoggingContextInfos.Process]):
|
457
|
+
info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Process]] = LoggingContextInfos.Process
|
458
|
+
|
459
|
+
_record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
|
460
|
+
# Process ID if available - that is, if `hasattr(os, 'getpid')` - and `logging.logProcesses` is truthy,
|
461
|
+
# otherwise None.
|
462
|
+
process=(ta.Optional[int], None),
|
463
|
+
)
|
464
|
+
|
465
|
+
def _info_to_record(self, info: LoggingContextInfos.Process) -> ta.Mapping[str, ta.Any]:
|
466
|
+
if logging.logProcesses:
|
467
|
+
return dict(
|
468
|
+
process=info.pid,
|
469
|
+
)
|
470
|
+
|
471
|
+
return self.record_defaults
|
472
|
+
|
473
|
+
def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Process]:
|
474
|
+
if (
|
475
|
+
(pid := rec.process) is not None
|
476
|
+
):
|
477
|
+
return LoggingContextInfos.Process(
|
478
|
+
pid=pid,
|
479
|
+
)
|
480
|
+
|
481
|
+
return None
|
482
|
+
|
483
|
+
class Multiprocessing(OptionalAdapter[LoggingContextInfos.Multiprocessing]):
|
484
|
+
info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.Multiprocessing]] = LoggingContextInfos.Multiprocessing
|
485
|
+
|
486
|
+
_record_attrs: ta.ClassVar[ta.Mapping[str, ta.Tuple[ta.Any, ta.Any]]] = dict(
|
487
|
+
# Process name if available. Set to None if `logging.logMultiprocessing` is not truthy. Otherwise, set to
|
488
|
+
# 'MainProcess', then `sys.modules.get('multiprocessing').current_process().name` if that works, otherwise
|
489
|
+
# remains as 'MainProcess'.
|
490
|
+
#
|
491
|
+
# As noted by stdlib:
|
492
|
+
#
|
493
|
+
# Errors may occur if multiprocessing has not finished loading yet - e.g. if a custom import hook causes
|
494
|
+
# third-party code to run when multiprocessing calls import. See issue 8200 for an example
|
495
|
+
#
|
496
|
+
processName=(ta.Optional[str], None),
|
497
|
+
)
|
498
|
+
|
499
|
+
def _info_to_record(self, info: LoggingContextInfos.Multiprocessing) -> ta.Mapping[str, ta.Any]:
|
500
|
+
if logging.logMultiprocessing:
|
501
|
+
return dict(
|
502
|
+
processName=info.process_name,
|
503
|
+
)
|
504
|
+
|
505
|
+
return self.record_defaults
|
506
|
+
|
507
|
+
def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.Multiprocessing]:
|
508
|
+
if (
|
509
|
+
(process_name := rec.processName) is not None
|
510
|
+
):
|
511
|
+
return LoggingContextInfos.Multiprocessing(
|
512
|
+
process_name=process_name,
|
513
|
+
)
|
514
|
+
|
515
|
+
return None
|
516
|
+
|
517
|
+
class AsyncioTask(OptionalAdapter[LoggingContextInfos.AsyncioTask]):
|
518
|
+
info_cls: ta.ClassVar[ta.Type[LoggingContextInfos.AsyncioTask]] = LoggingContextInfos.AsyncioTask
|
519
|
+
|
520
|
+
_record_attrs: ta.ClassVar[ta.Mapping[str, ta.Union[ta.Any, ta.Tuple[ta.Any, ta.Any]]]] = dict(
|
521
|
+
# Absent <3.12, otherwise asyncio.Task name if available, and `logging.logAsyncioTasks` is truthy. Set to
|
522
|
+
# `sys.modules.get('asyncio').current_task().get_name()`, otherwise None.
|
523
|
+
taskName=(ta.Optional[str], None),
|
524
|
+
)
|
525
|
+
|
526
|
+
def _info_to_record(self, info: LoggingContextInfos.AsyncioTask) -> ta.Mapping[str, ta.Any]:
|
527
|
+
if getattr(logging, 'logAsyncioTasks', None): # Absent <3.12
|
528
|
+
return dict(
|
529
|
+
taskName=info.name,
|
530
|
+
)
|
531
|
+
|
532
|
+
return self.record_defaults
|
533
|
+
|
534
|
+
def record_to_info(self, rec: logging.LogRecord) -> ta.Optional[LoggingContextInfos.AsyncioTask]:
|
535
|
+
if (
|
536
|
+
(name := getattr(rec, 'taskName', None)) is not None
|
537
|
+
):
|
538
|
+
return LoggingContextInfos.AsyncioTask(
|
539
|
+
name=name,
|
540
|
+
)
|
541
|
+
|
542
|
+
return None
|
543
|
+
|
544
|
+
|
545
|
+
_LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_: ta.Sequence[LoggingContextInfoRecordAdapters.Adapter] = [ # noqa
|
546
|
+
LoggingContextInfoRecordAdapters.Name(),
|
547
|
+
LoggingContextInfoRecordAdapters.Level(),
|
548
|
+
LoggingContextInfoRecordAdapters.Msg(),
|
549
|
+
LoggingContextInfoRecordAdapters.Time(),
|
550
|
+
LoggingContextInfoRecordAdapters.Exc(),
|
551
|
+
LoggingContextInfoRecordAdapters.Caller(),
|
552
|
+
LoggingContextInfoRecordAdapters.SourceFile(),
|
553
|
+
LoggingContextInfoRecordAdapters.Thread(),
|
554
|
+
LoggingContextInfoRecordAdapters.Process(),
|
555
|
+
LoggingContextInfoRecordAdapters.Multiprocessing(),
|
556
|
+
LoggingContextInfoRecordAdapters.AsyncioTask(),
|
557
|
+
]
|
558
|
+
|
559
|
+
_LOGGING_CONTEXT_INFO_RECORD_ADAPTERS: ta.Mapping[ta.Type[LoggingContextInfo], LoggingContextInfoRecordAdapters.Adapter] = { # noqa
|
560
|
+
ad.info_cls: ad for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_
|
561
|
+
}
|
562
|
+
|
563
|
+
|
564
|
+
##
|
143
565
|
|
144
566
|
|
145
567
|
# Formatter:
|
146
568
|
# - https://github.com/python/cpython/blob/39b2f82717a69dde7212bc39b673b0f55c99e6a3/Lib/logging/__init__.py#L514 (3.8)
|
147
569
|
# - https://github.com/python/cpython/blob/f070f54c5f4a42c7c61d1d5d3b8f3b7203b4a0fb/Lib/logging/__init__.py#L554 (~3.14) # noqa
|
148
570
|
#
|
149
|
-
|
571
|
+
_KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
|
150
572
|
# The logged message, computed as msg % args. Set to `record.getMessage()`.
|
151
573
|
message=str,
|
152
574
|
|
@@ -160,20 +582,31 @@ KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS: ta.Dict[str, ta.Any] = dict(
|
|
160
582
|
exc_text=ta.Optional[str],
|
161
583
|
)
|
162
584
|
|
163
|
-
KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS)
|
164
|
-
|
165
585
|
|
166
586
|
##
|
167
587
|
|
168
588
|
|
589
|
+
_KNOWN_STD_LOGGING_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(
|
590
|
+
a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs
|
591
|
+
)
|
592
|
+
|
593
|
+
_KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTR_SET: ta.FrozenSet[str] = frozenset(_KNOWN_STD_LOGGING_FORMATTER_RECORD_ATTRS)
|
594
|
+
|
595
|
+
|
169
596
|
class UnknownStdLoggingRecordAttrsWarning(LoggingSetupWarning):
|
170
597
|
pass
|
171
598
|
|
172
599
|
|
173
600
|
def _check_std_logging_record_attrs() -> None:
|
601
|
+
if (
|
602
|
+
len([a for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS.values() for a in ad.record_attrs]) !=
|
603
|
+
len(_KNOWN_STD_LOGGING_RECORD_ATTR_SET)
|
604
|
+
):
|
605
|
+
raise RuntimeError('Duplicate LoggingContextInfoRecordAdapter record attrs')
|
606
|
+
|
174
607
|
rec_dct = dict(logging.makeLogRecord({}).__dict__)
|
175
608
|
|
176
|
-
if (unk_rec_fields := frozenset(rec_dct) -
|
609
|
+
if (unk_rec_fields := frozenset(rec_dct) - _KNOWN_STD_LOGGING_RECORD_ATTR_SET):
|
177
610
|
import warnings # noqa
|
178
611
|
|
179
612
|
warnings.warn(
|
@@ -189,113 +622,40 @@ _check_std_logging_record_attrs()
|
|
189
622
|
|
190
623
|
|
191
624
|
class LoggingContextLogRecord(logging.LogRecord):
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
self.
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
self.
|
229
|
-
|
230
|
-
self.levelname: str = logging.getLevelName(ctx.level)
|
231
|
-
self.levelno: int = ctx.level
|
232
|
-
|
233
|
-
if (caller := ctx.caller()) is not None:
|
234
|
-
self.pathname: str = caller.file_path
|
235
|
-
else:
|
236
|
-
self.pathname = self._UNKNOWN_PATH_NAME
|
237
|
-
|
238
|
-
if (src_file := ctx.source_file()) is not None:
|
239
|
-
self.filename: str = src_file.file_name
|
240
|
-
self.module: str = src_file.module
|
241
|
-
else:
|
242
|
-
self.filename = self.pathname
|
243
|
-
self.module = self._UNKNOWN_MODULE
|
244
|
-
|
245
|
-
self.exc_info: ta.Optional[LoggingExcInfoTuple] = ctx.exc_info_tuple
|
246
|
-
self.exc_text: ta.Optional[str] = None
|
247
|
-
|
248
|
-
# If ctx.build_caller() was never called, we simply don't have a stack trace.
|
249
|
-
if caller is not None:
|
250
|
-
if (sinfo := caller.stack_info) is not None:
|
251
|
-
self.stack_info: ta.Optional[str] = '\n'.join([
|
252
|
-
self._STACK_INFO_PREFIX,
|
253
|
-
sinfo[1:] if sinfo.endswith('\n') else sinfo,
|
254
|
-
])
|
255
|
-
else:
|
256
|
-
self.stack_info = None
|
257
|
-
|
258
|
-
self.lineno: int = caller.line_no
|
259
|
-
self.funcName: str = caller.name
|
260
|
-
|
261
|
-
else:
|
262
|
-
self.stack_info = None
|
263
|
-
|
264
|
-
self.lineno = 0
|
265
|
-
self.funcName = self._UNKNOWN_FUNC_NAME
|
266
|
-
|
267
|
-
times = ctx.times
|
268
|
-
self.created: float = times.created
|
269
|
-
self.msecs: float = times.msecs
|
270
|
-
self.relativeCreated: float = times.relative_created
|
271
|
-
|
272
|
-
if logging.logThreads:
|
273
|
-
thread = check.not_none(ctx.thread())
|
274
|
-
self.thread: ta.Optional[int] = thread.ident
|
275
|
-
self.threadName: ta.Optional[str] = thread.name
|
276
|
-
else:
|
277
|
-
self.thread = None
|
278
|
-
self.threadName = None
|
279
|
-
|
280
|
-
if logging.logProcesses:
|
281
|
-
process = check.not_none(ctx.process())
|
282
|
-
self.process: ta.Optional[int] = process.pid
|
283
|
-
else:
|
284
|
-
self.process = None
|
285
|
-
|
286
|
-
if logging.logMultiprocessing:
|
287
|
-
if (mp := ctx.multiprocessing()) is not None:
|
288
|
-
self.processName: ta.Optional[str] = mp.process_name
|
289
|
-
else:
|
290
|
-
self.processName = None
|
291
|
-
else:
|
292
|
-
self.processName = None
|
293
|
-
|
294
|
-
# Absent <3.12
|
295
|
-
if getattr(logging, 'logAsyncioTasks', None):
|
296
|
-
if (at := ctx.asyncio_task()) is not None:
|
297
|
-
self.taskName: ta.Optional[str] = at.name
|
298
|
-
else:
|
299
|
-
self.taskName = None
|
300
|
-
else:
|
301
|
-
self.taskName = None
|
625
|
+
# LogRecord.__init__ args:
|
626
|
+
# - name: str
|
627
|
+
# - level: int
|
628
|
+
# - pathname: str - Confusingly referred to as `fn` before the LogRecord ctor. May be empty or "(unknown file)".
|
629
|
+
# - lineno: int - May be 0.
|
630
|
+
# - msg: str
|
631
|
+
# - args: tuple | dict | 1-tuple[dict]
|
632
|
+
# - exc_info: LoggingExcInfoTuple | None
|
633
|
+
# - func: str | None = None -> funcName
|
634
|
+
# - sinfo: str | None = None -> stack_info
|
635
|
+
|
636
|
+
def __init__(self, *, _logging_context: LoggingContext) -> None: # noqa
|
637
|
+
self._logging_context = _logging_context
|
638
|
+
|
639
|
+
for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_:
|
640
|
+
self.__dict__.update(ad.context_to_record(_logging_context))
|
641
|
+
|
642
|
+
|
643
|
+
##
|
644
|
+
|
645
|
+
|
646
|
+
@ta.final
|
647
|
+
class LogRecordLoggingContext(LoggingContext):
|
648
|
+
def __init__(self, rec: logging.LogRecord) -> None:
|
649
|
+
if isinstance(rec, LoggingContextLogRecord):
|
650
|
+
raise TypeError(rec)
|
651
|
+
|
652
|
+
self._rec = rec
|
653
|
+
|
654
|
+
self._infos: ta.Dict[ta.Type[LoggingContextInfo], LoggingContextInfo] = {
|
655
|
+
type(info): info
|
656
|
+
for ad in _LOGGING_CONTEXT_INFO_RECORD_ADAPTERS_
|
657
|
+
if (info := ad.record_to_info(rec)) is not None
|
658
|
+
}
|
659
|
+
|
660
|
+
def get_info(self, ty: ta.Type[LoggingContextInfoT]) -> ta.Optional[LoggingContextInfoT]:
|
661
|
+
return self._infos.get(ty)
|