3 """Utilities related to logging."""
8 from logging.handlers import RotatingFileHandler, SysLogHandler
13 # This module is commonly used by others in here and should avoid
14 # taking any unnecessary dependencies back on them.
18 cfg = config.add_commandline_args(
19 f'Logging ({__file__})',
20 'Args related to logging')
22 '--logging_config_file',
23 type=argparse_utils.valid_filename,
26 help='Config file containing the logging setup, see: https://docs.python.org/3/howto/logging.html#logging-advanced-tutorial',
32 choices=['NOTSET', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
34 help='The level below which to squelch log messages.',
39 default='%(levelname).1s:%(asctime)s: %(message)s',
40 help='The format for lines logged via the logger module.'
43 '--logging_date_format',
45 default='%Y/%m/%dT%H:%M:%S.%f%z',
47 help='The format of any dates in --logging_format.'
51 action=argparse_utils.ActionNoYes,
53 help='Should we log to the console (stderr)',
60 help='The filename of the logfile to write.'
63 '--logging_filename_maxsize',
67 help='The maximum size (in bytes) to write to the logging_filename.'
70 '--logging_filename_count',
74 help='The number of logging_filename copies to keep before deleting.'
78 action=argparse_utils.ActionNoYes,
80 help='Should we log to localhost\'s syslog.'
83 '--logging_debug_threads',
84 action=argparse_utils.ActionNoYes,
86 help='Should we prepend pid/tid data to all log messages?'
89 '--logging_info_is_print',
90 action=argparse_utils.ActionNoYes,
92 help='logging.info also prints to stdout.'
96 class OnlyInfoFilter(logging.Filter):
97 def filter(self, record):
98 return record.levelno == logging.INFO
101 class MillisecondAwareFormatter(logging.Formatter):
102 converter = datetime.datetime.fromtimestamp
104 def formatTime(self, record, datefmt=None):
105 ct = MillisecondAwareFormatter.converter(
106 record.created, pytz.timezone("US/Pacific")
109 s = ct.strftime(datefmt)
111 t = ct.strftime("%Y-%m-%d %H:%M:%S")
112 s = "%s,%03d" % (t, record.msecs)
116 def initialize_logging(logger=None) -> logging.Logger:
117 assert config.has_been_parsed()
119 logger = logging.getLogger() # Root logger
121 if config.config['logging_config_file'] is not None:
122 logging.config.fileConfig('logging.conf')
126 numeric_level = getattr(
128 config.config['logging_level'].upper(),
131 if not isinstance(numeric_level, int):
132 raise ValueError('Invalid level: %s' % config.config['logging_level'])
134 fmt = config.config['logging_format']
135 if config.config['logging_debug_threads']:
136 fmt = f'%(process)d.%(thread)d|{fmt}'
138 if config.config['logging_syslog']:
139 if sys.platform in ('win32', 'cygwin'):
141 "WARNING: Current platform does not support syslog; IGNORING.",
145 handler = SysLogHandler()
146 # for k, v in encoded_priorities.items():
147 # handler.encodePriority(k, v)
148 handler.setFormatter(
149 MillisecondAwareFormatter(
151 datefmt=config.config['logging_date_format'],
154 handler.setLevel(numeric_level)
155 handlers.append(handler)
157 if config.config['logging_filename'] is not None:
158 handler = RotatingFileHandler(
159 config.config['logging_filename'],
160 maxBytes = config.config['logging_filename_maxsize'],
161 backupCount = config.config['logging_filename_count'],
163 handler.setLevel(numeric_level)
164 handler.setFormatter(
165 MillisecondAwareFormatter(
167 datefmt=config.config['logging_date_format'],
170 handlers.append(handler)
172 if config.config['logging_console']:
173 handler = logging.StreamHandler(sys.stderr)
174 handler.setLevel(numeric_level)
175 handler.setFormatter(
176 MillisecondAwareFormatter(
178 datefmt=config.config['logging_date_format'],
181 handlers.append(handler)
183 if len(handlers) == 0:
184 handlers.append(logging.NullHandler())
186 for handler in handlers:
187 logger.addHandler(handler)
188 if config.config['logging_info_is_print']:
189 handler = logging.StreamHandler(sys.stdout)
190 handler.addFilter(OnlyInfoFilter())
191 logger.addHandler(handler)
192 logger.setLevel(numeric_level)
193 logger.propagate = False
197 def get_logger(name: str = ""):
198 logger = logging.getLogger(name)
199 return initialize_logging(logger)
202 def tprint(*args, **kwargs) -> None:
203 if config.config['logging_debug_threads']:
204 from thread_utils import current_thread_id
205 print(f'{current_thread_id()}', end="")
206 print(*args, **kwargs)
211 def dprint(*args, **kwargs) -> None:
212 print(*args, file=sys.stderr, **kwargs)
215 class OutputSink(object):
217 # Bits in the destination_bitv bitvector. Used to indicate the
218 # output destination.
223 LOG_WARNING = 0x10 # > Should provide logger to the c'tor.
225 LOG_CRITICAL = 0x40 # _/
226 FILENAME = 0x80 # Must provide a filename to the c'tor.
229 ALL_LOG_DESTINATIONS = (
230 LOG_DEBUG | LOG_INFO | LOG_WARNING | LOG_ERROR | LOG_CRITICAL
232 ALL_OUTPUT_DESTINATIONS = 0x1FF
235 destination_bitv: int,
240 logger = logging.getLogger(None)
243 if filename is not None:
244 self.f = open(filename, "wb", buffering=0)
246 if self.destination_bitv & OutputSink.FILENAME:
248 "Filename argument is required if bitv & FILENAME"
251 self.set_destination_bitv(destination_bitv)
253 def get_destination_bitv(self):
254 return self.destination_bitv
256 def set_destination_bitv(self, destination_bitv: int):
257 if destination_bitv & self.FILENAME and self.f is None:
259 "Filename argument is required if bitv & FILENAME"
261 self.destination_bitv = destination_bitv
263 def print(self, *args, **kwargs):
264 from string_utils import sprintf, strip_escape_sequences
265 end = kwargs.pop("end", None)
267 if not isinstance(end, str):
268 raise TypeError("end must be None or a string")
269 sep = kwargs.pop("sep", None)
271 if not isinstance(sep, str):
272 raise TypeError("sep must be None or a string")
274 raise TypeError("invalid keyword arguments to print()")
275 buf = sprintf(*args, end="", sep=sep)
280 if self.destination_bitv & self.STDOUT:
281 print(buf, file=sys.stdout, sep=sep, end=end)
282 if self.destination_bitv & self.STDERR:
283 print(buf, file=sys.stderr, sep=sep, end=end)
286 if self.destination_bitv & self.FILENAME and self.f is not None:
287 self.f.write(buf.encode('utf-8'))
289 buf = strip_escape_sequences(buf)
290 if self.logger is not None:
291 if self.destination_bitv & self.LOG_DEBUG:
292 self.logger.debug(buf)
293 if self.destination_bitv & self.LOG_INFO:
294 self.logger.info(buf)
295 if self.destination_bitv & self.LOG_WARNING:
296 self.logger.warning(buf)
297 if self.destination_bitv & self.LOG_ERROR:
298 self.logger.error(buf)
299 if self.destination_bitv & self.LOG_CRITICAL:
300 self.logger.critical(buf)
301 if self.destination_bitv & self.HLOG:
305 if self.f is not None:
309 class OutputContext(OutputSink, contextlib.ContextDecorator):
311 destination_bitv: int,
315 super().__init__(destination_bitv, logger=logger, filename=filename)
320 def __exit__(self, etype, value, traceback):
322 if etype is not None:
327 def hlog(message: str) -> None:
328 message = message.replace("'", "'\"'\"'")
329 os.system(f"/usr/bin/logger -p local7.info -- '{message}'")