tremors 0.4.2__tar.gz → 0.5.0__tar.gz
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.
- {tremors-0.4.2/src/tremors.egg-info → tremors-0.5.0}/PKG-INFO +52 -4
- {tremors-0.4.2 → tremors-0.5.0}/README.rst +49 -1
- {tremors-0.4.2 → tremors-0.5.0}/pyproject.toml +2 -2
- {tremors-0.4.2 → tremors-0.5.0}/src/tremors/__init__.py +1 -1
- {tremors-0.4.2 → tremors-0.5.0}/src/tremors/collector.py +147 -30
- {tremors-0.4.2 → tremors-0.5.0}/src/tremors/decorator.py +14 -9
- {tremors-0.4.2 → tremors-0.5.0}/src/tremors/logger.py +37 -5
- {tremors-0.4.2 → tremors-0.5.0/src/tremors.egg-info}/PKG-INFO +52 -4
- {tremors-0.4.2 → tremors-0.5.0}/src/tremors.egg-info/requires.txt +2 -2
- {tremors-0.4.2 → tremors-0.5.0}/LICENSE +0 -0
- {tremors-0.4.2 → tremors-0.5.0}/setup.cfg +0 -0
- {tremors-0.4.2 → tremors-0.5.0}/src/tremors/py.typed +0 -0
- {tremors-0.4.2 → tremors-0.5.0}/src/tremors.egg-info/SOURCES.txt +0 -0
- {tremors-0.4.2 → tremors-0.5.0}/src/tremors.egg-info/dependency_links.txt +0 -0
- {tremors-0.4.2 → tremors-0.5.0}/src/tremors.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tremors
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Tremors is a library for logging while collecting metrics.
|
|
5
5
|
Author-email: Narvin Singh <Narvin.A.Singh@gmail.com>
|
|
6
6
|
License: Tremors is a library for logging with metrics.
|
|
@@ -32,10 +32,10 @@ Description-Content-Type: text/x-rst
|
|
|
32
32
|
License-File: LICENSE
|
|
33
33
|
Provides-Extra: dev
|
|
34
34
|
Requires-Dist: coverage~=7.11; extra == "dev"
|
|
35
|
-
Requires-Dist: mypy~=1.
|
|
35
|
+
Requires-Dist: mypy~=1.19; extra == "dev"
|
|
36
36
|
Requires-Dist: pre-commit~=4.3; extra == "dev"
|
|
37
37
|
Requires-Dist: pytest~=9.0; extra == "dev"
|
|
38
|
-
Requires-Dist: ruff~=0.
|
|
38
|
+
Requires-Dist: ruff~=0.15.0; extra == "dev"
|
|
39
39
|
Requires-Dist: sphinx-lint~=1.0; extra == "dev"
|
|
40
40
|
Requires-Dist: yamllint~=1.37; extra == "dev"
|
|
41
41
|
Provides-Extra: doc
|
|
@@ -233,9 +233,16 @@ In the previous example, a new counter collector was used each time the
|
|
|
233
233
|
function is called. Let's reuse the same collector to keep a tally of errors
|
|
234
234
|
across *all* calls to the function.
|
|
235
235
|
|
|
236
|
+
.. note::
|
|
237
|
+
|
|
238
|
+
The counter factory returns a ``CollectorFactory`` that will result in a
|
|
239
|
+
new collector being created for each ``fn`` call. To use the *same*
|
|
240
|
+
collector, we must call the CollectorFactory, hence the trailing
|
|
241
|
+
parentheses on the line where we set ``fn_errors``.
|
|
242
|
+
|
|
236
243
|
.. code-block:: python
|
|
237
244
|
|
|
238
|
-
fn_errors = collector.counter.factory(name="errors", level=logging.ERROR)
|
|
245
|
+
fn_errors = collector.counter.factory(name="errors", level=logging.ERROR)()
|
|
239
246
|
|
|
240
247
|
|
|
241
248
|
@tremors.logged(fn_errors)
|
|
@@ -283,6 +290,47 @@ single logger used in both function calls maintains its state between calls.
|
|
|
283
290
|
errors=2 ERROR:root:uh-ho!
|
|
284
291
|
errors=2 INFO:root:exited: context
|
|
285
292
|
|
|
293
|
+
Collectors may be inherited by descendant loggers. Let's count errors across
|
|
294
|
+
nested loggers.
|
|
295
|
+
|
|
296
|
+
.. code-block:: python
|
|
297
|
+
|
|
298
|
+
@tremors.logged(
|
|
299
|
+
collector.counter.factory(
|
|
300
|
+
name="errors", level=logging.ERROR, inherit=True
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
def parent(*, logger: tremors.Logger = tremors.from_logged) -> None:
|
|
304
|
+
logger.error("uh-ho!")
|
|
305
|
+
child()
|
|
306
|
+
child()
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@tremors.logged
|
|
310
|
+
def child(*, logger: tremors.Logger = tremors.from_logged) -> None:
|
|
311
|
+
logger.error("doh!")
|
|
312
|
+
grandchild()
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@tremors.logged
|
|
316
|
+
def grandchild(*, logger: tremors.Logger = tremors.from_logged) -> None:
|
|
317
|
+
logger.info("so far, so good")
|
|
318
|
+
logger.error("spoke too soon!")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
parent()
|
|
322
|
+
|
|
323
|
+
The entered and exited lines have been omitted. The ``parent`` counter is used the ``child`` and ``grandchild`` functions.
|
|
324
|
+
.. code-block:: shell
|
|
325
|
+
|
|
326
|
+
errors=1 ERROR:root:uh-ho!
|
|
327
|
+
errors=2 ERROR:root:doh!
|
|
328
|
+
errors=2 INFO:root:so far, so good
|
|
329
|
+
errors=3 ERROR:root:spoke too soon!
|
|
330
|
+
errors=4 ERROR:root:doh!
|
|
331
|
+
errors=4 INFO:root:so far, so good
|
|
332
|
+
errors=5 ERROR:root:spoke too soon!
|
|
333
|
+
|
|
286
334
|
See the `collector module`_ in the full `documentation`_ for how you can
|
|
287
335
|
define your own collectors, and bundles.
|
|
288
336
|
|
|
@@ -185,9 +185,16 @@ In the previous example, a new counter collector was used each time the
|
|
|
185
185
|
function is called. Let's reuse the same collector to keep a tally of errors
|
|
186
186
|
across *all* calls to the function.
|
|
187
187
|
|
|
188
|
+
.. note::
|
|
189
|
+
|
|
190
|
+
The counter factory returns a ``CollectorFactory`` that will result in a
|
|
191
|
+
new collector being created for each ``fn`` call. To use the *same*
|
|
192
|
+
collector, we must call the CollectorFactory, hence the trailing
|
|
193
|
+
parentheses on the line where we set ``fn_errors``.
|
|
194
|
+
|
|
188
195
|
.. code-block:: python
|
|
189
196
|
|
|
190
|
-
fn_errors = collector.counter.factory(name="errors", level=logging.ERROR)
|
|
197
|
+
fn_errors = collector.counter.factory(name="errors", level=logging.ERROR)()
|
|
191
198
|
|
|
192
199
|
|
|
193
200
|
@tremors.logged(fn_errors)
|
|
@@ -235,6 +242,47 @@ single logger used in both function calls maintains its state between calls.
|
|
|
235
242
|
errors=2 ERROR:root:uh-ho!
|
|
236
243
|
errors=2 INFO:root:exited: context
|
|
237
244
|
|
|
245
|
+
Collectors may be inherited by descendant loggers. Let's count errors across
|
|
246
|
+
nested loggers.
|
|
247
|
+
|
|
248
|
+
.. code-block:: python
|
|
249
|
+
|
|
250
|
+
@tremors.logged(
|
|
251
|
+
collector.counter.factory(
|
|
252
|
+
name="errors", level=logging.ERROR, inherit=True
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
def parent(*, logger: tremors.Logger = tremors.from_logged) -> None:
|
|
256
|
+
logger.error("uh-ho!")
|
|
257
|
+
child()
|
|
258
|
+
child()
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@tremors.logged
|
|
262
|
+
def child(*, logger: tremors.Logger = tremors.from_logged) -> None:
|
|
263
|
+
logger.error("doh!")
|
|
264
|
+
grandchild()
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@tremors.logged
|
|
268
|
+
def grandchild(*, logger: tremors.Logger = tremors.from_logged) -> None:
|
|
269
|
+
logger.info("so far, so good")
|
|
270
|
+
logger.error("spoke too soon!")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
parent()
|
|
274
|
+
|
|
275
|
+
The entered and exited lines have been omitted. The ``parent`` counter is used the ``child`` and ``grandchild`` functions.
|
|
276
|
+
.. code-block:: shell
|
|
277
|
+
|
|
278
|
+
errors=1 ERROR:root:uh-ho!
|
|
279
|
+
errors=2 ERROR:root:doh!
|
|
280
|
+
errors=2 INFO:root:so far, so good
|
|
281
|
+
errors=3 ERROR:root:spoke too soon!
|
|
282
|
+
errors=4 ERROR:root:doh!
|
|
283
|
+
errors=4 INFO:root:so far, so good
|
|
284
|
+
errors=5 ERROR:root:spoke too soon!
|
|
285
|
+
|
|
238
286
|
See the `collector module`_ in the full `documentation`_ for how you can
|
|
239
287
|
define your own collectors, and bundles.
|
|
240
288
|
|
|
@@ -25,10 +25,10 @@ dependencies = [
|
|
|
25
25
|
[project.optional-dependencies]
|
|
26
26
|
dev = [
|
|
27
27
|
"coverage ~= 7.11",
|
|
28
|
-
"mypy ~= 1.
|
|
28
|
+
"mypy ~= 1.19",
|
|
29
29
|
"pre-commit ~= 4.3",
|
|
30
30
|
"pytest ~= 9.0",
|
|
31
|
-
"ruff ~= 0.
|
|
31
|
+
"ruff ~= 0.15.0",
|
|
32
32
|
"sphinx-lint ~= 1.0",
|
|
33
33
|
"yamllint ~= 1.37",
|
|
34
34
|
]
|
|
@@ -15,7 +15,7 @@ fixed signature as described in :class:`Formatter`.
|
|
|
15
15
|
import dataclasses
|
|
16
16
|
import logging
|
|
17
17
|
import time
|
|
18
|
-
import uuid
|
|
18
|
+
import uuid
|
|
19
19
|
from collections.abc import Callable, Mapping
|
|
20
20
|
from typing import NamedTuple, Protocol
|
|
21
21
|
|
|
@@ -73,7 +73,8 @@ class CollectorBundle[T, **P](NamedTuple):
|
|
|
73
73
|
"""A bundle to create, and work with a collector."""
|
|
74
74
|
|
|
75
75
|
state: type[T]
|
|
76
|
-
|
|
76
|
+
factory_cls: type[tremors.logger.CollectorFactory[T]]
|
|
77
|
+
factory: Callable[P, tremors.logger.CollectorFactory[T]]
|
|
77
78
|
formatter: Formatter
|
|
78
79
|
|
|
79
80
|
|
|
@@ -96,19 +97,50 @@ def _identifier_collect(
|
|
|
96
97
|
return IndentifierState(group_id=logger.group_id, parent=logger.parent, path=logger.path)
|
|
97
98
|
|
|
98
99
|
|
|
100
|
+
class IdentifierFactory(tremors.logger.CollectorFactory[IndentifierState | None]):
|
|
101
|
+
"""Create collectors to gather logger identifiers."""
|
|
102
|
+
|
|
103
|
+
def __init__(
|
|
104
|
+
self, name: str = "elapsed", *, level: int = logging.INFO, inherit: bool = False
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Initialize the state to create collectors.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
name: The name of collectors created by the factory.
|
|
110
|
+
level: The level of collectors created by the factory.
|
|
111
|
+
inherit: If True, collectors created by the factory will be
|
|
112
|
+
inherited.
|
|
113
|
+
"""
|
|
114
|
+
self._name = name
|
|
115
|
+
self._level = level
|
|
116
|
+
self._inherit = inherit
|
|
117
|
+
|
|
118
|
+
def __call__(self) -> tremors.logger.Collector[IndentifierState | None]:
|
|
119
|
+
"""Create a collector."""
|
|
120
|
+
return tremors.logger.Collector(
|
|
121
|
+
id=uuid.uuid4(),
|
|
122
|
+
name=self._name,
|
|
123
|
+
level=self._level,
|
|
124
|
+
inherit=self._inherit,
|
|
125
|
+
state=None,
|
|
126
|
+
collect=_identifier_collect,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
99
130
|
def identifier_factory(
|
|
100
|
-
name: str = "identifier", *, level: int = logging.INFO
|
|
101
|
-
) ->
|
|
102
|
-
"""Create a
|
|
131
|
+
name: str = "identifier", *, level: int = logging.INFO, inherit: bool = False
|
|
132
|
+
) -> IdentifierFactory:
|
|
133
|
+
"""Create a factory for collectors to gather logger identifiers.
|
|
103
134
|
|
|
104
135
|
Args:
|
|
105
|
-
name: The
|
|
106
|
-
level: The
|
|
136
|
+
name: The name of collectors created by the factory.
|
|
137
|
+
level: The level of collectors created by the factory.
|
|
138
|
+
inherit: If True, collectors created by the factory will be inherited.
|
|
107
139
|
|
|
108
140
|
Returns:
|
|
109
|
-
|
|
141
|
+
A factory to create an identifer collector with the specified attributes.
|
|
110
142
|
"""
|
|
111
|
-
return
|
|
143
|
+
return IdentifierFactory(name, level=level, inherit=inherit)
|
|
112
144
|
|
|
113
145
|
|
|
114
146
|
def identifier_formatter(
|
|
@@ -154,7 +186,10 @@ def identifier_formatter(
|
|
|
154
186
|
|
|
155
187
|
|
|
156
188
|
identifier = CollectorBundle(
|
|
157
|
-
state=IndentifierState,
|
|
189
|
+
state=IndentifierState,
|
|
190
|
+
factory_cls=IdentifierFactory,
|
|
191
|
+
factory=identifier_factory,
|
|
192
|
+
formatter=identifier_formatter,
|
|
158
193
|
)
|
|
159
194
|
"""The identifier collector bundle.
|
|
160
195
|
|
|
@@ -283,26 +318,69 @@ def _counter_collect(state: CounterState, _: tremors.logger.LogItem) -> CounterS
|
|
|
283
318
|
return state
|
|
284
319
|
|
|
285
320
|
|
|
321
|
+
class CounterFactory(tremors.logger.CollectorFactory[CounterState]):
|
|
322
|
+
"""Create collectors to count logging calls."""
|
|
323
|
+
|
|
324
|
+
def __init__(
|
|
325
|
+
self,
|
|
326
|
+
name: str = "counter",
|
|
327
|
+
*,
|
|
328
|
+
level: int = logging.INFO,
|
|
329
|
+
inherit: bool = False,
|
|
330
|
+
initial: int = 0,
|
|
331
|
+
step: int = 1,
|
|
332
|
+
) -> None:
|
|
333
|
+
"""Initialize the state to create collectors.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
name: The name of collectors created by the factory.
|
|
337
|
+
level: The level of collectors created by the factory.
|
|
338
|
+
inherit: If True, collectors created by the factory will be
|
|
339
|
+
inherited.
|
|
340
|
+
initial: The initial value of the count of collectors created by
|
|
341
|
+
the factory.
|
|
342
|
+
step: How much to increase the count by each time the collectors
|
|
343
|
+
created by the factory run.
|
|
344
|
+
"""
|
|
345
|
+
self._name = name
|
|
346
|
+
self._level = level
|
|
347
|
+
self._inherit = inherit
|
|
348
|
+
self._initial = initial
|
|
349
|
+
self._step = step
|
|
350
|
+
|
|
351
|
+
def __call__(self) -> tremors.logger.Collector[CounterState]:
|
|
352
|
+
"""Create a collector."""
|
|
353
|
+
return tremors.logger.Collector(
|
|
354
|
+
id=uuid.uuid4(),
|
|
355
|
+
name=self._name,
|
|
356
|
+
level=self._level,
|
|
357
|
+
inherit=self._inherit,
|
|
358
|
+
state=CounterState(count=self._initial, step=self._step),
|
|
359
|
+
collect=_counter_collect,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
286
363
|
def counter_factory(
|
|
287
|
-
name: str = "counter",
|
|
288
|
-
|
|
364
|
+
name: str = "counter",
|
|
365
|
+
*,
|
|
366
|
+
level: int = logging.INFO,
|
|
367
|
+
inherit: bool = False,
|
|
368
|
+
initial: int = 0,
|
|
369
|
+
step: int = 1,
|
|
370
|
+
) -> CounterFactory:
|
|
289
371
|
"""Create a collector to count logging calls.
|
|
290
372
|
|
|
291
373
|
Args:
|
|
292
374
|
name: The collector name.
|
|
293
375
|
level: The collector level.
|
|
376
|
+
inherit: If True, the collector will be inherited.
|
|
294
377
|
initial: The initial value of the count.
|
|
295
378
|
step: How much to increase the count by each time the collector runs.
|
|
296
379
|
|
|
297
380
|
Returns:
|
|
298
|
-
A counter collector with the specified
|
|
381
|
+
A factory to create counter collector with the specified attributes.
|
|
299
382
|
"""
|
|
300
|
-
return
|
|
301
|
-
name=name,
|
|
302
|
-
level=level,
|
|
303
|
-
state=CounterState(count=initial, step=step),
|
|
304
|
-
collect=_counter_collect,
|
|
305
|
-
)
|
|
383
|
+
return CounterFactory(name, level=level, inherit=inherit, initial=initial, step=step)
|
|
306
384
|
|
|
307
385
|
|
|
308
386
|
def counter_formatter(
|
|
@@ -325,7 +403,12 @@ def counter_formatter(
|
|
|
325
403
|
return str(fmt).format(counter=state.count)
|
|
326
404
|
|
|
327
405
|
|
|
328
|
-
counter = CollectorBundle(
|
|
406
|
+
counter = CollectorBundle(
|
|
407
|
+
state=CounterState,
|
|
408
|
+
factory_cls=CounterFactory,
|
|
409
|
+
factory=counter_factory,
|
|
410
|
+
formatter=counter_formatter,
|
|
411
|
+
)
|
|
329
412
|
"""The counter collector bundle.
|
|
330
413
|
|
|
331
414
|
Examples:
|
|
@@ -432,21 +515,50 @@ def _collect_elapsed(state: ElapsedState, _: tremors.logger.LogItem) -> ElapsedS
|
|
|
432
515
|
return state
|
|
433
516
|
|
|
434
517
|
|
|
518
|
+
class ElapsedFactory(tremors.logger.CollectorFactory[ElapsedState]):
|
|
519
|
+
"""Create collectors to measure how much time has elapsed."""
|
|
520
|
+
|
|
521
|
+
def __init__(
|
|
522
|
+
self, name: str = "elapsed", *, level: int = logging.INFO, inherit: bool = False
|
|
523
|
+
) -> None:
|
|
524
|
+
"""Initialize the state to create collectors.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
name: The name of collectors created by the factory.
|
|
528
|
+
level: The level of collectors created by the factory.
|
|
529
|
+
inherit: If True, collectors created by the factory will be
|
|
530
|
+
inherited.
|
|
531
|
+
"""
|
|
532
|
+
self._name = name
|
|
533
|
+
self._level = level
|
|
534
|
+
self._inherit = inherit
|
|
535
|
+
|
|
536
|
+
def __call__(self) -> tremors.logger.Collector[ElapsedState]:
|
|
537
|
+
"""Create a collector."""
|
|
538
|
+
return tremors.logger.Collector(
|
|
539
|
+
id=uuid.uuid4(),
|
|
540
|
+
name=self._name,
|
|
541
|
+
level=self._level,
|
|
542
|
+
inherit=self._inherit,
|
|
543
|
+
state=ElapsedState(t0=None, t=None),
|
|
544
|
+
collect=_collect_elapsed,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
|
|
435
548
|
def elapsed_factory(
|
|
436
|
-
name: str = "elapsed", *, level: int = logging.INFO
|
|
437
|
-
) ->
|
|
438
|
-
"""Create a
|
|
549
|
+
name: str = "elapsed", *, level: int = logging.INFO, inherit: bool = False
|
|
550
|
+
) -> ElapsedFactory:
|
|
551
|
+
"""Create a factory for collectors to measure how much time has elapsed.
|
|
439
552
|
|
|
440
553
|
Args:
|
|
441
|
-
name: The
|
|
442
|
-
level: The
|
|
554
|
+
name: The name of collectors created by the factory.
|
|
555
|
+
level: The level of collectors created by the factory.
|
|
556
|
+
inherit: If True, collectors created by the factory will be inherited.
|
|
443
557
|
|
|
444
558
|
Returns:
|
|
445
|
-
|
|
559
|
+
A factory to create an elapsed collector with the specified attributes.
|
|
446
560
|
"""
|
|
447
|
-
return
|
|
448
|
-
name=name, level=level, state=ElapsedState(t0=None, t=None), collect=_collect_elapsed
|
|
449
|
-
)
|
|
561
|
+
return ElapsedFactory(name, level=level, inherit=inherit)
|
|
450
562
|
|
|
451
563
|
|
|
452
564
|
def elapsed_formatter(
|
|
@@ -479,7 +591,12 @@ def elapsed_formatter(
|
|
|
479
591
|
return str(fmt).format(elapsed=elapsed_s)
|
|
480
592
|
|
|
481
593
|
|
|
482
|
-
elapsed = CollectorBundle(
|
|
594
|
+
elapsed = CollectorBundle(
|
|
595
|
+
state=ElapsedState,
|
|
596
|
+
factory_cls=ElapsedFactory,
|
|
597
|
+
factory=elapsed_factory,
|
|
598
|
+
formatter=elapsed_formatter,
|
|
599
|
+
)
|
|
483
600
|
"""The elapsed collector bundle.
|
|
484
601
|
|
|
485
602
|
Examples:
|
|
@@ -6,20 +6,20 @@ and injects that logger into the callable.
|
|
|
6
6
|
|
|
7
7
|
import functools
|
|
8
8
|
from collections.abc import Callable # noqa: TC003
|
|
9
|
-
from typing import Any,
|
|
9
|
+
from typing import Any, overload
|
|
10
10
|
|
|
11
11
|
import tremors.logger
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def _logged[TRet, **P](
|
|
15
15
|
fn: Callable[P, TRet],
|
|
16
|
-
*collectors: tremors.logger.Collector[Any],
|
|
16
|
+
*collectors: tremors.logger.Collector[Any] | tremors.logger.CollectorFactory[Any],
|
|
17
17
|
name: str,
|
|
18
18
|
logger_name: str | None = None,
|
|
19
19
|
) -> Callable[P, TRet]:
|
|
20
20
|
@functools.wraps(fn)
|
|
21
21
|
def logged_wrapper(*args: P.args, **kwargs: P.kwargs) -> TRet:
|
|
22
|
-
if "logger"
|
|
22
|
+
if (logger_arg := kwargs.get("logger")) and logger_arg is not from_logged:
|
|
23
23
|
return fn(*args, **kwargs)
|
|
24
24
|
with tremors.logger.Logger(*collectors, name=name, logger_name=logger_name) as logger:
|
|
25
25
|
kwargs["logger"] = logger
|
|
@@ -30,8 +30,8 @@ def _logged[TRet, **P](
|
|
|
30
30
|
|
|
31
31
|
@overload
|
|
32
32
|
def logged[TRet, **P](
|
|
33
|
-
*collectors: tremors.logger.Collector[Any],
|
|
34
|
-
name: str
|
|
33
|
+
*collectors: tremors.logger.Collector[Any] | tremors.logger.CollectorFactory[Any],
|
|
34
|
+
name: str,
|
|
35
35
|
logger_name: str | None = None,
|
|
36
36
|
) -> Callable[[Callable[P, TRet]], Callable[P, TRet]]: ...
|
|
37
37
|
|
|
@@ -41,8 +41,11 @@ def logged[TRet, **P](fn_or_collector: Callable[P, TRet]) -> Callable[P, TRet]:
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def logged[TRet, **P](
|
|
44
|
-
fn_or_collector: Callable[P, TRet]
|
|
45
|
-
|
|
44
|
+
fn_or_collector: Callable[P, TRet]
|
|
45
|
+
| tremors.logger.Collector[Any]
|
|
46
|
+
| tremors.logger.CollectorFactory[Any]
|
|
47
|
+
| None = None,
|
|
48
|
+
*collectors: tremors.logger.Collector[Any] | tremors.logger.CollectorFactory[Any],
|
|
46
49
|
name: str | None = None,
|
|
47
50
|
logger_name: str | None = None,
|
|
48
51
|
) -> Callable[P, TRet] | Callable[[Callable[P, TRet]], Callable[P, TRet]]:
|
|
@@ -66,7 +69,9 @@ def logged[TRet, **P](
|
|
|
66
69
|
logger_name: The name of the underlying logger. If None, the standard
|
|
67
70
|
root logger is used.
|
|
68
71
|
"""
|
|
69
|
-
if callable(fn_or_collector)
|
|
72
|
+
if callable(fn_or_collector) and not isinstance(
|
|
73
|
+
fn_or_collector, tremors.logger.CollectorFactory
|
|
74
|
+
):
|
|
70
75
|
default_collectors = () # TODO @NAS: Implement default collectors.
|
|
71
76
|
return _logged(fn_or_collector, *default_collectors, name=fn_or_collector.__name__)
|
|
72
77
|
|
|
@@ -91,7 +96,7 @@ def logged[TRet, **P](
|
|
|
91
96
|
return decorator
|
|
92
97
|
|
|
93
98
|
|
|
94
|
-
from_logged
|
|
99
|
+
from_logged = tremors.logger.Logger(name="__from_logged__")
|
|
95
100
|
"""A sentinel logger value.
|
|
96
101
|
|
|
97
102
|
When used as the ``logger`` argument to a :func:`logged` callable, the
|
|
@@ -41,6 +41,7 @@ define custom collectors, and bundles.
|
|
|
41
41
|
|
|
42
42
|
from __future__ import annotations
|
|
43
43
|
|
|
44
|
+
import abc
|
|
44
45
|
import contextvars
|
|
45
46
|
import functools
|
|
46
47
|
import itertools
|
|
@@ -82,23 +83,38 @@ class Collector[TState](NamedTuple):
|
|
|
82
83
|
"""The collector specification.
|
|
83
84
|
|
|
84
85
|
Attributes:
|
|
86
|
+
id: A unique identifier for the collector.
|
|
85
87
|
name: The name of the collector. When the collector runs
|
|
86
88
|
for a logged message, the collector's state is added to the
|
|
87
89
|
:class:`~logging.LogRecord`, and may be retrieved using this name.
|
|
88
90
|
level: The minimum level a logged message must be for the collector
|
|
89
91
|
to run.
|
|
92
|
+
inherit: If True, the collector will also be added to descendant
|
|
93
|
+
loggers, provided its name does not conflict that of any of the
|
|
94
|
+
other collectors on the descendant loggers.
|
|
90
95
|
state: The initial state of the collector. Each time the collector
|
|
91
96
|
runs, it may update this state.
|
|
92
97
|
collect: When the collector runs, this function is called with the
|
|
93
98
|
current state, and the LogItem. It returns the new state.
|
|
94
99
|
"""
|
|
95
100
|
|
|
101
|
+
id: uuid.UUID
|
|
96
102
|
name: str
|
|
97
103
|
level: int
|
|
104
|
+
inherit: bool
|
|
98
105
|
state: TState
|
|
99
106
|
collect: Callable[[TState, LogItem], TState]
|
|
100
107
|
|
|
101
108
|
|
|
109
|
+
class CollectorFactory[TState](metaclass=abc.ABCMeta):
|
|
110
|
+
"""A factory that creates a collector."""
|
|
111
|
+
|
|
112
|
+
@abc.abstractmethod
|
|
113
|
+
def __call__(self) -> Collector[TState]:
|
|
114
|
+
"""Create a collector."""
|
|
115
|
+
raise NotImplementedError
|
|
116
|
+
|
|
117
|
+
|
|
102
118
|
def _collectors_reducer[T](
|
|
103
119
|
acc: tuple[MutableMapping[str, Collector[T]], MutableMapping[str, T]],
|
|
104
120
|
curr: tuple[LogItem, tuple[str, Collector[T]]],
|
|
@@ -110,7 +126,9 @@ def _collectors_reducer[T](
|
|
|
110
126
|
acc_extra[name] = curr_c.state
|
|
111
127
|
return acc_collectors, acc_extra
|
|
112
128
|
state = curr_c.collect(curr_c.state, log_item)
|
|
113
|
-
acc_collectors[name] = Collector(
|
|
129
|
+
acc_collectors[name] = Collector(
|
|
130
|
+
curr_c.id, curr_c.name, curr_c.level, curr_c.inherit, state, curr_c.collect
|
|
131
|
+
)
|
|
114
132
|
acc_extra[name] = state
|
|
115
133
|
return acc_collectors, acc_extra
|
|
116
134
|
|
|
@@ -122,8 +140,9 @@ class Logger(logging.LoggerAdapter[logging.Logger]):
|
|
|
122
140
|
"""The Tremors logger.
|
|
123
141
|
|
|
124
142
|
Args:
|
|
125
|
-
*collectors: Collectors
|
|
126
|
-
|
|
143
|
+
*collectors: Collectors or CollectorFactories. The collectors, or
|
|
144
|
+
resultant collectors will be attached to the logger, and will be run
|
|
145
|
+
for every logged message for which the collector is enabled.
|
|
127
146
|
name: The name of the logger. This name is logged in entering, and
|
|
128
147
|
exiting messages. This name is used to generate the path, but
|
|
129
148
|
may be altered in the path to be unique in the hierarchy.
|
|
@@ -138,7 +157,7 @@ class Logger(logging.LoggerAdapter[logging.Logger]):
|
|
|
138
157
|
|
|
139
158
|
def __init__(
|
|
140
159
|
self,
|
|
141
|
-
*collectors: Collector[Any],
|
|
160
|
+
*collectors: Collector[Any] | CollectorFactory[Any],
|
|
142
161
|
name: str,
|
|
143
162
|
logger_name: str | None = None,
|
|
144
163
|
ctx_level: int = logging.INFO,
|
|
@@ -147,7 +166,12 @@ class Logger(logging.LoggerAdapter[logging.Logger]):
|
|
|
147
166
|
"""Initialize the logger."""
|
|
148
167
|
super().__init__(logging.getLogger(logger_name))
|
|
149
168
|
self._name = name
|
|
150
|
-
self._collectors:
|
|
169
|
+
self._collectors: MutableMapping[str, Collector[Any]] = {
|
|
170
|
+
(coll := c_or_fact()).name
|
|
171
|
+
if isinstance(c_or_fact, CollectorFactory)
|
|
172
|
+
else (coll := c_or_fact).name: coll
|
|
173
|
+
for c_or_fact in collectors
|
|
174
|
+
}
|
|
151
175
|
self._entered = 0
|
|
152
176
|
self._ctx_level = ctx_level
|
|
153
177
|
self._cv_token: contextvars.Token[Logger | None] | None = None
|
|
@@ -180,6 +204,9 @@ class Logger(logging.LoggerAdapter[logging.Logger]):
|
|
|
180
204
|
if self._parent:
|
|
181
205
|
self._group_id = self._parent.group_id
|
|
182
206
|
self._path = self._parent.register_path(self)
|
|
207
|
+
for name, collector in self._parent.collectors.items():
|
|
208
|
+
if collector.inherit and name not in self._collectors:
|
|
209
|
+
self._collectors[name] = collector
|
|
183
210
|
else:
|
|
184
211
|
self._group_id = uuid.uuid4()
|
|
185
212
|
self._path = (self._name,)
|
|
@@ -211,6 +238,11 @@ class Logger(logging.LoggerAdapter[logging.Logger]):
|
|
|
211
238
|
if self._entered == 0 and self._cv_token:
|
|
212
239
|
_current_logger.reset(self._cv_token)
|
|
213
240
|
|
|
241
|
+
@property
|
|
242
|
+
def collectors(self) -> Mapping[str, Collector[Any]]:
|
|
243
|
+
"""The logger collectors."""
|
|
244
|
+
return self._collectors
|
|
245
|
+
|
|
214
246
|
@property
|
|
215
247
|
def group_id(self) -> uuid.UUID | None:
|
|
216
248
|
"""The group ID for the hierarchy assigned by the root logger."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tremors
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Tremors is a library for logging while collecting metrics.
|
|
5
5
|
Author-email: Narvin Singh <Narvin.A.Singh@gmail.com>
|
|
6
6
|
License: Tremors is a library for logging with metrics.
|
|
@@ -32,10 +32,10 @@ Description-Content-Type: text/x-rst
|
|
|
32
32
|
License-File: LICENSE
|
|
33
33
|
Provides-Extra: dev
|
|
34
34
|
Requires-Dist: coverage~=7.11; extra == "dev"
|
|
35
|
-
Requires-Dist: mypy~=1.
|
|
35
|
+
Requires-Dist: mypy~=1.19; extra == "dev"
|
|
36
36
|
Requires-Dist: pre-commit~=4.3; extra == "dev"
|
|
37
37
|
Requires-Dist: pytest~=9.0; extra == "dev"
|
|
38
|
-
Requires-Dist: ruff~=0.
|
|
38
|
+
Requires-Dist: ruff~=0.15.0; extra == "dev"
|
|
39
39
|
Requires-Dist: sphinx-lint~=1.0; extra == "dev"
|
|
40
40
|
Requires-Dist: yamllint~=1.37; extra == "dev"
|
|
41
41
|
Provides-Extra: doc
|
|
@@ -233,9 +233,16 @@ In the previous example, a new counter collector was used each time the
|
|
|
233
233
|
function is called. Let's reuse the same collector to keep a tally of errors
|
|
234
234
|
across *all* calls to the function.
|
|
235
235
|
|
|
236
|
+
.. note::
|
|
237
|
+
|
|
238
|
+
The counter factory returns a ``CollectorFactory`` that will result in a
|
|
239
|
+
new collector being created for each ``fn`` call. To use the *same*
|
|
240
|
+
collector, we must call the CollectorFactory, hence the trailing
|
|
241
|
+
parentheses on the line where we set ``fn_errors``.
|
|
242
|
+
|
|
236
243
|
.. code-block:: python
|
|
237
244
|
|
|
238
|
-
fn_errors = collector.counter.factory(name="errors", level=logging.ERROR)
|
|
245
|
+
fn_errors = collector.counter.factory(name="errors", level=logging.ERROR)()
|
|
239
246
|
|
|
240
247
|
|
|
241
248
|
@tremors.logged(fn_errors)
|
|
@@ -283,6 +290,47 @@ single logger used in both function calls maintains its state between calls.
|
|
|
283
290
|
errors=2 ERROR:root:uh-ho!
|
|
284
291
|
errors=2 INFO:root:exited: context
|
|
285
292
|
|
|
293
|
+
Collectors may be inherited by descendant loggers. Let's count errors across
|
|
294
|
+
nested loggers.
|
|
295
|
+
|
|
296
|
+
.. code-block:: python
|
|
297
|
+
|
|
298
|
+
@tremors.logged(
|
|
299
|
+
collector.counter.factory(
|
|
300
|
+
name="errors", level=logging.ERROR, inherit=True
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
def parent(*, logger: tremors.Logger = tremors.from_logged) -> None:
|
|
304
|
+
logger.error("uh-ho!")
|
|
305
|
+
child()
|
|
306
|
+
child()
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@tremors.logged
|
|
310
|
+
def child(*, logger: tremors.Logger = tremors.from_logged) -> None:
|
|
311
|
+
logger.error("doh!")
|
|
312
|
+
grandchild()
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@tremors.logged
|
|
316
|
+
def grandchild(*, logger: tremors.Logger = tremors.from_logged) -> None:
|
|
317
|
+
logger.info("so far, so good")
|
|
318
|
+
logger.error("spoke too soon!")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
parent()
|
|
322
|
+
|
|
323
|
+
The entered and exited lines have been omitted. The ``parent`` counter is used the ``child`` and ``grandchild`` functions.
|
|
324
|
+
.. code-block:: shell
|
|
325
|
+
|
|
326
|
+
errors=1 ERROR:root:uh-ho!
|
|
327
|
+
errors=2 ERROR:root:doh!
|
|
328
|
+
errors=2 INFO:root:so far, so good
|
|
329
|
+
errors=3 ERROR:root:spoke too soon!
|
|
330
|
+
errors=4 ERROR:root:doh!
|
|
331
|
+
errors=4 INFO:root:so far, so good
|
|
332
|
+
errors=5 ERROR:root:spoke too soon!
|
|
333
|
+
|
|
286
334
|
See the `collector module`_ in the full `documentation`_ for how you can
|
|
287
335
|
define your own collectors, and bundles.
|
|
288
336
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|