Skip to main content

Time long-running steps with two ordinary log lines: write [Start] and [Done], get the elapsed time filled in for you.

Project description

donelogger

Time long-running steps with two ordinary log lines.

donelogger in action

Write [Start] when work begins and [Done] when it ends. donelogger fills the elapsed time into the log line for you.

from donelogger import getLogger

logger = getLogger()

logger.info("[Start] Training model...")
train()
logger.info("[Done] Finished")
# -> +[Go Job] Training model...
# -> -[Done Job(1m23.40s)] Finished     ← elapsed time, measured for you

That's the whole idea: no time.perf_counter() variables, no manual subtraction, no f"{...:.3f}s" formatting scattered through your code.

If you've ever written this:

t0 = time.perf_counter()
logger.info("Loading dataset...")
load_dataset()
logger.info(f"Finished loading in {time.perf_counter() - t0:.3f}s")

you can write this instead:

logger.info("[Start] Loading dataset...")
load_dataset()
logger.info("[Done] Finished loading")
# -> -[Done Job(2.413s)] Finished loading

The timing is just part of the log. Your call sites stay plain logger.info(...), and everything that is not a marker remains a normal log message.

No tag needed for the basic case. When you want to time nested or overlapping steps, add an optional :tag ([Start:train] ... [Done:train]).

Battle-tested: donelogger runs in production internal tooling, where knowing "how long did each stage take?" across a long pipeline matters every day.


Why donelogger?

1. Make timing cheap enough to use everywhere

When timing is annoying, you only add it after something gets slow. donelogger makes it cheap enough to leave timing breadcrumbs throughout a script, CLI, batch job, ML run, or data pipeline:

logger.info("[Start] Download files")
download_files()
logger.info("[Done] Downloaded")

logger.info("[Start] Parse records")
parse_records()
logger.info("[Done] Parsed")

logger.info("[Start] Write output")
write_output()
logger.info("[Done] Wrote output")

You get readable progress logs while the job runs, and elapsed times once each step finishes.

2. Stop subtracting timestamps by hand

The usual timing pattern is repetitive and easy to get slightly wrong:

t0 = time.perf_counter()
logger.info("Loading dataset...")
load_dataset()
logger.info(f"Finished loading in {time.perf_counter() - t0:.3f}s")

donelogger keeps the stopwatch attached to the log line instead of your local variables:

logger.info("[Start] Loading dataset...")
load_dataset()
logger.info("[Done] Finished loading")

This really pays off once timings are nested. You often want an inner step's time and the whole job's time. By the time the job ends, the start line has scrolled far up the log; squinting at two timestamps to subtract them is exactly the chore donelogger removes. (See nested timing.)

3. Skip the logging setup boilerplate

Getting plain logging to print the way you want takes a handler, a formatter, a level, and a few lines of wiring before a single line shows up:

# Before — standard logging needs setup before it's usable
import logging, sys
logger = logging.getLogger("myapp")
logger.setLevel(logging.INFO)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter("%(asctime)s|%(levelname)s|%(message)s",
                                        "%d/%m/%Y %H:%M:%S"))
logger.addHandler(handler)

getLogger() does all of that for you — and hands back a real logging.Logger, so levels, file output, and custom formats keep working exactly as you'd expect:

# After — configured and ready in one line
from donelogger import getLogger
logger = getLogger()                      # console-ready, sensible defaults
logger = getLogger(logfile="app.log")     # ...also writes to a rotating file

Nothing proprietary to learn: it's logging underneath, just without the setup.

Features

  • Zero-boilerplate timing — wrap work in [Start] / [Done] and get the elapsed time for free; no tag required.
  • One-line setupgetLogger() returns a ready-to-use logger (handler, formatter, and level already wired) — no logging boilerplate.
  • Reads like normal logs — markers are just text at the front of your message; nothing new to learn.
  • Named tags — time overlapping or nested stages independently (total, load, train, …).
  • Cross-module — start a timer in one file and finish it in another, as long as they share a logger name.
  • Human-friendly durations — adaptive units from microseconds to hours (300us, 512.0ms, 1.003s, 1m15.40s, 1h15m00s), or force fixed seconds.
  • Drop-in logginggetLogger() returns a real logging.Logger; all the usual .info() / .warning() / .error() work unchanged.
  • Optional rotating file log — one argument enables a RotatingFileHandler with a detailed format.
  • Zero dependencies — pure standard library, built on logging.Formatter.

Installation

pip install git+https://github.com/rkskmt/donelogger.git

Or install locally for development:

git clone https://github.com/rkskmt/donelogger.git
cd donelogger
pip install -e .

Quick Start

import time
from donelogger import getLogger

logger = getLogger()

# The basics: bare [Start] / [Done], no tag.
logger.info("[Start] Loading dataset...")
time.sleep(2)
logger.info("[Done] Finished loading")
# -> -[Done Job(2.001s)] Finished loading

# Need to time several things at once? Add an optional tag.
logger.info("[Start:train] Training model")
time.sleep(1)
logger.info("[Done:train]")
# -> -[Done train(1.002s)]

Usage

Default timer (no tag required)

Tags are optional. Bare [Start] / [Done] use a default timer named Job:

logger.info("[Start] Processing")
# ... work ...
logger.info("[Done] Complete")
# -> -[Done Job(512.0ms)] Complete

Named tags: time nested or overlapping work

Bare [Start] / [Done] track one thing at a time. Add a :tag to run several timers at once — ideal when you want an inner step's time and the overall time, without scrolling back up the log to subtract timestamps by hand:

logger.info("[Start:total] Pipeline starting")
logger.info("[Start:load]  Loading dataset...")
load_dataset()
logger.info("[Done:load]   Data ready")          # inner step time
train()
logger.info("[Done:total]  Pipeline finished")   # whole-pipeline time
# -> -[Done load(2.001s)] Data ready
# -> -[Done total(1m25.40s)] Pipeline finished

Timers are independent, so tags can nest (as above) or overlap freely without clobbering each other:

logger.info("[Start:download] Downloading files")
logger.info("[Start:parse]    Parsing config")
logger.info("[Done:parse]     Config ready")     # parse stops first
logger.info("[Done:download]  Files saved")      # download stops later

Cross-module timing

getLogger(name=...) returns the same logger instance for a given name (process-wide singleton), and the timer state lives on that logger. So any module that calls getLogger() with the same name shares the same timers — start in one file, finish in another:

# === data_loader.py ===
from donelogger import getLogger
getLogger().info("[Start:pipeline] Begin data pipeline")

# === trainer.py ===
from donelogger import getLogger
# ... after all stages finish ...
getLogger().info("[Done:pipeline] Pipeline complete")
# -> -[Done pipeline(42.300s)] Pipeline complete

Loggers created with different names keep independent timers, so unrelated components never clobber each other's tags.

Regular logging

Everything that isn't a marker passes straight through — donelogger is a normal logger:

logger.info("Just a normal message")
logger.warning("This is a warning")
logger.error("Something went wrong")

(Only INFO-level messages are scanned for markers; other levels are never touched.)

Choosing the elapsed-time format

elapsed_style controls how durations are rendered (default "adaptive"):

logger = getLogger(elapsed_style="adaptive")  # 300us, 512.0ms, 1.003s, 1m15.40s, 1h15m00s
logger = getLogger(elapsed_style="seconds")   # always seconds: 0.300s, 0.512s, 1.003s, 75.400s

File logging

Pass logfile= to also write to a rotating file (1 MB × 2 backups) with a detailed, machine-friendly format:

logger = getLogger(name="myapp", logfile="app.log")

Full configuration

import logging
from donelogger import getLogger

logger = getLogger(
    name="myapp",
    logLevel=logging.DEBUG,
    logfile="app.log",
    fmt="%(asctime)s|%(levelname)s|%(message)s",
    datefmt="%d/%m/%Y %H:%M:%S",
    elapsed_style="adaptive",
)

Marker syntax

Marker Meaning
[Start] / [Go] Start the default (Job) timer
[Start:tag] / [Go:tag] Start a named timer
[Done] Stop the default timer and print elapsed time
[Done:tag] Stop a named timer and print elapsed time
  • The keyword is case-insensitive (start, Start, go, Go, done, Done).
  • A marker is only recognized at the very beginning of the message.
  • [Done:tag] without a prior [Start:tag] emits *LOG ERROR* (tag is not started) {...} so mistakes are obvious.
  • A tag is not consumed on [Done], so you can stop the same tag more than once (each reports elapsed since its [Start]).

Output format

+[Go data_load] Loading dataset...      ← '+' = timer started
-[Done data_load(2.001s)] Finished      ← '-' = timer stopped, elapsed shown

adaptive (default) picks a unit by magnitude:

Duration Rendered
300 µs 300us
5 ms 5.0ms
0.512 s 512.0ms
1.003 s 1.003s
75.4 s 1m15.40s
4500 s 1h15m00s

seconds always uses seconds (handy when you post-process logs):

Duration Rendered
0.005 s 0.005s
75.4 s 1m15.400s
4500 s 75m00.000s

API

getLogger(name="doneLogger", logLevel=logging.INFO, logfile=None, fmt=..., datefmt=..., elapsed_style="adaptive")

Returns a configured logging.Logger. Calling it again with the same name returns the cached instance (so configuration only happens once).

Parameter Default Description
name "doneLogger" Logger name. Same name → same instance (and shared timers). "root" configures the root logger.
logLevel logging.INFO Level for the console handler / logger.
logfile None If set, also log to this file via a rotating handler (1 MB × 2 backups).
fmt %(asctime)s|%(levelname)s|%(message)s Console log format (standard logging format string).
datefmt %d/%m/%Y %H:%M:%S Timestamp format.
elapsed_style "adaptive" "adaptive" (µs→h) or "seconds" (always seconds).

The package also exposes DoneloggerFormatter, DoneloggerStreamHandler, and LoggerManager for advanced/custom wiring. DoneloggerFormatter is a drop-in logging.Formatter (the standard fmt / datefmt / style arguments still work), with one extra keyword-only argument, elapsed_style.

How it works

donelogger installs a custom logging.Formatter that inspects each INFO message. A [Start]/[Go] marker records time.perf_counter() under the tag; the matching [Done] looks it up, computes the delta, and rewrites the line with the elapsed time. Because it's all in the formatter, your call sites stay plain logger.info(...) calls and non-marker logging is unaffected.

Testing

python -m unittest discover

The suite is dependency-free and mocks time.perf_counter, so the timing assertions are deterministic (no sleep, no flakiness).

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

donelogger-0.1.7.tar.gz (13.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

donelogger-0.1.7-py3-none-any.whl (9.8 kB view details)

Uploaded Python 3

File details

Details for the file donelogger-0.1.7.tar.gz.

File metadata

  • Download URL: donelogger-0.1.7.tar.gz
  • Upload date:
  • Size: 13.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.10

File hashes

Hashes for donelogger-0.1.7.tar.gz
Algorithm Hash digest
SHA256 d343dd6bc1b193f46013566faab248427ab92e67685f28312950f33459b31247
MD5 90a099c3377bd3a1d086b456b3d7f5ae
BLAKE2b-256 5006981cfe0bf8a4a75e0095071fd55c7096160923dfef48f47f7a3bbc4c9a1b

See more details on using hashes here.

File details

Details for the file donelogger-0.1.7-py3-none-any.whl.

File metadata

  • Download URL: donelogger-0.1.7-py3-none-any.whl
  • Upload date:
  • Size: 9.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.10

File hashes

Hashes for donelogger-0.1.7-py3-none-any.whl
Algorithm Hash digest
SHA256 0ebac0ac2fdce97794112d36c97ffa549d020196fde6b48dbc5cec5c551b7bd8
MD5 b0957483975bf8aa2803f19d30c71eb8
BLAKE2b-256 fd9d28ea12b65a0358774c1085be338c61b7c8fd2d612a4c5faa9320b7fac374

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page