donelogger 0.1.7__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.
- donelogger-0.1.7/LICENSE +21 -0
- donelogger-0.1.7/PKG-INFO +415 -0
- donelogger-0.1.7/README.md +371 -0
- donelogger-0.1.7/donelogger/__init__.py +3 -0
- donelogger-0.1.7/donelogger/donelogger.py +145 -0
- donelogger-0.1.7/donelogger.egg-info/PKG-INFO +415 -0
- donelogger-0.1.7/donelogger.egg-info/SOURCES.txt +10 -0
- donelogger-0.1.7/donelogger.egg-info/dependency_links.txt +1 -0
- donelogger-0.1.7/donelogger.egg-info/top_level.txt +1 -0
- donelogger-0.1.7/pyproject.toml +33 -0
- donelogger-0.1.7/setup.cfg +4 -0
- donelogger-0.1.7/tests/test_donelogger.py +164 -0
donelogger-0.1.7/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022-2026 rkskmt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: donelogger
|
|
3
|
+
Version: 0.1.7
|
|
4
|
+
Summary: Time long-running steps with two ordinary log lines: write [Start] and [Done], get the elapsed time filled in for you.
|
|
5
|
+
Author: rkskmt
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2022-2026 rkskmt
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/rkskmt/donelogger
|
|
29
|
+
Project-URL: Repository, https://github.com/rkskmt/donelogger
|
|
30
|
+
Project-URL: Issues, https://github.com/rkskmt/donelogger/issues
|
|
31
|
+
Keywords: logging,timer,elapsed,stopwatch,profiling
|
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Operating System :: OS Independent
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
38
|
+
Classifier: Topic :: System :: Logging
|
|
39
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
40
|
+
Requires-Python: >=3.7
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
License-File: LICENSE
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# donelogger
|
|
46
|
+
|
|
47
|
+
**Time long-running steps with two ordinary log lines.**
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
Write `[Start]` when work begins and `[Done]` when it ends. donelogger fills
|
|
52
|
+
the elapsed time into the log line for you.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from donelogger import getLogger
|
|
56
|
+
|
|
57
|
+
logger = getLogger()
|
|
58
|
+
|
|
59
|
+
logger.info("[Start] Training model...")
|
|
60
|
+
train()
|
|
61
|
+
logger.info("[Done] Finished")
|
|
62
|
+
# -> +[Go Job] Training model...
|
|
63
|
+
# -> -[Done Job(1m23.40s)] Finished ← elapsed time, measured for you
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
That's the whole idea: no `time.perf_counter()` variables, no manual
|
|
67
|
+
subtraction, no `f"{...:.3f}s"` formatting scattered through your code.
|
|
68
|
+
|
|
69
|
+
If you've ever written this:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
t0 = time.perf_counter()
|
|
73
|
+
logger.info("Loading dataset...")
|
|
74
|
+
load_dataset()
|
|
75
|
+
logger.info(f"Finished loading in {time.perf_counter() - t0:.3f}s")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
you can write this instead:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
logger.info("[Start] Loading dataset...")
|
|
82
|
+
load_dataset()
|
|
83
|
+
logger.info("[Done] Finished loading")
|
|
84
|
+
# -> -[Done Job(2.413s)] Finished loading
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The timing is just part of the log. Your call sites stay plain
|
|
88
|
+
`logger.info(...)`, and everything that is not a marker remains a normal log
|
|
89
|
+
message.
|
|
90
|
+
|
|
91
|
+
No tag needed for the basic case. When you want to time nested or overlapping
|
|
92
|
+
steps, add an optional `:tag` (`[Start:train]` ... `[Done:train]`).
|
|
93
|
+
|
|
94
|
+
> Battle-tested: donelogger runs in production internal tooling, where knowing
|
|
95
|
+
> "how long did each stage take?" across a long pipeline matters every day.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Why donelogger?
|
|
100
|
+
|
|
101
|
+
### 1. Make timing cheap enough to use everywhere
|
|
102
|
+
|
|
103
|
+
When timing is annoying, you only add it after something gets slow. donelogger
|
|
104
|
+
makes it cheap enough to leave timing breadcrumbs throughout a script, CLI,
|
|
105
|
+
batch job, ML run, or data pipeline:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
logger.info("[Start] Download files")
|
|
109
|
+
download_files()
|
|
110
|
+
logger.info("[Done] Downloaded")
|
|
111
|
+
|
|
112
|
+
logger.info("[Start] Parse records")
|
|
113
|
+
parse_records()
|
|
114
|
+
logger.info("[Done] Parsed")
|
|
115
|
+
|
|
116
|
+
logger.info("[Start] Write output")
|
|
117
|
+
write_output()
|
|
118
|
+
logger.info("[Done] Wrote output")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
You get readable progress logs while the job runs, and elapsed times once each
|
|
122
|
+
step finishes.
|
|
123
|
+
|
|
124
|
+
### 2. Stop subtracting timestamps by hand
|
|
125
|
+
|
|
126
|
+
The usual timing pattern is repetitive and easy to get slightly wrong:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
t0 = time.perf_counter()
|
|
130
|
+
logger.info("Loading dataset...")
|
|
131
|
+
load_dataset()
|
|
132
|
+
logger.info(f"Finished loading in {time.perf_counter() - t0:.3f}s")
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
donelogger keeps the stopwatch attached to the log line instead of your local
|
|
136
|
+
variables:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
logger.info("[Start] Loading dataset...")
|
|
140
|
+
load_dataset()
|
|
141
|
+
logger.info("[Done] Finished loading")
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
This really pays off once timings are **nested**. You often want an inner step's
|
|
145
|
+
time *and* the whole job's time. By the time the job ends, the start line has
|
|
146
|
+
scrolled far up the log; squinting at two timestamps to subtract them is exactly
|
|
147
|
+
the chore donelogger removes. (See [nested timing](#named-tags-time-nested-or-overlapping-work).)
|
|
148
|
+
|
|
149
|
+
### 3. Skip the `logging` setup boilerplate
|
|
150
|
+
|
|
151
|
+
Getting plain `logging` to print the way you want takes a handler, a formatter,
|
|
152
|
+
a level, and a few lines of wiring before a single line shows up:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
# Before — standard logging needs setup before it's usable
|
|
156
|
+
import logging, sys
|
|
157
|
+
logger = logging.getLogger("myapp")
|
|
158
|
+
logger.setLevel(logging.INFO)
|
|
159
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
160
|
+
handler.setFormatter(logging.Formatter("%(asctime)s|%(levelname)s|%(message)s",
|
|
161
|
+
"%d/%m/%Y %H:%M:%S"))
|
|
162
|
+
logger.addHandler(handler)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`getLogger()` does all of that for you — and hands back a **real
|
|
166
|
+
`logging.Logger`**, so levels, file output, and custom formats keep working
|
|
167
|
+
exactly as you'd expect:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
# After — configured and ready in one line
|
|
171
|
+
from donelogger import getLogger
|
|
172
|
+
logger = getLogger() # console-ready, sensible defaults
|
|
173
|
+
logger = getLogger(logfile="app.log") # ...also writes to a rotating file
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Nothing proprietary to learn: it's `logging` underneath, just without the setup.
|
|
177
|
+
|
|
178
|
+
## Features
|
|
179
|
+
|
|
180
|
+
- **Zero-boilerplate timing** — wrap work in `[Start]` / `[Done]` and get the elapsed time for free; no tag required.
|
|
181
|
+
- **One-line setup** — `getLogger()` returns a ready-to-use logger (handler, formatter, and level already wired) — no `logging` boilerplate.
|
|
182
|
+
- **Reads like normal logs** — markers are just text at the front of your message; nothing new to learn.
|
|
183
|
+
- **Named tags** — time overlapping or nested stages independently (`total`, `load`, `train`, …).
|
|
184
|
+
- **Cross-module** — start a timer in one file and finish it in another, as long as they share a logger name.
|
|
185
|
+
- **Human-friendly durations** — adaptive units from microseconds to hours (`300us`, `512.0ms`, `1.003s`, `1m15.40s`, `1h15m00s`), or force fixed seconds.
|
|
186
|
+
- **Drop-in `logging`** — `getLogger()` returns a real `logging.Logger`; all the usual `.info()` / `.warning()` / `.error()` work unchanged.
|
|
187
|
+
- **Optional rotating file log** — one argument enables a `RotatingFileHandler` with a detailed format.
|
|
188
|
+
- **Zero dependencies** — pure standard library, built on `logging.Formatter`.
|
|
189
|
+
|
|
190
|
+
## Installation
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
pip install git+https://github.com/rkskmt/donelogger.git
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Or install locally for development:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
git clone https://github.com/rkskmt/donelogger.git
|
|
200
|
+
cd donelogger
|
|
201
|
+
pip install -e .
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Quick Start
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
import time
|
|
208
|
+
from donelogger import getLogger
|
|
209
|
+
|
|
210
|
+
logger = getLogger()
|
|
211
|
+
|
|
212
|
+
# The basics: bare [Start] / [Done], no tag.
|
|
213
|
+
logger.info("[Start] Loading dataset...")
|
|
214
|
+
time.sleep(2)
|
|
215
|
+
logger.info("[Done] Finished loading")
|
|
216
|
+
# -> -[Done Job(2.001s)] Finished loading
|
|
217
|
+
|
|
218
|
+
# Need to time several things at once? Add an optional tag.
|
|
219
|
+
logger.info("[Start:train] Training model")
|
|
220
|
+
time.sleep(1)
|
|
221
|
+
logger.info("[Done:train]")
|
|
222
|
+
# -> -[Done train(1.002s)]
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Usage
|
|
226
|
+
|
|
227
|
+
### Default timer (no tag required)
|
|
228
|
+
|
|
229
|
+
Tags are optional. Bare `[Start]` / `[Done]` use a default timer named `Job`:
|
|
230
|
+
|
|
231
|
+
```python
|
|
232
|
+
logger.info("[Start] Processing")
|
|
233
|
+
# ... work ...
|
|
234
|
+
logger.info("[Done] Complete")
|
|
235
|
+
# -> -[Done Job(512.0ms)] Complete
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Named tags: time nested or overlapping work
|
|
239
|
+
|
|
240
|
+
Bare `[Start]` / `[Done]` track one thing at a time. Add a `:tag` to run several
|
|
241
|
+
timers at once — ideal when you want an inner step's time **and** the overall
|
|
242
|
+
time, without scrolling back up the log to subtract timestamps by hand:
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
logger.info("[Start:total] Pipeline starting")
|
|
246
|
+
logger.info("[Start:load] Loading dataset...")
|
|
247
|
+
load_dataset()
|
|
248
|
+
logger.info("[Done:load] Data ready") # inner step time
|
|
249
|
+
train()
|
|
250
|
+
logger.info("[Done:total] Pipeline finished") # whole-pipeline time
|
|
251
|
+
# -> -[Done load(2.001s)] Data ready
|
|
252
|
+
# -> -[Done total(1m25.40s)] Pipeline finished
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Timers are independent, so tags can nest (as above) or overlap freely without
|
|
256
|
+
clobbering each other:
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
logger.info("[Start:download] Downloading files")
|
|
260
|
+
logger.info("[Start:parse] Parsing config")
|
|
261
|
+
logger.info("[Done:parse] Config ready") # parse stops first
|
|
262
|
+
logger.info("[Done:download] Files saved") # download stops later
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Cross-module timing
|
|
266
|
+
|
|
267
|
+
`getLogger(name=...)` returns the **same logger instance** for a given name
|
|
268
|
+
(process-wide singleton), and the timer state lives on that logger. So any
|
|
269
|
+
module that calls `getLogger()` with the same name shares the same timers —
|
|
270
|
+
**start in one file, finish in another**:
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
# === data_loader.py ===
|
|
274
|
+
from donelogger import getLogger
|
|
275
|
+
getLogger().info("[Start:pipeline] Begin data pipeline")
|
|
276
|
+
|
|
277
|
+
# === trainer.py ===
|
|
278
|
+
from donelogger import getLogger
|
|
279
|
+
# ... after all stages finish ...
|
|
280
|
+
getLogger().info("[Done:pipeline] Pipeline complete")
|
|
281
|
+
# -> -[Done pipeline(42.300s)] Pipeline complete
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Loggers created with **different names keep independent timers**, so unrelated
|
|
285
|
+
components never clobber each other's tags.
|
|
286
|
+
|
|
287
|
+
### Regular logging
|
|
288
|
+
|
|
289
|
+
Everything that isn't a marker passes straight through — donelogger is a normal
|
|
290
|
+
logger:
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
logger.info("Just a normal message")
|
|
294
|
+
logger.warning("This is a warning")
|
|
295
|
+
logger.error("Something went wrong")
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
(Only `INFO`-level messages are scanned for markers; other levels are never
|
|
299
|
+
touched.)
|
|
300
|
+
|
|
301
|
+
### Choosing the elapsed-time format
|
|
302
|
+
|
|
303
|
+
`elapsed_style` controls how durations are rendered (default `"adaptive"`):
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
logger = getLogger(elapsed_style="adaptive") # 300us, 512.0ms, 1.003s, 1m15.40s, 1h15m00s
|
|
307
|
+
logger = getLogger(elapsed_style="seconds") # always seconds: 0.300s, 0.512s, 1.003s, 75.400s
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### File logging
|
|
311
|
+
|
|
312
|
+
Pass `logfile=` to also write to a rotating file (1 MB × 2 backups) with a
|
|
313
|
+
detailed, machine-friendly format:
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
logger = getLogger(name="myapp", logfile="app.log")
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Full configuration
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
import logging
|
|
323
|
+
from donelogger import getLogger
|
|
324
|
+
|
|
325
|
+
logger = getLogger(
|
|
326
|
+
name="myapp",
|
|
327
|
+
logLevel=logging.DEBUG,
|
|
328
|
+
logfile="app.log",
|
|
329
|
+
fmt="%(asctime)s|%(levelname)s|%(message)s",
|
|
330
|
+
datefmt="%d/%m/%Y %H:%M:%S",
|
|
331
|
+
elapsed_style="adaptive",
|
|
332
|
+
)
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Marker syntax
|
|
336
|
+
|
|
337
|
+
| Marker | Meaning |
|
|
338
|
+
|---|---|
|
|
339
|
+
| `[Start]` / `[Go]` | Start the default (`Job`) timer |
|
|
340
|
+
| `[Start:tag]` / `[Go:tag]` | Start a named timer |
|
|
341
|
+
| `[Done]` | Stop the default timer and print elapsed time |
|
|
342
|
+
| `[Done:tag]` | Stop a named timer and print elapsed time |
|
|
343
|
+
|
|
344
|
+
- The keyword is **case-insensitive** (`start`, `Start`, `go`, `Go`, `done`, `Done`).
|
|
345
|
+
- A marker is only recognized at the **very beginning** of the message.
|
|
346
|
+
- `[Done:tag]` without a prior `[Start:tag]` emits `*LOG ERROR* (tag is not started) {...}` so mistakes are obvious.
|
|
347
|
+
- A tag is not consumed on `[Done]`, so you can stop the same tag more than once (each reports elapsed since its `[Start]`).
|
|
348
|
+
|
|
349
|
+
## Output format
|
|
350
|
+
|
|
351
|
+
```
|
|
352
|
+
+[Go data_load] Loading dataset... ← '+' = timer started
|
|
353
|
+
-[Done data_load(2.001s)] Finished ← '-' = timer stopped, elapsed shown
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**`adaptive`** (default) picks a unit by magnitude:
|
|
357
|
+
|
|
358
|
+
| Duration | Rendered |
|
|
359
|
+
|---|---|
|
|
360
|
+
| 300 µs | `300us` |
|
|
361
|
+
| 5 ms | `5.0ms` |
|
|
362
|
+
| 0.512 s | `512.0ms` |
|
|
363
|
+
| 1.003 s | `1.003s` |
|
|
364
|
+
| 75.4 s | `1m15.40s` |
|
|
365
|
+
| 4500 s | `1h15m00s` |
|
|
366
|
+
|
|
367
|
+
**`seconds`** always uses seconds (handy when you post-process logs):
|
|
368
|
+
|
|
369
|
+
| Duration | Rendered |
|
|
370
|
+
|---|---|
|
|
371
|
+
| 0.005 s | `0.005s` |
|
|
372
|
+
| 75.4 s | `1m15.400s` |
|
|
373
|
+
| 4500 s | `75m00.000s` |
|
|
374
|
+
|
|
375
|
+
## API
|
|
376
|
+
|
|
377
|
+
### `getLogger(name="doneLogger", logLevel=logging.INFO, logfile=None, fmt=..., datefmt=..., elapsed_style="adaptive")`
|
|
378
|
+
|
|
379
|
+
Returns a configured `logging.Logger`. Calling it again with the same `name`
|
|
380
|
+
returns the cached instance (so configuration only happens once).
|
|
381
|
+
|
|
382
|
+
| Parameter | Default | Description |
|
|
383
|
+
|---|---|---|
|
|
384
|
+
| `name` | `"doneLogger"` | Logger name. Same name → same instance (and shared timers). `"root"` configures the root logger. |
|
|
385
|
+
| `logLevel` | `logging.INFO` | Level for the console handler / logger. |
|
|
386
|
+
| `logfile` | `None` | If set, also log to this file via a rotating handler (1 MB × 2 backups). |
|
|
387
|
+
| `fmt` | `%(asctime)s\|%(levelname)s\|%(message)s` | Console log format (standard `logging` format string). |
|
|
388
|
+
| `datefmt` | `%d/%m/%Y %H:%M:%S` | Timestamp format. |
|
|
389
|
+
| `elapsed_style` | `"adaptive"` | `"adaptive"` (µs→h) or `"seconds"` (always seconds). |
|
|
390
|
+
|
|
391
|
+
The package also exposes `DoneloggerFormatter`, `DoneloggerStreamHandler`, and
|
|
392
|
+
`LoggerManager` for advanced/custom wiring. `DoneloggerFormatter` is a drop-in
|
|
393
|
+
`logging.Formatter` (the standard `fmt` / `datefmt` / `style` arguments still
|
|
394
|
+
work), with one extra keyword-only argument, `elapsed_style`.
|
|
395
|
+
|
|
396
|
+
## How it works
|
|
397
|
+
|
|
398
|
+
donelogger installs a custom `logging.Formatter` that inspects each `INFO`
|
|
399
|
+
message. A `[Start]`/`[Go]` marker records `time.perf_counter()` under the tag;
|
|
400
|
+
the matching `[Done]` looks it up, computes the delta, and rewrites the line
|
|
401
|
+
with the elapsed time. Because it's all in the formatter, your call sites stay
|
|
402
|
+
plain `logger.info(...)` calls and non-marker logging is unaffected.
|
|
403
|
+
|
|
404
|
+
## Testing
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
python -m unittest discover
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
The suite is dependency-free and mocks `time.perf_counter`, so the timing
|
|
411
|
+
assertions are deterministic (no `sleep`, no flakiness).
|
|
412
|
+
|
|
413
|
+
## License
|
|
414
|
+
|
|
415
|
+
[MIT](LICENSE)
|