diff options
| author | Scott Gasch <[email protected]> | 2021-03-24 18:08:54 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2021-03-24 18:08:54 -0700 |
| commit | 497fb9e21f45ec08e1486abaee6dfa7b20b8a691 (patch) | |
| tree | 47aa97a0fca36c4e7025cee5ad4e9ec6db49b881 /logging_utils.py | |
Initial revision
Diffstat (limited to 'logging_utils.py')
| -rw-r--r-- | logging_utils.py | 310 |
1 files changed, 310 insertions, 0 deletions
diff --git a/logging_utils.py b/logging_utils.py new file mode 100644 index 0000000..cf784ca --- /dev/null +++ b/logging_utils.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 + +"""Utilities related to logging.""" + +import contextlib +import logging +from logging.handlers import RotatingFileHandler, SysLogHandler +import os +import sys + +import argparse_utils +import config +import string_utils as su +import thread_utils as tu + +parser = config.add_commandline_args( + f'Logging ({__file__})', + 'Args related to logging') +parser.add_argument( + '--logging_config_file', + type=argparse_utils.valid_filename, + default=None, + metavar='FILENAME', + help='Config file containing the logging setup, see: https://docs.python.org/3/howto/logging.html#logging-advanced-tutorial', +) +parser.add_argument( + '--logging_level', + type=str, + default='INFO', + choices=['NOTSET', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], + metavar='LEVEL', + help='The level below which to squelch log messages.', +) +parser.add_argument( + '--logging_format', + type=str, + default='%(levelname)s:%(asctime)s: %(message)s', + help='The format for lines logged via the logger module.' +) +parser.add_argument( + '--logging_date_format', + type=str, + default='%Y/%m/%dT%H:%M:%S%z', + metavar='DATEFMT', + help='The format of any dates in --logging_format.' +) +parser.add_argument( + '--logging_console', + action=argparse_utils.ActionNoYes, + default=True, + help='Should we log to the console (stderr)', +) +parser.add_argument( + '--logging_filename', + type=str, + default=None, + metavar='FILENAME', + help='The filename of the logfile to write.' +) +parser.add_argument( + '--logging_filename_maxsize', + type=int, + default=(1024*1024), + metavar='#BYTES', + help='The maximum size (in bytes) to write to the logging_filename.' +) +parser.add_argument( + '--logging_filename_count', + type=int, + default=2, + metavar='COUNT', + help='The number of logging_filename copies to keep before deleting.' +) +parser.add_argument( + '--logging_syslog', + action=argparse_utils.ActionNoYes, + default=False, + help='Should we log to localhost\'s syslog.' +) +parser.add_argument( + '--logging_debug_threads', + action=argparse_utils.ActionNoYes, + default=False, + help='Should we prepend pid/tid data to all log messages?' +) +parser.add_argument( + '--logging_info_is_print', + action=argparse_utils.ActionNoYes, + default=False, + help='logging.info also prints to stdout.' +) + + +class OnlyInfoFilter(logging.Filter): + def filter(self, record): + return record.levelno == logging.INFO + + +def initialize_logging(logger=None) -> logging.Logger: + assert config.has_been_parsed() + if logger is None: + logger = logging.getLogger() # Root logger + + if config.config['logging_config_file'] is not None: + logging.config.fileConfig('logging.conf') + return logger + + handlers = [] + numeric_level = getattr( + logging, + config.config['logging_level'].upper(), + None + ) + if not isinstance(numeric_level, int): + raise ValueError('Invalid level: %s' % config.config['logging_level']) + + fmt = config.config['logging_format'] + if config.config['logging_debug_threads']: + fmt = f'%(process)d.%(thread)d|{fmt}' + + if config.config['logging_syslog']: + if sys.platform in ('win32', 'cygwin'): + print( + "WARNING: Current platform does not support syslog; IGNORING.", + file=sys.stderr + ) + else: + handler = SysLogHandler() +# for k, v in encoded_priorities.items(): +# handler.encodePriority(k, v) + handler.setFormatter( + logging.Formatter( + fmt=fmt, + datefmt=config.config['logging_date_format'], + ) + ) + handler.setLevel(numeric_level) + handlers.append(handler) + + if config.config['logging_filename'] is not None: + handler = RotatingFileHandler( + config.config['logging_filename'], + maxBytes = config.config['logging_filename_maxsize'], + backupCount = config.config['logging_filename_count'], + ) + handler.setLevel(numeric_level) + handler.setFormatter( + logging.Formatter( + fmt=fmt, + datefmt=config.config['logging_date_format'], + ) + ) + handlers.append(handler) + + if config.config['logging_console']: + handler = logging.StreamHandler(sys.stderr) + handler.setLevel(numeric_level) + handler.setFormatter( + logging.Formatter( + fmt=fmt, + datefmt=config.config['logging_date_format'], + ) + ) + handlers.append(handler) + + if len(handlers) == 0: + handlers.append(logging.NullHandler()) + + for handler in handlers: + logger.addHandler(handler) + if config.config['logging_info_is_print']: + handler = logging.StreamHandler(sys.stdout) + handler.addFilter(OnlyInfoFilter()) + logger.addHandler(handler) + logger.setLevel(numeric_level) + logger.propagate = False + return logger + + +def get_logger(name: str = ""): + logger = logging.getLogger(name) + return initialize_logging(logger) + + +def tprint(*args, **kwargs) -> None: + if config.config['logging_debug_threads']: + print(f'{tu.current_thread_id()}', end="") + print(*args, **kwargs) + else: + pass + + +def dprint(*args, **kwargs) -> None: + print(*args, file=sys.stderr, **kwargs) + + +class OutputSink(object): + + # Bits in the destination_bitv bitvector. Used to indicate the + # output destination. + STDOUT = 0x1 + STDERR = 0x2 + LOG_DEBUG = 0x4 # -\ + LOG_INFO = 0x8 # | + LOG_WARNING = 0x10 # > Should provide logger to the c'tor. + LOG_ERROR = 0x20 # | + LOG_CRITICAL = 0x40 # _/ + FILENAME = 0x80 # Must provide a filename to the c'tor. + HLOG = 0x100 + + ALL_LOG_DESTINATIONS = ( + LOG_DEBUG | LOG_INFO | LOG_WARNING | LOG_ERROR | LOG_CRITICAL + ) + ALL_OUTPUT_DESTINATIONS = 0x1FF + + def __init__(self, + destination_bitv: int, + *, + logger=None, + filename=None): + if logger is None: + logger = logging.getLogger(None) + self.logger = logger + + if filename is not None: + self.f = open(filename, "wb", buffering=0) + else: + if self.destination_bitv & OutputSink.FILENAME: + raise ValueError( + "Filename argument is required if bitv & FILENAME" + ) + self.f = None + self.set_destination_bitv(destination_bitv) + + def get_destination_bitv(self): + return self.destination_bitv + + def set_destination_bitv(self, destination_bitv: int): + if destination_bitv & self.FILENAME and self.f is None: + raise ValueError( + "Filename argument is required if bitv & FILENAME" + ) + self.destination_bitv = destination_bitv + + def print(self, *args, **kwargs): + end = kwargs.pop("end", None) + if end is not None: + if not isinstance(end, str): + raise TypeError("end must be None or a string") + sep = kwargs.pop("sep", None) + if sep is not None: + if not isinstance(sep, str): + raise TypeError("sep must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + buf = su.sprintf(*args, end="", sep=sep) + if sep is None: + sep = " " + if end is None: + end = "\n" + if self.destination_bitv & self.STDOUT: + print(buf, file=sys.stdout, sep=sep, end=end) + if self.destination_bitv & self.STDERR: + print(buf, file=sys.stderr, sep=sep, end=end) + if end == '\n': + buf += '\n' + if self.destination_bitv & self.FILENAME and self.f is not None: + self.f.write(buf.encode('utf-8')) + self.f.flush() + buf = su.strip_escape_sequences(buf) + if self.logger is not None: + if self.destination_bitv & self.LOG_DEBUG: + self.logger.debug(buf) + if self.destination_bitv & self.LOG_INFO: + self.logger.info(buf) + if self.destination_bitv & self.LOG_WARNING: + self.logger.warning(buf) + if self.destination_bitv & self.LOG_ERROR: + self.logger.error(buf) + if self.destination_bitv & self.LOG_CRITICAL: + self.logger.critical(buf) + if self.destination_bitv & self.HLOG: + hlog(buf) + + def close(self): + if self.f is not None: + self.f.close() + + +class OutputContext(OutputSink, contextlib.ContextDecorator): + def __init__(self, + destination_bitv: int, + *, + logger=None, + filename=None): + super().__init__(destination_bitv, logger=logger, filename=filename) + + def __enter__(self): + return self + + def __exit__(self, etype, value, traceback): + super().close() + if etype is not None: + return False + return True + + +def hlog(message: str) -> None: + message = message.replace("'", "'\"'\"'") + os.system(f"/usr/bin/logger -p local7.info -- '{message}'") |
