3 """Utilities related to logging."""
10 from logging.handlers import RotatingFileHandler, SysLogHandler
14 from typing import Iterable, Optional
16 # This module is commonly used by others in here and should avoid
17 # taking any unnecessary dependencies back on them.
21 cfg = config.add_commandline_args(
22 f'Logging ({__file__})',
23 'Args related to logging')
25 '--logging_config_file',
26 type=argparse_utils.valid_filename,
29 help='Config file containing the logging setup, see: https://docs.python.org/3/howto/logging.html#logging-advanced-tutorial',
35 choices=['NOTSET', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
37 help='The level below which to squelch log messages.',
42 default='%(levelname).1s:%(asctime)s: %(message)s',
43 help='The format for lines logged via the logger module.'
46 '--logging_date_format',
48 default='%Y/%m/%dT%H:%M:%S.%f%z',
50 help='The format of any dates in --logging_format.'
54 action=argparse_utils.ActionNoYes,
56 help='Should we log to the console (stderr)',
63 help='The filename of the logfile to write.'
66 '--logging_filename_maxsize',
70 help='The maximum size (in bytes) to write to the logging_filename.'
73 '--logging_filename_count',
77 help='The number of logging_filename copies to keep before deleting.'
81 action=argparse_utils.ActionNoYes,
83 help='Should we log to localhost\'s syslog.'
86 '--logging_debug_threads',
87 action=argparse_utils.ActionNoYes,
89 help='Should we prepend pid/tid data to all log messages?'
92 '--logging_info_is_print',
93 action=argparse_utils.ActionNoYes,
95 help='logging.info also prints to stdout.'
98 # See also: OutputMultiplexer
100 '--logging_captures_prints',
101 action=argparse_utils.ActionNoYes,
103 help='When calling print also log.info too'
106 built_in_print = print
109 class OnlyInfoFilter(logging.Filter):
110 def filter(self, record):
111 return record.levelno == logging.INFO
114 class MillisecondAwareFormatter(logging.Formatter):
115 converter = datetime.datetime.fromtimestamp
117 def formatTime(self, record, datefmt=None):
118 ct = MillisecondAwareFormatter.converter(
119 record.created, pytz.timezone("US/Pacific")
122 s = ct.strftime(datefmt)
124 t = ct.strftime("%Y-%m-%d %H:%M:%S")
125 s = "%s,%03d" % (t, record.msecs)
129 def initialize_logging(logger=None) -> logging.Logger:
130 assert config.has_been_parsed()
132 logger = logging.getLogger() # Root logger
134 if config.config['logging_config_file'] is not None:
135 logging.config.fileConfig('logging.conf')
139 numeric_level = getattr(
141 config.config['logging_level'].upper(),
144 if not isinstance(numeric_level, int):
145 raise ValueError('Invalid level: %s' % config.config['logging_level'])
147 fmt = config.config['logging_format']
148 if config.config['logging_debug_threads']:
149 fmt = f'%(process)d.%(thread)d|{fmt}'
151 if config.config['logging_syslog']:
152 if sys.platform not in ('win32', 'cygwin'):
153 handler = SysLogHandler()
154 # for k, v in encoded_priorities.items():
155 # handler.encodePriority(k, v)
156 handler.setFormatter(
157 MillisecondAwareFormatter(
159 datefmt=config.config['logging_date_format'],
162 handler.setLevel(numeric_level)
163 handlers.append(handler)
165 if config.config['logging_filename']:
166 handler = RotatingFileHandler(
167 config.config['logging_filename'],
168 maxBytes = config.config['logging_filename_maxsize'],
169 backupCount = config.config['logging_filename_count'],
171 handler.setLevel(numeric_level)
172 handler.setFormatter(
173 MillisecondAwareFormatter(
175 datefmt=config.config['logging_date_format'],
178 handlers.append(handler)
180 if config.config['logging_console']:
181 handler = logging.StreamHandler(sys.stderr)
182 handler.setLevel(numeric_level)
183 handler.setFormatter(
184 MillisecondAwareFormatter(
186 datefmt=config.config['logging_date_format'],
189 handlers.append(handler)
191 if len(handlers) == 0:
192 handlers.append(logging.NullHandler())
194 for handler in handlers:
195 logger.addHandler(handler)
197 if config.config['logging_info_is_print']:
198 handler = logging.StreamHandler(sys.stdout)
199 handler.addFilter(OnlyInfoFilter())
200 logger.addHandler(handler)
202 logger.setLevel(numeric_level)
203 logger.propagate = False
205 if config.config['logging_captures_prints']:
207 global built_in_print
209 def print_and_also_log(*arg, **kwarg):
210 f = kwarg.get('file', None)
215 built_in_print(*arg, **kwarg)
216 builtins.print = print_and_also_log
221 def get_logger(name: str = ""):
222 logger = logging.getLogger(name)
223 return initialize_logging(logger)
226 def tprint(*args, **kwargs) -> None:
227 if config.config['logging_debug_threads']:
228 from thread_utils import current_thread_id
229 print(f'{current_thread_id()}', end="")
230 print(*args, **kwargs)
235 def dprint(*args, **kwargs) -> None:
236 print(*args, file=sys.stderr, **kwargs)
239 class OutputMultiplexer(object):
241 class Destination(enum.IntEnum):
242 """Bits in the destination_bitv bitvector. Used to indicate the
243 output destination."""
244 LOG_DEBUG = 0x01 # -\
246 LOG_WARNING = 0x04 # > Should provide logger to the c'tor.
248 LOG_CRITICAL = 0x10 # _/
249 FILENAMES = 0x20 # Must provide a filename to the c'tor.
250 FILEHANDLES = 0x40 # Must provide a handle to the c'tor.
252 ALL_LOG_DESTINATIONS = (
253 LOG_DEBUG | LOG_INFO | LOG_WARNING | LOG_ERROR | LOG_CRITICAL
255 ALL_OUTPUT_DESTINATIONS = 0x8F
258 destination_bitv: int,
261 filenames: Optional[Iterable[str]] = None,
262 handles: Optional[Iterable[io.TextIOWrapper]] = None):
264 logger = logging.getLogger(None)
267 if filenames is not None:
269 open(filename, 'wb', buffering=0) for filename in filenames
272 if destination_bitv & OutputMultiplexer.FILENAMES:
274 "Filenames argument is required if bitv & FILENAMES"
278 if handles is not None:
279 self.h = [handle for handle in handles]
281 if destination_bitv & OutputMultiplexer.Destination.FILEHANDLES:
283 "Handle argument is required if bitv & FILEHANDLES"
287 self.set_destination_bitv(destination_bitv)
289 def get_destination_bitv(self):
290 return self.destination_bitv
292 def set_destination_bitv(self, destination_bitv: int):
293 if destination_bitv & self.Destination.FILENAMES and self.f is None:
295 "Filename argument is required if bitv & FILENAMES"
297 if destination_bitv & self.Destination.FILEHANDLES and self.h is None:
299 "Handle argument is required if bitv & FILEHANDLES"
301 self.destination_bitv = destination_bitv
303 def print(self, *args, **kwargs):
304 from string_utils import sprintf, strip_escape_sequences
305 end = kwargs.pop("end", None)
307 if not isinstance(end, str):
308 raise TypeError("end must be None or a string")
309 sep = kwargs.pop("sep", None)
311 if not isinstance(sep, str):
312 raise TypeError("sep must be None or a string")
314 raise TypeError("invalid keyword arguments to print()")
315 buf = sprintf(*args, end="", sep=sep)
323 self.destination_bitv & self.Destination.FILENAMES and
327 _.write(buf.encode('utf-8'))
331 self.destination_bitv & self.Destination.FILEHANDLES and
338 buf = strip_escape_sequences(buf)
339 if self.logger is not None:
340 if self.destination_bitv & self.Destination.LOG_DEBUG:
341 self.logger.debug(buf)
342 if self.destination_bitv & self.Destination.LOG_INFO:
343 self.logger.info(buf)
344 if self.destination_bitv & self.Destination.LOG_WARNING:
345 self.logger.warning(buf)
346 if self.destination_bitv & self.Destination.LOG_ERROR:
347 self.logger.error(buf)
348 if self.destination_bitv & self.Destination.LOG_CRITICAL:
349 self.logger.critical(buf)
350 if self.destination_bitv & self.Destination.HLOG:
354 if self.f is not None:
359 class OutputMultiplexerContext(OutputMultiplexer, contextlib.ContextDecorator):
361 destination_bitv: OutputMultiplexer.Destination,
375 def __exit__(self, etype, value, traceback) -> bool:
377 if etype is not None:
382 def hlog(message: str) -> None:
383 message = message.replace("'", "'\"'\"'")
384 os.system(f"/usr/bin/logger -p local7.info -- '{message}'")