2019-04-01 13:07:22 +00:00
|
|
|
"""Global logging helpers."""
|
|
|
|
|
2019-03-28 15:54:45 +00:00
|
|
|
import logging
|
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
|
|
|
|
import colorama
|
2019-04-01 13:07:22 +00:00
|
|
|
from pythonjsonlogger import jsonlogger
|
|
|
|
|
2020-04-06 07:42:49 +00:00
|
|
|
CONSOLE_FORMAT = "{}%(levelname)s:{} %(message)s"
|
2021-10-07 21:17:27 +00:00
|
|
|
JSON_FORMAT = "%(asctime)s %(levelname)s %(message)s"
|
2019-04-03 15:42:46 +00:00
|
|
|
|
2019-03-28 15:54:45 +00:00
|
|
|
|
2023-10-16 10:16:44 +00:00
|
|
|
def strtobool(value):
|
|
|
|
"""Convert a string representation of truth to true or false."""
|
|
|
|
|
|
|
|
_map = {
|
|
|
|
"y": True,
|
|
|
|
"yes": True,
|
|
|
|
"t": True,
|
|
|
|
"true": True,
|
|
|
|
"on": True,
|
|
|
|
"1": True,
|
|
|
|
"n": False,
|
|
|
|
"no": False,
|
|
|
|
"f": False,
|
|
|
|
"false": False,
|
|
|
|
"off": False,
|
2023-11-10 13:50:48 +00:00
|
|
|
"0": False,
|
2023-10-16 10:16:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
try:
|
|
|
|
return _map[str(value).lower()]
|
|
|
|
except KeyError as err:
|
|
|
|
raise ValueError(f'"{value}" is not a valid bool value') from err
|
|
|
|
|
|
|
|
|
2020-02-03 23:03:26 +00:00
|
|
|
def to_bool(string):
|
|
|
|
return bool(strtobool(str(string)))
|
|
|
|
|
|
|
|
|
2019-04-01 13:07:22 +00:00
|
|
|
def _should_do_markup():
|
|
|
|
py_colors = os.environ.get("PY_COLORS", None)
|
2019-03-28 15:54:45 +00:00
|
|
|
if py_colors is not None:
|
2020-02-03 23:12:03 +00:00
|
|
|
return to_bool(py_colors)
|
2019-03-28 15:54:45 +00:00
|
|
|
|
2019-04-01 13:07:22 +00:00
|
|
|
return sys.stdout.isatty() and os.environ.get("TERM") != "dumb"
|
2019-03-28 15:54:45 +00:00
|
|
|
|
|
|
|
|
2020-04-06 06:34:25 +00:00
|
|
|
colorama.init(autoreset=True, strip=(not _should_do_markup()))
|
2019-03-28 15:54:45 +00:00
|
|
|
|
2019-04-03 21:39:27 +00:00
|
|
|
|
2019-04-05 10:23:18 +00:00
|
|
|
def flag_extra(extra):
|
|
|
|
"""Ensure extra args are prefixed."""
|
2023-02-10 07:51:17 +00:00
|
|
|
flagged = {}
|
2019-04-11 09:57:30 +00:00
|
|
|
|
|
|
|
if isinstance(extra, dict):
|
2021-08-08 19:25:03 +00:00
|
|
|
for key, value in extra.items():
|
2019-04-11 09:57:30 +00:00
|
|
|
flagged["later_" + key] = value
|
2019-04-03 21:39:27 +00:00
|
|
|
|
|
|
|
return flagged
|
2019-04-03 15:42:46 +00:00
|
|
|
|
|
|
|
|
2023-02-10 07:51:17 +00:00
|
|
|
class LogFilter:
|
2019-04-01 13:07:22 +00:00
|
|
|
"""A custom log filter which excludes log messages above the logged level."""
|
2019-03-28 15:54:45 +00:00
|
|
|
|
|
|
|
def __init__(self, level):
|
2019-04-01 13:07:22 +00:00
|
|
|
"""
|
|
|
|
Initialize a new custom log filter.
|
|
|
|
|
|
|
|
:param level: Log level limit
|
|
|
|
:returns: None
|
|
|
|
|
|
|
|
"""
|
2019-03-28 15:54:45 +00:00
|
|
|
self.__level = level
|
|
|
|
|
2019-04-01 13:07:22 +00:00
|
|
|
def filter(self, logRecord): # noqa
|
2019-03-28 15:54:45 +00:00
|
|
|
# https://docs.python.org/3/library/logging.html#logrecord-attributes
|
|
|
|
return logRecord.levelno <= self.__level
|
|
|
|
|
|
|
|
|
2019-04-05 10:23:18 +00:00
|
|
|
class MultilineFormatter(logging.Formatter):
|
|
|
|
"""Logging Formatter to reset color after newline characters."""
|
|
|
|
|
2024-01-15 14:14:16 +00:00
|
|
|
def format(self, record):
|
2023-01-09 10:59:25 +00:00
|
|
|
record.msg = record.msg.replace("\n", f"\n{colorama.Style.RESET_ALL}... ")
|
2020-04-05 13:24:51 +00:00
|
|
|
record.msg = record.msg + "\n"
|
2019-04-05 10:23:18 +00:00
|
|
|
return logging.Formatter.format(self, record)
|
|
|
|
|
|
|
|
|
|
|
|
class MultilineJsonFormatter(jsonlogger.JsonFormatter):
|
|
|
|
"""Logging Formatter to remove newline characters."""
|
|
|
|
|
2024-01-15 14:14:16 +00:00
|
|
|
def format(self, record):
|
2019-04-05 10:23:18 +00:00
|
|
|
record.msg = record.msg.replace("\n", " ")
|
|
|
|
return jsonlogger.JsonFormatter.format(self, record)
|
|
|
|
|
|
|
|
|
2019-03-28 15:54:45 +00:00
|
|
|
def get_logger(name=None, level=logging.DEBUG, json=False):
|
|
|
|
"""
|
|
|
|
Build a logger with the given name and returns the logger.
|
2019-04-01 13:07:22 +00:00
|
|
|
|
|
|
|
:param name: The name for the logger. This is usually the module name, `__name__`.
|
|
|
|
:param level: Initialize the new logger with given log level.
|
|
|
|
:param json: Boolean flag to enable json formatted log output.
|
2019-03-28 15:54:45 +00:00
|
|
|
:return: logger object
|
|
|
|
|
2019-04-01 13:07:22 +00:00
|
|
|
"""
|
2019-03-28 15:54:45 +00:00
|
|
|
logger = logging.getLogger(name)
|
|
|
|
logger.setLevel(level)
|
|
|
|
logger.addHandler(_get_error_handler(json=json))
|
|
|
|
logger.addHandler(_get_warn_handler(json=json))
|
|
|
|
logger.addHandler(_get_info_handler(json=json))
|
2019-04-02 14:34:03 +00:00
|
|
|
logger.addHandler(_get_critical_handler(json=json))
|
2019-03-28 15:54:45 +00:00
|
|
|
logger.propagate = False
|
|
|
|
|
|
|
|
return logger
|
|
|
|
|
|
|
|
|
2019-04-03 15:42:46 +00:00
|
|
|
def update_logger(logger, level=None, json=None):
|
2019-04-04 14:06:18 +00:00
|
|
|
"""Update logger configuration to change logging settings."""
|
2019-04-03 15:42:46 +00:00
|
|
|
for handler in logger.handlers[:]:
|
|
|
|
logger.removeHandler(handler)
|
|
|
|
|
|
|
|
logger.setLevel(level)
|
|
|
|
logger.addHandler(_get_error_handler(json=json))
|
|
|
|
logger.addHandler(_get_warn_handler(json=json))
|
|
|
|
logger.addHandler(_get_info_handler(json=json))
|
|
|
|
logger.addHandler(_get_critical_handler(json=json))
|
|
|
|
|
|
|
|
|
2019-03-28 15:54:45 +00:00
|
|
|
def _get_error_handler(json=False):
|
|
|
|
handler = logging.StreamHandler(sys.stderr)
|
|
|
|
handler.setLevel(logging.ERROR)
|
|
|
|
handler.addFilter(LogFilter(logging.ERROR))
|
2019-04-05 10:23:18 +00:00
|
|
|
handler.setFormatter(MultilineFormatter(error(CONSOLE_FORMAT)))
|
2019-03-28 15:54:45 +00:00
|
|
|
|
|
|
|
if json:
|
2019-04-05 10:23:18 +00:00
|
|
|
handler.setFormatter(MultilineJsonFormatter(JSON_FORMAT))
|
2019-03-28 15:54:45 +00:00
|
|
|
|
|
|
|
return handler
|
|
|
|
|
|
|
|
|
|
|
|
def _get_warn_handler(json=False):
|
|
|
|
handler = logging.StreamHandler(sys.stdout)
|
|
|
|
handler.setLevel(logging.WARN)
|
|
|
|
handler.addFilter(LogFilter(logging.WARN))
|
2019-04-05 10:23:18 +00:00
|
|
|
handler.setFormatter(MultilineFormatter(warn(CONSOLE_FORMAT)))
|
2019-03-28 15:54:45 +00:00
|
|
|
|
|
|
|
if json:
|
2019-04-05 10:23:18 +00:00
|
|
|
handler.setFormatter(MultilineJsonFormatter(JSON_FORMAT))
|
2019-03-28 15:54:45 +00:00
|
|
|
|
|
|
|
return handler
|
|
|
|
|
|
|
|
|
|
|
|
def _get_info_handler(json=False):
|
2019-04-11 13:56:20 +00:00
|
|
|
handler = logging.StreamHandler(sys.stdout)
|
2019-03-28 15:54:45 +00:00
|
|
|
handler.setLevel(logging.INFO)
|
|
|
|
handler.addFilter(LogFilter(logging.INFO))
|
2019-04-11 13:56:20 +00:00
|
|
|
handler.setFormatter(MultilineFormatter(info(CONSOLE_FORMAT)))
|
2019-03-28 15:54:45 +00:00
|
|
|
|
|
|
|
if json:
|
2019-04-05 10:23:18 +00:00
|
|
|
handler.setFormatter(MultilineJsonFormatter(JSON_FORMAT))
|
2019-03-28 15:54:45 +00:00
|
|
|
|
|
|
|
return handler
|
|
|
|
|
|
|
|
|
2019-04-02 14:34:03 +00:00
|
|
|
def _get_critical_handler(json=False):
|
|
|
|
handler = logging.StreamHandler(sys.stderr)
|
|
|
|
handler.setLevel(logging.CRITICAL)
|
|
|
|
handler.addFilter(LogFilter(logging.CRITICAL))
|
2019-04-05 10:23:18 +00:00
|
|
|
handler.setFormatter(MultilineFormatter(critical(CONSOLE_FORMAT)))
|
2019-04-02 14:34:03 +00:00
|
|
|
|
|
|
|
if json:
|
2019-04-05 10:23:18 +00:00
|
|
|
handler.setFormatter(MultilineJsonFormatter(JSON_FORMAT))
|
2019-04-02 14:34:03 +00:00
|
|
|
|
|
|
|
return handler
|
|
|
|
|
|
|
|
|
|
|
|
def critical(message):
|
|
|
|
"""Format critical messages and return string."""
|
2020-04-06 06:34:25 +00:00
|
|
|
return color_text(colorama.Fore.RED, message)
|
2019-03-28 15:54:45 +00:00
|
|
|
|
|
|
|
|
|
|
|
def error(message):
|
2019-04-01 13:07:22 +00:00
|
|
|
"""Format error messages and return string."""
|
2020-04-06 06:34:25 +00:00
|
|
|
return color_text(colorama.Fore.RED, message)
|
2019-03-28 15:54:45 +00:00
|
|
|
|
|
|
|
|
|
|
|
def warn(message):
|
2019-04-01 13:07:22 +00:00
|
|
|
"""Format warn messages and return string."""
|
2020-04-06 06:34:25 +00:00
|
|
|
return color_text(colorama.Fore.YELLOW, message)
|
2019-03-28 15:54:45 +00:00
|
|
|
|
|
|
|
|
|
|
|
def info(message):
|
2019-04-01 13:07:22 +00:00
|
|
|
"""Format info messages and return string."""
|
2020-04-06 06:34:25 +00:00
|
|
|
return color_text(colorama.Fore.BLUE, message)
|
2019-03-28 15:54:45 +00:00
|
|
|
|
|
|
|
|
2020-04-06 06:34:25 +00:00
|
|
|
def color_text(color, msg):
|
2019-04-01 13:07:22 +00:00
|
|
|
"""
|
|
|
|
Colorize strings.
|
|
|
|
|
|
|
|
:param color: colorama color settings
|
|
|
|
:param msg: string to colorize
|
|
|
|
:returns: string
|
|
|
|
|
|
|
|
"""
|
2020-04-06 07:42:49 +00:00
|
|
|
msg = msg.format(colorama.Style.BRIGHT, colorama.Style.NORMAL)
|
2023-01-09 10:59:25 +00:00
|
|
|
return f"{color}{msg}{colorama.Style.RESET_ALL}"
|