fix: console handler writes to stdout, not stderr

console=True now binds sys.stdout explicitly (was the stdlib StreamHandler
stderr default), so a supervisor capturing stdout sees console log lines.
console stays off by default (file is the primary sink). docs corrected to
stdout.

Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
2026-07-03 16:31:51 -04:00
parent 6fb245f690
commit 17d1d20865
4 changed files with 12 additions and 8 deletions
+9 -5
View File
@@ -13,12 +13,12 @@ and emit; their records flow into the handlers `log_setup` wired.
## Install ## Install
``` ```
log_setup @ git+ssh://git@git.rethinkstudios.io/rethink-public/log_setup.git@v0.6.2 log_setup @ git+ssh://git@git.rethinkstudios.io/rethink-public/log_setup.git@v0.6.3
``` ```
No dependencies — stdlib only. No dependencies — stdlib only.
Drop the `@v0.6.2` suffix from the line above to install the latest unpinned. Drop the `@v0.6.3` suffix from the line above to install the latest unpinned.
## Quick start ## Quick start
@@ -29,7 +29,7 @@ from log_setup import setup_logging
setup_logging(name="run", level="INFO") # daily rotation, logs/ dir, gzip (file only) setup_logging(name="run", level="INFO") # daily rotation, logs/ dir, gzip (file only)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.info("started") # -> ./run.log (add console=True for stderr too) 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 Call it once, at the app's entry point — before the rest of the app runs. Every module
@@ -57,7 +57,7 @@ emits; the records land in the configured root.
- **Retention** = `backup_count` (default 14) for every mode — unless tiered retention is - **Retention** = `backup_count` (default 14) for every mode — unless tiered retention is
enabled (below). For `rotate="size"`, `backup_count=0` is "keep none" (not "disable enabled (below). For `rotate="size"`, `backup_count=0` is "keep none" (not "disable
rotation") — see the `size` bullet above and the note at the bottom of this section. rotation") — see the `size` bullet above and the note at the bottom of this section.
- **console=True** (off by default) also logs to stderr in the same format — opt in when - **console=True** (off by default) also logs to stdout in the same format — opt in when
you want live terminal output alongside the file. you want live terminal output alongside the file.
The `name` you pass is normalized so it produces exactly one `.log`: `name="latest"` and The `name` you pass is normalized so it produces exactly one `.log`: `name="latest"` and
@@ -174,7 +174,7 @@ setup_logging(
keep_compressed=None, # tiered: next M rolled logs kept GZIPPED (opt-in) keep_compressed=None, # tiered: next M rolled logs kept GZIPPED (opt-in)
max_bytes=10_000_000, # only for rotate="size" max_bytes=10_000_000, # only for rotate="size"
compress=True, # gzip rolled files compress=True, # gzip rolled files
console=False, # also log to stderr (off by default; opt in) console=False, # also log to stdout (off by default; opt in)
queue=False, # route through a background QueueListener (async-friendly) queue=False, # route through a background QueueListener (async-friendly)
output="text", # "text" (human, local time) | "json" (structured, UTC) output="text", # "text" (human, local time) | "json" (structured, UTC)
fmt=None, # override the text format string (text mode only) fmt=None, # override the text format string (text mode only)
@@ -248,6 +248,10 @@ setup_logging(name="run", queue=True)
documented (keeps that many rolled files). This does not change `"daily"`/`"on_start"`, documented (keeps that many rolled files). This does not change `"daily"`/`"on_start"`,
where `backup_count=0` still means "roll, but don't prune the rolled files" (unbounded where `backup_count=0` still means "roll, but don't prune the rolled files" (unbounded
`log_dir` growth) — that is a separate, pre-existing knob, not this fix's scope. `log_dir` growth) — that is a separate, pre-existing knob, not this fix's scope.
- **`console=True` writes to stdout (v0.6.3).** the console handler now binds `sys.stdout`
explicitly instead of the stdlib `StreamHandler` default of `sys.stderr`, so a supervisor
capturing stdout sees console log lines. `console` stays off by default — the file is the
primary sink.
- **`"daily"` regression fixed (v0.6.2).** v0.5.1's `rotate="size"` fix above shared its - **`"daily"` regression fixed (v0.6.2).** v0.5.1's `rotate="size"` fix above shared its
rotator with `"daily"`, so a `rotate="daily", backup_count=0` roll was incorrectly rotator with `"daily"`, so a `rotate="daily", backup_count=0` roll was incorrectly
deleted at every midnight rollover instead of just landing unpruned. The rotator is now deleted at every midnight rollover instead of just landing unpruned. The rotator is now
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "log_setup" name = "log_setup"
version = "0.6.2" version = "0.6.3"
description = "stdlib app-entry-point logging setup: live run.log, rotation, gzip, retention, consistent format" description = "stdlib app-entry-point logging setup: live run.log, rotation, gzip, retention, consistent format"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [] dependencies = []
+1 -1
View File
@@ -13,4 +13,4 @@ from .setup import setup_logging
__all__ = ["setup_logging"] __all__ = ["setup_logging"]
__version__ = "0.6.2" __version__ = "0.6.3"
+1 -1
View File
@@ -278,7 +278,7 @@ def setup_logging(
file_ok = False file_ok = False
if console or not file_ok: if console or not file_ok:
sh = logging.StreamHandler() sh = logging.StreamHandler(sys.stdout)
sh.setFormatter(formatter) sh.setFormatter(formatter)
handlers.append(sh) handlers.append(sh)