Source code for meeg_utils.logger
"""Logging for meeg-utils.
meeg-utils logs through `loguru <https://loguru.readthedocs.io>`_ but, like any
library, configures nothing on import: its messages are disabled until you
opt in, and it never adds, removes or changes handlers you did not ask for.
Turn logging on with :func:`setup_logging`, or, if you configure loguru
yourself, with ``logger.enable("meeg_utils")``.
"""
from __future__ import annotations
import sys
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path
from loguru import logger
logger.disable("meeg_utils")
# Handler ids added by setup_logging, so that it only ever removes its own.
_HANDLER_IDS: list[int] = []
_CONSOLE_FORMAT = (
"<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | "
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>"
)
_FILE_FORMAT = "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}"
[docs]
def setup_logging(
level: str | None = "INFO",
log_file: str | Path | None = None,
file_level: str = "DEBUG",
*,
enqueue: bool = False,
) -> None:
"""Show meeg-utils log messages on the console and/or in a file.
Calling it again replaces the handlers added by the previous call.
Handlers you added yourself are left alone; loguru's built-in default
stderr handler is removed so that messages are not printed twice.
Parameters
----------
level : str | None
Minimum level shown on the console (stderr), e.g. ``"DEBUG"``,
``"INFO"`` or ``"WARNING"``. ``None`` disables console output.
log_file : str | Path | None
File to write log messages to; parent directories are created.
``None`` (default) disables file logging.
file_level : str
Minimum level written to ``log_file``.
enqueue : bool
Pass messages through a multiprocessing-safe queue. Use it when
several processes write to the same file.
Examples
--------
>>> import meeg_utils as meu
>>> meu.setup_logging("INFO", log_file="logs/preproc.log") # doctest: +SKIP
"""
teardown_logging()
try:
logger.remove(0) # loguru's default stderr handler, if still present
except ValueError:
pass
logger.enable("meeg_utils")
if level is not None:
_HANDLER_IDS.append(
logger.add(
sys.stderr,
level=level,
format=_CONSOLE_FORMAT,
filter="meeg_utils",
enqueue=enqueue,
)
)
if log_file is not None:
log_file = Path(log_file)
log_file.parent.mkdir(parents=True, exist_ok=True)
_HANDLER_IDS.append(
logger.add(
log_file,
level=file_level,
format=_FILE_FORMAT,
filter="meeg_utils",
enqueue=enqueue,
backtrace=True,
diagnose=False, # do not dump local variables (may hold data) into files
)
)
[docs]
def teardown_logging() -> None:
"""Remove the handlers added by :func:`setup_logging` and silence meeg-utils again."""
while _HANDLER_IDS:
handler_id = _HANDLER_IDS.pop()
try:
logger.remove(handler_id)
except ValueError: # already removed by the user
pass
logger.disable("meeg_utils")
[docs]
@contextmanager
def log_to_file(log_file: str | Path, level: str = "DEBUG") -> Iterator[Path]:
"""Write meeg-utils log messages to a file for the duration of a ``with`` block.
Afterwards the file handler is removed and, unless :func:`setup_logging`
is active, meeg-utils is silenced again.
Parameters
----------
log_file : str | Path
File to write to; parent directories are created.
level : str
Minimum level written to the file.
Yields
------
Path
The log file.
"""
log_file = Path(log_file)
log_file.parent.mkdir(parents=True, exist_ok=True)
configured = bool(_HANDLER_IDS)
logger.enable("meeg_utils")
handler_id = logger.add(
log_file, level=level, format=_FILE_FORMAT, filter="meeg_utils", enqueue=True
)
try:
yield log_file
finally:
logger.remove(handler_id)
if not configured:
logger.disable("meeg_utils")