X-Git-Url: https://wannabe.guru.org/gitweb/?a=blobdiff_plain;f=logging_utils.py;h=25919a765ef2430283cb0e67572d326ca62507f0;hb=b10d30a46e601c9ee1f843241f2d69a1f90f7a94;hp=a24f1c9fb520f37ad7c13ec423e65c08b86074ae;hpb=3bc4daf1edc121cd633429187392227f2fa61885;p=python_utils.git diff --git a/logging_utils.py b/logging_utils.py index a24f1c9..25919a7 100644 --- a/logging_utils.py +++ b/logging_utils.py @@ -4,16 +4,19 @@ import contextlib import datetime +import enum +import io import logging from logging.handlers import RotatingFileHandler, SysLogHandler import os import pytz import sys +from typing import Iterable, Optional +# This module is commonly used by others in here and should avoid +# taking any unnecessary dependencies back on them. import argparse_utils import config -import string_utils as su -import thread_utils as tu cfg = config.add_commandline_args( f'Logging ({__file__})', @@ -36,7 +39,7 @@ cfg.add_argument( cfg.add_argument( '--logging_format', type=str, - default='%(levelname)s:%(asctime)s: %(message)s', + default='%(levelname).1s:%(asctime)s: %(message)s', help='The format for lines logged via the logger module.' ) cfg.add_argument( @@ -92,6 +95,16 @@ cfg.add_argument( help='logging.info also prints to stdout.' ) +# See also: OutputMultiplexer +cfg.add_argument( + '--logging_captures_prints', + action=argparse_utils.ActionNoYes, + default=False, + help='When calling print also log.info too' +) + +built_in_print = print + class OnlyInfoFilter(logging.Filter): def filter(self, record): @@ -102,7 +115,9 @@ class MillisecondAwareFormatter(logging.Formatter): converter = datetime.datetime.fromtimestamp def formatTime(self, record, datefmt=None): - ct = self.converter(record.created, pytz.timezone("US/Pacific")) + ct = MillisecondAwareFormatter.converter( + record.created, pytz.timezone("US/Pacific") + ) if datefmt: s = ct.strftime(datefmt) else: @@ -134,12 +149,7 @@ def initialize_logging(logger=None) -> logging.Logger: 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: + if sys.platform not in ('win32', 'cygwin'): handler = SysLogHandler() # for k, v in encoded_priorities.items(): # handler.encodePriority(k, v) @@ -152,7 +162,7 @@ def initialize_logging(logger=None) -> logging.Logger: handler.setLevel(numeric_level) handlers.append(handler) - if config.config['logging_filename'] is not None: + if config.config['logging_filename']: handler = RotatingFileHandler( config.config['logging_filename'], maxBytes = config.config['logging_filename_maxsize'], @@ -183,12 +193,28 @@ def initialize_logging(logger=None) -> logging.Logger: 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 + + if config.config['logging_captures_prints']: + import builtins + global built_in_print + + def print_and_also_log(*arg, **kwarg): + f = kwarg.get('file', None) + if f == sys.stderr: + logger.warning(*arg) + else: + logger.info(*arg) + built_in_print(*arg, **kwarg) + builtins.print = print_and_also_log + return logger @@ -199,7 +225,8 @@ def get_logger(name: str = ""): def tprint(*args, **kwargs) -> None: if config.config['logging_debug_threads']: - print(f'{tu.current_thread_id()}', end="") + from thread_utils import current_thread_id + print(f'{current_thread_id()}', end="") print(*args, **kwargs) else: pass @@ -209,55 +236,72 @@ 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 +class OutputMultiplexer(object): + + class Destination(enum.IntEnum): + """Bits in the destination_bitv bitvector. Used to indicate the + output destination.""" + LOG_DEBUG = 0x01 # -\ + LOG_INFO = 0x02 # | + LOG_WARNING = 0x04 # > Should provide logger to the c'tor. + LOG_ERROR = 0x08 # | + LOG_CRITICAL = 0x10 # _/ + FILENAMES = 0x20 # Must provide a filename to the c'tor. + FILEHANDLES = 0x40 # Must provide a handle to the c'tor. + HLOG = 0x80 + ALL_LOG_DESTINATIONS = ( + LOG_DEBUG | LOG_INFO | LOG_WARNING | LOG_ERROR | LOG_CRITICAL + ) + ALL_OUTPUT_DESTINATIONS = 0x8F def __init__(self, destination_bitv: int, *, logger=None, - filename=None): + filenames: Optional[Iterable[str]] = None, + handles: Optional[Iterable[io.TextIOWrapper]] = None): if logger is None: logger = logging.getLogger(None) self.logger = logger - if filename is not None: - self.f = open(filename, "wb", buffering=0) + if filenames is not None: + self.f = [ + open(filename, 'wb', buffering=0) for filename in filenames + ] else: - if self.destination_bitv & OutputSink.FILENAME: + if destination_bitv & OutputMultiplexer.FILENAMES: raise ValueError( - "Filename argument is required if bitv & FILENAME" + "Filenames argument is required if bitv & FILENAMES" ) self.f = None + + if handles is not None: + self.h = [handle for handle in handles] + else: + if destination_bitv & OutputMultiplexer.Destination.FILEHANDLES: + raise ValueError( + "Handle argument is required if bitv & FILEHANDLES" + ) + self.h = 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: + if destination_bitv & self.Destination.FILENAMES and self.f is None: raise ValueError( - "Filename argument is required if bitv & FILENAME" + "Filename argument is required if bitv & FILENAMES" ) + if destination_bitv & self.Destination.FILEHANDLES and self.h is None: + raise ValueError( + "Handle argument is required if bitv & FILEHANDLES" + ) self.destination_bitv = destination_bitv def print(self, *args, **kwargs): + from string_utils import sprintf, strip_escape_sequences end = kwargs.pop("end", None) if end is not None: if not isinstance(end, str): @@ -268,52 +312,67 @@ class OutputSink(object): 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) + buf = 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.destination_bitv & self.Destination.FILENAMES and + self.f is not None + ): + for _ in self.f: + _.write(buf.encode('utf-8')) + _.flush() + + if ( + self.destination_bitv & self.Destination.FILEHANDLES and + self.h is not None + ): + for _ in self.h: + _.write(buf) + _.flush() + + buf = strip_escape_sequences(buf) if self.logger is not None: - if self.destination_bitv & self.LOG_DEBUG: + if self.destination_bitv & self.Destination.LOG_DEBUG: self.logger.debug(buf) - if self.destination_bitv & self.LOG_INFO: + if self.destination_bitv & self.Destination.LOG_INFO: self.logger.info(buf) - if self.destination_bitv & self.LOG_WARNING: + if self.destination_bitv & self.Destination.LOG_WARNING: self.logger.warning(buf) - if self.destination_bitv & self.LOG_ERROR: + if self.destination_bitv & self.Destination.LOG_ERROR: self.logger.error(buf) - if self.destination_bitv & self.LOG_CRITICAL: + if self.destination_bitv & self.Destination.LOG_CRITICAL: self.logger.critical(buf) - if self.destination_bitv & self.HLOG: + if self.destination_bitv & self.Destination.HLOG: hlog(buf) def close(self): if self.f is not None: - self.f.close() + for _ in self.f: + _.close() -class OutputContext(OutputSink, contextlib.ContextDecorator): +class OutputMultiplexerContext(OutputMultiplexer, contextlib.ContextDecorator): def __init__(self, - destination_bitv: int, + destination_bitv: OutputMultiplexer.Destination, *, - logger=None, - filename=None): - super().__init__(destination_bitv, logger=logger, filename=filename) + logger = None, + filenames = None, + handles = None): + super().__init__( + destination_bitv, + logger=logger, + filenames=filenames, + handles=handles) def __enter__(self): return self - def __exit__(self, etype, value, traceback): + def __exit__(self, etype, value, traceback) -> bool: super().close() if etype is not None: return False