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__})',
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(
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):
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:
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)
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'],
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
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
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):
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