fix: build_formatter routes the unknown-output warning through the setup buffer

build_formatter warned immediately via getLogger(__name__).warning, but
setup_logging calls it after _clear_owned strips the lib's handlers and before
any new handler attaches - so the warning hit a handler-less root and landed
on stderr via lastResort, never reaching the configured log. The v0.6.0
warnings buffer already threads this class of setup-time warning through for
an unknown rotate value; build_formatter now takes the same warnings list and
appends to it instead of emitting inline, so setup_logging can flush it once
handlers are attached like the others.

Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
2026-07-06 00:14:54 -04:00
parent 3454f73b6a
commit ca9bf4520b
2 changed files with 13 additions and 5 deletions
+12 -4
View File
@@ -54,7 +54,7 @@ class JsonLinesFormatter(logging.Formatter):
return json.dumps(payload, default=str) return json.dumps(payload, default=str)
def build_formatter(output: str = "text", fmt=None, datefmt=None) -> logging.Formatter: def build_formatter(output: str = "text", fmt=None, datefmt=None, warnings: list = None) -> logging.Formatter:
"""build the formatter for the chosen output format """build the formatter for the chosen output format
`output="text"` (default) returns the human-readable text formatter, honoring `output="text"` (default) returns the human-readable text formatter, honoring
@@ -62,11 +62,19 @@ def build_formatter(output: str = "text", fmt=None, datefmt=None) -> logging.For
structured `JsonLinesFormatter` (which ignores `fmt`/`datefmt` - it builds structured `JsonLinesFormatter` (which ignores `fmt`/`datefmt` - it builds
fields, not a format string). an unrecognized `output` falls back to text and fields, not a format string). an unrecognized `output` falls back to text and
warns, never raising - a bad format arg must not take the app down. warns, never raising - a bad format arg must not take the app down.
`warnings` is the setup-time buffer (see `setup_logging`): passing it appends
the unknown-output warning there instead of emitting immediately, so it lands
in the configured log like the other setup-time warnings rather than only
stderr. `None` (default) preserves the old immediate-emit behavior for direct
callers outside `setup_logging`.
""" """
if output == "json": if output == "json":
return JsonLinesFormatter() return JsonLinesFormatter()
if output != "text": if output != "text":
logging.getLogger(__name__).warning( message = "log_setup: unknown output %r; falling back to 'text'"
"log_setup: unknown output %r; falling back to 'text'", output if warnings is None:
) logging.getLogger(__name__).warning(message, output)
else:
warnings.append((message, output))
return logging.Formatter(fmt or DEFAULT_FORMAT, datefmt or DEFAULT_DATEFMT) return logging.Formatter(fmt or DEFAULT_FORMAT, datefmt or DEFAULT_DATEFMT)
+1 -1
View File
@@ -266,7 +266,7 @@ def setup_logging(
_apply_module_levels(module_levels, warnings) _apply_module_levels(module_levels, warnings)
_clear_owned(root, warnings) _clear_owned(root, warnings)
formatter = build_formatter(output, fmt, datefmt) formatter = build_formatter(output, fmt, datefmt, warnings)
stem = _normalize_name(name) stem = _normalize_name(name)
live_path = f"{stem}.log" live_path = f"{stem}.log"
# history_name if given, else the cwd basename; falls back to the live stem for a # history_name if given, else the cwd basename; falls back to the live stem for a