Skip to content

Instantly share code, notes, and snippets.

@cheginit
Last active December 30, 2025 21:22
Show Gist options
  • Select an option

  • Save cheginit/61e6f5a57eabc2ac8be49ae6b7fc0b75 to your computer and use it in GitHub Desktop.

Select an option

Save cheginit/61e6f5a57eabc2ac8be49ae6b7fc0b75 to your computer and use it in GitHub Desktop.
A Python logger with optional support for Rich
"""Logging utilities."""
from __future__ import annotations
import logging
import sys
from pathlib import Path
from typing import Literal
__all__ = ["configure_logger", "logger"]
logger = logging.getLogger("package_name")
logger.setLevel(logging.DEBUG) # Let handlers control filtering
logger.propagate = False
_file_handler: logging.FileHandler | None = None
_console_handler: logging.Handler | None = None
if not logger.handlers:
try:
from rich.console import Console # pyright: ignore[reportMissingImports]
from rich.logging import RichHandler # pyright: ignore[reportMissingImports]
_console_handler = RichHandler(
console=Console(stderr=True, force_jupyter=False, width=120),
show_time=True,
show_level=True,
show_path=False,
rich_tracebacks=True,
tracebacks_show_locals=True,
log_time_format="[%Y/%m/%d %H:%M:%S]",
omit_repeated_times=False,
)
_console_handler.setFormatter(logging.Formatter("%(message)s"))
except ImportError:
_console_handler = logging.StreamHandler(sys.stdout)
_console_handler.setFormatter(
logging.Formatter(
fmt="[%(asctime)s] %(levelname)s\t%(message)s", datefmt="%Y/%m/%d %H:%M:%S"
)
)
_console_handler.setLevel(logging.WARNING)
logger.addHandler(_console_handler)
def _validate_level(level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | int) -> int:
"""Validate and convert logging level to integer.
Parameters
----------
level : str or int
Logging level as string or integer.
Returns
-------
int
Validated logging level as integer constant.
Raises
------
ValueError
If the provided level is not valid.
TypeError
If the provided level is not a string or integer.
"""
if isinstance(level, str):
level_upper = level.upper()
if level_upper not in ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"):
msg = (
f"Invalid log level: {level}. Must be one of: DEBUG, INFO, WARNING, ERROR, CRITICAL"
)
raise ValueError(msg)
return getattr(logging, level_upper)
if isinstance(level, int):
if level not in (
logging.DEBUG,
logging.INFO,
logging.WARNING,
logging.ERROR,
logging.CRITICAL,
):
msg = f"Invalid log level: {level}. Must be a valid logging level constant."
raise ValueError(msg)
return level
msg = f"Level must be str or int, got {type(level).__name__}"
raise TypeError(msg)
def configure_logger(
*,
verbose: bool | None = None,
level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | int | None = None,
file: str | Path | None = None,
file_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | int | None = None,
file_mode: Literal["a", "w"] = "a",
) -> None:
"""Configure logging settings.
Parameters
----------
verbose : bool, optional
Shortcut to set console level to DEBUG (True) or WARNING (False).
If both ``level`` and ``verbose`` are provided, ``level`` takes precedence.
level : str or int, optional
Set console logging level. Valid options: ``DEBUG``, ``INFO``, ``WARNING``,
``ERROR``, ``CRITICAL``, or their integer equivalents.
file : str or Path, optional
Path to log file. If provided, enables file logging.
Pass ``None`` to disable file logging.
file_level : str or int, optional
Logging level for file handler. Defaults to DEBUG if not specified.
file_mode : {'a', 'w'}, optional
File mode: 'a' for append, 'w' for overwrite. Defaults to 'a'.
Raises
------
ValueError
If invalid level or file_level is provided.
TypeError
If level or file_level is not a string or integer.
Notes
-----
The logger itself is set to DEBUG level, allowing handlers to independently
control what messages they receive. This means file logging can capture
DEBUG messages even when console is set to WARNING.
Examples
--------
>>> # Enable verbose logging
>>> configure_logger(verbose=True)
>>> # Set specific level
>>> configure_logger(level="INFO")
>>> # Enable file logging (captures all levels by default)
>>> configure_logger(verbose=True, file="debug.log")
>>> # Console shows warnings, file captures everything
>>> configure_logger(verbose=False, file="full.log", file_level="DEBUG")
>>> # File logging with custom level
>>> configure_logger(file="errors.log", file_level="ERROR", file_mode="w")
>>> # Disable file logging
>>> configure_logger(file=None)
"""
global _file_handler # noqa: PLW0603
if level is not None:
level_int = _validate_level(level)
if _console_handler is not None:
_console_handler.setLevel(level_int)
elif verbose is not None:
if _console_handler is not None:
_console_handler.setLevel(logging.DEBUG if verbose else logging.WARNING)
if file is not None:
if _file_handler is not None:
logger.removeHandler(_file_handler)
_file_handler.close()
_file_handler = None
file_level_int = _validate_level(file_level) if file_level is not None else logging.DEBUG
filepath = Path(file)
filepath.parent.mkdir(parents=True, exist_ok=True)
if file_mode not in ("a", "w"):
raise ValueError(f"Invalid file_mode: {file_mode}. Must be 'a' or 'w'.")
_file_handler = logging.FileHandler(filepath, mode=file_mode)
_file_handler.setLevel(file_level_int)
_file_handler.setFormatter(
logging.Formatter(
fmt="[%(asctime)s] %(levelname)-8s %(message)s", datefmt="%Y/%m/%d %H:%M:%S"
)
)
logger.addHandler(_file_handler)
elif file is None and _file_handler is not None:
logger.removeHandler(_file_handler)
_file_handler.close()
_file_handler = None
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment