App logging setup. One call wires up a rotating, gzip'd run.log with console output.
Go to file
disqualifier 84e1744d6f fix: never crash on a bad level= string (v0.1.1)
_level_value used logging.getLevelName(name), which returns the string 'Level XXX'
for an unknown name; that string then reached setLevel() and raised ValueError,
violating the 'never crashes the app over logging' contract. validate the result is
an int and fall back to INFO otherwise.

verified: level='BOGUS' -> INFO (no crash); 'DEBUG' and int levels still honored.
Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-27 21:49:20 -04:00
src/log_setup fix: never crash on a bad level= string (v0.1.1) 2026-06-27 21:49:20 -04:00
.gitignore init: stdlib app-entry-point logging setup (live run.log, rotation, gzip, retention) 2026-06-27 20:21:02 -04:00
pyproject.toml fix: never crash on a bad level= string (v0.1.1) 2026-06-27 21:49:20 -04:00
README.md init: stdlib app-entry-point logging setup (live run.log, rotation, gzip, retention) 2026-06-27 20:21:02 -04:00

log_setup

Stdlib, sync, zero-dependency logging setup an application calls once at its entry point: a live run.log, rotation (daily / size / on-start), gzip of rolled files, retention, console output, and a consistent time | module | level | message format.

It configures logging (handlers, rotation, format) — which reusable libraries here must never do. That's fine because log_setup is the application's entry-point setup, not library-internal config. Libraries still only logging.getLogger(__name__) and emit; their records flow into the handlers log_setup wired.

Install

log_setup @ git+ssh://git@git.rethinkstudios.io/rethink-public/log_setup.git@v0.1.0

No dependencies — stdlib only.

Quick start

import logging
from log_setup import setup_logging

setup_logging(name="run", level="INFO")     # daily rotation, logs/ dir, gzip (file only)

log = logging.getLogger(__name__)
log.info("started")                          # -> ./run.log  (add console=True for stdout too)

Call it once, at the app's entry point — before the rest of the app runs. Every module (yours and the libraries you import) then just does logging.getLogger(__name__) and emits; the records land in the configured root.

What you get

  • Live file at a stable path: ./run.log — always tail -f run.log, no dated name to chase. Rolled/compressed copies go into log_dir (default logs/).
  • Format: 2026-06-27 19:55:05 | module.name | INFO | message. %(name)s is the getLogger name each module used, so you see which lib/module logged.
  • Rotation (rotate=):
    • "daily" (default) — rolls at midnight, dated name into log_dir, keeps backup_count days.
    • "size" — rolls at max_bytes, numbered backups in log_dir.
    • "on_start" — on startup, moves an existing run.log into log_dir (run.<timestamp>.log[.gz]) and starts fresh; prunes to backup_count.
    • None — single file, no rotation.
  • compress=True (default) gzips each rolled file (run.log.2026-06-27.gz).
  • Retention = backup_count (default 14) for every mode.
  • console=True (off by default) also logs to stdout in the same format — opt in when you want live terminal output alongside the file.

Signature

setup_logging(
    name="run",               # base -> run.log (the live file at cwd)
    log_dir="logs",           # rotated/compressed copies live here (created if absent)
    level="INFO",             # root level (str name or logging constant)
    rotate="daily",           # "daily" | "size" | "on_start" | None
    backup_count=14,          # rotated files to keep (older auto-deleted)
    max_bytes=10_000_000,     # only for rotate="size"
    compress=True,            # gzip rolled files
    console=False,            # also log to stdout (off by default; opt in)
    queue=False,              # route through a background QueueListener (async-friendly)
    fmt=None,                 # override the format string
    datefmt=None,             # override the date format
) -> logging.Logger           # returns the configured root logger

Async-friendly (queue=True)

For async-heavy apps, queue=True routes records through a stdlib QueueHandler to a background QueueListener that owns the file/console handlers, so the event loop never blocks on file I/O. The API stays sync (log.info() as usual); the queue is internal. The listener is stopped (and flushed) cleanly at process exit, so no records are lost.

setup_logging(name="run", queue=True)

Safety

  • Idempotent: calling setup_logging again clears only the handlers it added (no duplicate lines) and leaves handlers your app added itself alone.
  • Never crashes the app over logging: if log_dir isn't writable, it falls back to console-only with a warning instead of raising.

Scope — what this is NOT

log_setup produces clean, rotating, compressed, retention-managed, consistently formatted files. It does not ship logs anywhere — no Loki/ELK/syslog/network handlers. Getting files to a backend is a separate concern (e.g. Promtail tails run.log → Loki → Grafana panels + alerting). Keeping shipping out means the log backend can change without touching any app, and the consistent format here is what makes downstream parsing and alerting easy.

Also out of v0.1.0 (possible later additions): structured/JSON logging, color formatting, per-logger filters, remote handlers.

Versioning

Tagged vX.Y.Z. Pin the tag.