Various changes.
[python_utils.git] / logging_utils.py
index 03a23d9ad063a352d4f1728e585fc6a83421958b..25919a765ef2430283cb0e67572d326ca62507f0 100644 (file)
@@ -4,28 +4,31 @@
 
 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
 
-parser = config.add_commandline_args(
+cfg = config.add_commandline_args(
     f'Logging ({__file__})',
     'Args related to logging')
-parser.add_argument(
+cfg.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(
+cfg.add_argument(
     '--logging_level',
     type=str,
     default='INFO',
@@ -33,65 +36,75 @@ parser.add_argument(
     metavar='LEVEL',
     help='The level below which to squelch log messages.',
 )
-parser.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.'
 )
-parser.add_argument(
+cfg.add_argument(
     '--logging_date_format',
     type=str,
     default='%Y/%m/%dT%H:%M:%S.%f%z',
     metavar='DATEFMT',
     help='The format of any dates in --logging_format.'
 )
-parser.add_argument(
+cfg.add_argument(
     '--logging_console',
     action=argparse_utils.ActionNoYes,
     default=True,
     help='Should we log to the console (stderr)',
 )
-parser.add_argument(
+cfg.add_argument(
     '--logging_filename',
     type=str,
     default=None,
     metavar='FILENAME',
     help='The filename of the logfile to write.'
 )
-parser.add_argument(
+cfg.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(
+cfg.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(
+cfg.add_argument(
     '--logging_syslog',
     action=argparse_utils.ActionNoYes,
     default=False,
     help='Should we log to localhost\'s syslog.'
 )
-parser.add_argument(
+cfg.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(
+cfg.add_argument(
     '--logging_info_is_print',
     action=argparse_utils.ActionNoYes,
     default=False,
     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