|
Server : Apache System : Linux ecngx264.inmotionhosting.com 4.18.0-553.77.1.lve.el8.x86_64 #1 SMP Wed Oct 8 14:21:00 UTC 2025 x86_64 User : lonias5 ( 3576) PHP Version : 7.3.33 Disable Function : NONE Directory : /usr/lib/imh-whmapi/venv/lib/python3.13/site-packages/rads/ |
Upload File : |
"""Rads logging functions"""
from typing import Literal, IO
import sys
import os
from pathlib import Path
import logging
from logging.handlers import WatchedFileHandler
from .color import red, yellow
def setup_logging(
path: Path | str | None,
name: str | None = None,
fmt: str = '%(asctime)s %(levelname)s %(message)s',
datefmt: str = r'%Y-%m-%d %H:%M:%S',
multiline: bool = True,
loglevel: int | str = logging.DEBUG,
print_out: IO | Literal['stdout', 'stderr'] | None = None,
print_loglevel: int | str | None = None,
chown: tuple[int, int] | None = None,
chmod: int | None = None,
) -> logging.Logger:
"""Sets up and returns the root logger, or a named logger if ``name`` is set
Args:
path: file path to log to. If set to None, print_out must not be None.
name: logger name for logging.getLogger()
fmt: format for ``logging.Formatter``
datefmt: date format for ``logging.Formatter``
multiline: whether to support multiline logging
loglevel: loglevel for logging.setLevel - will accept a constant from
the logging module such as logging.INFO, or the string of the level
name, e.g. 'INFO'
print_out: set this to ``sys.stdout`` or ``sys.stderr`` to
also print there. Also accepts 'stdout' or 'stderr' as literal str
print_loglevel: optional separate log level for the ``print_out`` kwarg.
If unset, it will use the ``loglevel`` kwarg.
chown: ensure ownership of the log path
Only valid if path is set.
chmod: ensure perms of the log path *in octal*.
Only valid if path is set.
"""
if isinstance(loglevel, str):
loglevel = getattr(logging, loglevel.upper())
assert isinstance(loglevel, int)
if isinstance(print_loglevel, str):
print_loglevel = getattr(logging, print_loglevel.upper())
assert isinstance(print_loglevel, int)
if isinstance(print_out, str):
if print_out.lower() == 'stdout':
print_out = sys.stdout
elif print_out.lower() == 'stderr':
print_out = sys.stderr
else:
raise TypeError(print_out)
if path:
path = Path(path)
path.touch(mode=0o644 if chmod is None else chmod, exist_ok=True)
elif not print_out:
raise TypeError("At least one of 'path' and/or 'print_out' must be set")
if chmod is not None:
if not path:
raise TypeError("'path' must be set to use 'chmod'")
os.chmod(path, chmod)
if chown is not None:
if not isinstance(chown, tuple):
raise TypeError("'chown' must be a tuple")
if not path:
raise TypeError("'path' must be set to use 'chown'")
os.chown(path, chown[0], chown[1])
logger = logging.getLogger(name)
if multiline:
formatter = MultilineFormatter(fmt=fmt, datefmt=datefmt)
else:
formatter = logging.Formatter(fmt=fmt, datefmt=datefmt)
if path:
main_handler = WatchedFileHandler(path)
main_handler.setFormatter(formatter)
main_handler.setLevel(loglevel)
logger.addHandler(main_handler)
if print_out:
print_handler = logging.StreamHandler(stream=print_out)
print_handler.setFormatter(formatter)
print_handler.setLevel(print_loglevel or loglevel)
logger.addHandler(print_handler)
levels = [loglevel]
if print_loglevel is not None:
levels.append(print_loglevel)
logger.setLevel(min(levels))
return logger
setup_logging.__module__ = 'rads'
def setup_verbosity(
loglevel: int | str = 'DEBUG',
color: bool | None = None,
name: str | None = 'rads_verbosity',
):
"""Return a custom logger used to easily handle error and message printing.
debug & info: prints to stdout
warning: prints to stderr (in yellow, if enabled)
error & critical: prints to stderr (in red, if enabled)
Args:
loglevel: filter to only print up to this level.
This is especially useful to add --verbose/--quiet behavior
color: set this True or False to force colors on or off. If unset,
it will check if stderr is a TTY and enable colors if so
name: name of the logger for logging.getLogger()
Returns:
Logger: the configured Logger object
"""
if isinstance(loglevel, str):
loglevel = getattr(logging, loglevel.upper())
assert isinstance(loglevel, int)
logger = logging.getLogger(name)
stdout_handler = logging.StreamHandler(stream=sys.stdout)
stdout_handler.setFormatter(logging.Formatter(fmt='%(message)s'))
stdout_handler.addFilter(LevelFilter(logging.DEBUG, logging.INFO))
if color is None:
color = hasattr(sys.stderr, 'isatty') and sys.stdout.isatty()
if color:
err_fmt = logging.Formatter(fmt=red('%(message)s'))
warn_fmt = logging.Formatter(fmt=yellow('%(message)s'))
else:
warn_fmt = err_fmt = logging.Formatter(fmt='%(message)s')
warning_handler = logging.StreamHandler(stream=sys.stderr)
warning_handler.setFormatter(warn_fmt)
warning_handler.addFilter(LevelFilter(logging.WARNING, logging.WARNING))
error_handler = logging.StreamHandler(stream=sys.stderr)
error_handler.setFormatter(err_fmt)
error_handler.addFilter(LevelFilter(logging.ERROR, logging.CRITICAL))
logger.addHandler(stdout_handler)
logger.addHandler(warning_handler)
logger.addHandler(error_handler)
logger.setLevel(loglevel)
return logger
setup_verbosity.__module__ = 'rads'
class LevelFilter(logging.Filter):
"""Allows setting both a min and max log level via log.addFilter instead of
log.setLevel"""
__module__ = 'rads'
def __init__(self, low, high):
self._low = low
self._high = high
logging.Filter.__init__(self)
def filter(self, record):
if self._low <= record.levelno <= self._high:
return True
return False
class MultilineFormatter(logging.Formatter):
"""Subclass of logging.Formatter that can handle multiline strings.
rads.setup_logging() will use this by default unless multiline=False"""
__module__ = 'rads'
def format(self, record: logging.LogRecord):
if not self._fmt.endswith(' %(message)s'):
# Unsupported format. Just return normally
return super().format(record)
message = record.getMessage()
orig_msg, orig_args = record.msg, record.args
record.msg, record.args = '', ()
prefix = super().format(record)
record.msg, record.args = orig_msg, orig_args
out = []
for line in message.splitlines():
out.append(f"{prefix}{line}")
return '\n'.join(out)