init: stdlib app-entry-point logging setup (live run.log, rotation, gzip, retention)
Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
commit
da8b86b258
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
# claude
|
||||
CLAUDE.md
|
||||
|
||||
# python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
.eggs/
|
||||
|
||||
# env
|
||||
.venv/
|
||||
venv/
|
||||
.env
|
||||
.pytest_cache/
|
||||
105
README.md
Normal file
105
README.md
Normal file
@ -0,0 +1,105 @@
|
||||
# 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
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```python
|
||||
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.
|
||||
|
||||
```python
|
||||
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.
|
||||
Loading…
Reference in New Issue
Block a user