123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228 |
- import logging
- import logging.handlers
- import os
- import sys
- import colorama
- 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 colorama marking up.
- '''
- if no_color:
- return False
- if any(config.get('output', {}).get('color') is False for config in configs.values()):
- 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_color_formatter(logging.Formatter):
- def format(self, record):
- add_custom_log_levels()
- color = {
- logging.CRITICAL: colorama.Fore.RED,
- logging.ERROR: colorama.Fore.RED,
- logging.WARN: colorama.Fore.YELLOW,
- logging.ANSWER: colorama.Fore.MAGENTA,
- logging.INFO: colorama.Fore.GREEN,
- logging.DEBUG: colorama.Fore.CYAN,
- }.get(record.levelno)
- return color_text(color, record.msg)
- def color_text(color, message):
- '''
- Give colored text.
- '''
- if not color:
- return message
- return f'{color}{message}{colorama.Style.RESET_ALL}'
- 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 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,
- ):
- '''
- Configure logging to go to both the console and (syslog or log file). Use the given log levels,
- respectively.
- Raise FileNotFoundError or PermissionError if the log file could not be opened for writing.
- '''
- if syslog_log_level is None:
- syslog_log_level = console_log_level
- 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
- add_custom_log_levels()
- # 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,
- }
- )
- console_handler.setFormatter(Console_color_formatter())
- console_handler.setLevel(console_log_level)
- syslog_path = None
- if log_file is None and syslog_log_level != logging.DISABLED:
- 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 and not interactive_console():
- syslog_handler = logging.handlers.SysLogHandler(address=syslog_path)
- syslog_handler.setFormatter(
- logging.Formatter('borgmatic: {levelname} {message}', style='{') # noqa: FS003
- )
- syslog_handler.setLevel(syslog_log_level)
- handlers = (console_handler, syslog_handler)
- elif 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}: {message}', style='{' # noqa: FS003
- )
- )
- file_handler.setLevel(log_file_log_level)
- handlers = (console_handler, file_handler)
- else:
- handlers = (console_handler,)
- logging.basicConfig(
- level=min(console_log_level, syslog_log_level, log_file_log_level, monitoring_log_level),
- handlers=handlers,
- )
|