123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- import enum
- import logging
- import logging.handlers
- import os
- import sys
- def to_bool(arg):
- '''
- Return a boolean value based on `arg`.
- '''
- if arg is None or isinstance(arg, bool):
- return arg
- if isinstance(arg, str):
- arg = arg.lower()
- if arg in ('yes', 'on', '1', 'true', 1):
- return True
- return False
- def interactive_console():
- '''
- Return whether the current console is "interactive". Meaning: Capable of
- user input and not just something like a cron job.
- '''
- return sys.stderr.isatty() and os.environ.get('TERM') != 'dumb'
- def should_do_markup(no_color, configs):
- '''
- Given the value of the command-line no-color argument, and a dict of configuration filename to
- corresponding parsed configuration, determine if we should enable color marking up.
- '''
- if no_color:
- return False
- if any(config.get('color', True) is False for config in configs.values()):
- return False
- if os.environ.get('NO_COLOR', None):
- return False
- py_colors = os.environ.get('PY_COLORS', None)
- if py_colors is not None:
- return to_bool(py_colors)
- return interactive_console()
- class Multi_stream_handler(logging.Handler):
- '''
- A logging handler that dispatches each log record to one of multiple stream handlers depending
- on the record's log level.
- '''
- def __init__(self, log_level_to_stream_handler):
- super(Multi_stream_handler, self).__init__()
- self.log_level_to_handler = log_level_to_stream_handler
- self.handlers = set(self.log_level_to_handler.values())
- def flush(self): # pragma: no cover
- super(Multi_stream_handler, self).flush()
- for handler in self.handlers:
- handler.flush()
- def emit(self, record):
- '''
- Dispatch the log record to the appropriate stream handler for the record's log level.
- '''
- self.log_level_to_handler[record.levelno].emit(record)
- def setFormatter(self, formatter): # pragma: no cover
- super(Multi_stream_handler, self).setFormatter(formatter)
- for handler in self.handlers:
- handler.setFormatter(formatter)
- def setLevel(self, level): # pragma: no cover
- super(Multi_stream_handler, self).setLevel(level)
- for handler in self.handlers:
- handler.setLevel(level)
- class Console_no_color_formatter(logging.Formatter):
- def __init__(self, *args, **kwargs):
- super(Console_no_color_formatter, self).__init__('{prefix}{message}', style='{', defaults={'prefix': ''}, *args, **kwargs)
- class Color(enum.Enum):
- RESET = 0
- RED = 31
- GREEN = 32
- YELLOW = 33
- MAGENTA = 35
- CYAN = 36
- class Console_color_formatter(logging.Formatter):
- def __init__(self, *args, **kwargs):
- super(Console_color_formatter, self).__init__('{prefix}{message}', style='{', defaults={'prefix': ''}, *args, **kwargs)
- def format(self, record):
- add_custom_log_levels()
- color = (
- {
- logging.CRITICAL: Color.RED,
- logging.ERROR: Color.RED,
- logging.WARN: Color.YELLOW,
- logging.ANSWER: Color.MAGENTA,
- logging.INFO: Color.GREEN,
- logging.DEBUG: Color.CYAN,
- }
- .get(record.levelno)
- .value
- )
- return color_text(color, super(Console_color_formatter, self).format(record))
- def ansi_escape_code(color): # pragma: no cover
- '''
- Given a color value, produce the corresponding ANSI escape code.
- '''
- return f'\x1b[{color}m'
- def color_text(color, message):
- '''
- Given a color value and a message, return the message colored with ANSI escape codes.
- '''
- if not color:
- return message
- return f'{ansi_escape_code(color)}{message}{ansi_escape_code(Color.RESET.value)}'
- def add_logging_level(level_name, level_number):
- '''
- Globally add a custom logging level based on the given (all uppercase) level name and number.
- Do this idempotently.
- Inspired by https://stackoverflow.com/questions/2183233/how-to-add-a-custom-loglevel-to-pythons-logging-facility/35804945#35804945
- '''
- method_name = level_name.lower()
- if not hasattr(logging, level_name):
- logging.addLevelName(level_number, level_name)
- setattr(logging, level_name, level_number)
- if not hasattr(logging, method_name):
- def log_for_level(self, message, *args, **kwargs): # pragma: no cover
- if self.isEnabledFor(level_number):
- self._log(level_number, message, args, **kwargs)
- setattr(logging.getLoggerClass(), method_name, log_for_level)
- if not hasattr(logging.getLoggerClass(), method_name):
- def log_to_root(message, *args, **kwargs): # pragma: no cover
- logging.log(level_number, message, *args, **kwargs)
- setattr(logging, method_name, log_to_root)
- ANSWER = logging.WARN - 5
- DISABLED = logging.CRITICAL + 10
- def add_custom_log_levels(): # pragma: no cover
- '''
- Add a custom log level between WARN and INFO for user-requested answers.
- '''
- add_logging_level('ANSWER', ANSWER)
- add_logging_level('DISABLED', DISABLED)
- def set_log_prefix(prefix):
- '''
- Given a prefix string, set it onto the formatter defaults for every logging handler so that it
- shows up in every subsequent logging message. For this to work, this relies on each logging
- formatter to be initialized with "{prefix}" somewhere in its logging format.
- '''
- for handler in logging.getLogger().handlers:
- handler.formatter._style._defaults = {'prefix': f'{prefix}: ' if prefix else ''}
- def configure_logging(
- console_log_level,
- syslog_log_level=None,
- log_file_log_level=None,
- monitoring_log_level=None,
- log_file=None,
- log_file_format=None,
- color_enabled=True,
- ):
- '''
- Configure logging to go to both the console and (syslog or log file). Use the given log levels,
- respectively. If color is enabled, set up log formatting accordingly.
- Raise FileNotFoundError or PermissionError if the log file could not be opened for writing.
- '''
- add_custom_log_levels()
- if syslog_log_level is None:
- syslog_log_level = logging.DISABLED
- if log_file_log_level is None:
- log_file_log_level = console_log_level
- if monitoring_log_level is None:
- monitoring_log_level = console_log_level
- # Log certain log levels to console stderr and others to stdout. This supports use cases like
- # grepping (non-error) output.
- console_disabled = logging.NullHandler()
- console_error_handler = logging.StreamHandler(sys.stderr)
- console_standard_handler = logging.StreamHandler(sys.stdout)
- console_handler = Multi_stream_handler(
- {
- logging.DISABLED: console_disabled,
- logging.CRITICAL: console_error_handler,
- logging.ERROR: console_error_handler,
- logging.WARN: console_error_handler,
- logging.ANSWER: console_standard_handler,
- logging.INFO: console_standard_handler,
- logging.DEBUG: console_standard_handler,
- }
- )
- if color_enabled:
- console_handler.setFormatter(Console_color_formatter())
- else:
- console_handler.setFormatter(Console_no_color_formatter())
- console_handler.setLevel(console_log_level)
- handlers = [console_handler]
- if syslog_log_level != logging.DISABLED:
- syslog_path = None
- if os.path.exists('/dev/log'):
- syslog_path = '/dev/log'
- elif os.path.exists('/var/run/syslog'):
- syslog_path = '/var/run/syslog'
- elif os.path.exists('/var/run/log'):
- syslog_path = '/var/run/log'
- if syslog_path:
- syslog_handler = logging.handlers.SysLogHandler(address=syslog_path)
- syslog_handler.setFormatter(
- logging.Formatter('borgmatic: {levelname} {prefix}{message}', style='{', defaults={'prefix': ''}) # noqa: FS003
- )
- syslog_handler.setLevel(syslog_log_level)
- handlers.append(syslog_handler)
- if log_file and log_file_log_level != logging.DISABLED:
- file_handler = logging.handlers.WatchedFileHandler(log_file)
- file_handler.setFormatter(
- logging.Formatter(
- log_file_format or '[{asctime}] {levelname}: {prefix}{message}', style='{', defaults={'prefix': ''} # noqa: FS003
- )
- )
- file_handler.setLevel(log_file_log_level)
- handlers.append(file_handler)
- logging.basicConfig(
- level=min(handler.level for handler in handlers),
- handlers=handlers,
- )
|