Use central exceptions.
[python_utils.git] / logging_utils.py
1 #!/usr/bin/env python3
2
3 """Utilities related to logging."""
4
5 import contextlib
6 import logging
7 from logging.handlers import RotatingFileHandler, SysLogHandler
8 import os
9 import sys
10
11 import argparse_utils
12 import config
13 import string_utils as su
14 import thread_utils as tu
15
16 parser = config.add_commandline_args(
17     f'Logging ({__file__})',
18     'Args related to logging')
19 parser.add_argument(
20     '--logging_config_file',
21     type=argparse_utils.valid_filename,
22     default=None,
23     metavar='FILENAME',
24     help='Config file containing the logging setup, see: https://docs.python.org/3/howto/logging.html#logging-advanced-tutorial',
25 )
26 parser.add_argument(
27     '--logging_level',
28     type=str,
29     default='INFO',
30     choices=['NOTSET', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
31     metavar='LEVEL',
32     help='The level below which to squelch log messages.',
33 )
34 parser.add_argument(
35     '--logging_format',
36     type=str,
37     default='%(levelname)s:%(asctime)s: %(message)s',
38     help='The format for lines logged via the logger module.'
39 )
40 parser.add_argument(
41     '--logging_date_format',
42     type=str,
43     default='%Y/%m/%dT%H:%M:%S%z',
44     metavar='DATEFMT',
45     help='The format of any dates in --logging_format.'
46 )
47 parser.add_argument(
48     '--logging_console',
49     action=argparse_utils.ActionNoYes,
50     default=True,
51     help='Should we log to the console (stderr)',
52 )
53 parser.add_argument(
54     '--logging_filename',
55     type=str,
56     default=None,
57     metavar='FILENAME',
58     help='The filename of the logfile to write.'
59 )
60 parser.add_argument(
61     '--logging_filename_maxsize',
62     type=int,
63     default=(1024*1024),
64     metavar='#BYTES',
65     help='The maximum size (in bytes) to write to the logging_filename.'
66 )
67 parser.add_argument(
68     '--logging_filename_count',
69     type=int,
70     default=2,
71     metavar='COUNT',
72     help='The number of logging_filename copies to keep before deleting.'
73 )
74 parser.add_argument(
75     '--logging_syslog',
76     action=argparse_utils.ActionNoYes,
77     default=False,
78     help='Should we log to localhost\'s syslog.'
79 )
80 parser.add_argument(
81     '--logging_debug_threads',
82     action=argparse_utils.ActionNoYes,
83     default=False,
84     help='Should we prepend pid/tid data to all log messages?'
85 )
86 parser.add_argument(
87     '--logging_info_is_print',
88     action=argparse_utils.ActionNoYes,
89     default=False,
90     help='logging.info also prints to stdout.'
91 )
92
93
94 class OnlyInfoFilter(logging.Filter):
95     def filter(self, record):
96         return record.levelno == logging.INFO
97
98
99 def initialize_logging(logger=None) -> logging.Logger:
100     assert config.has_been_parsed()
101     if logger is None:
102         logger = logging.getLogger()       # Root logger
103
104     if config.config['logging_config_file'] is not None:
105         logging.config.fileConfig('logging.conf')
106         return logger
107
108     handlers = []
109     numeric_level = getattr(
110         logging,
111         config.config['logging_level'].upper(),
112         None
113     )
114     if not isinstance(numeric_level, int):
115         raise ValueError('Invalid level: %s' % config.config['logging_level'])
116
117     fmt = config.config['logging_format']
118     if config.config['logging_debug_threads']:
119         fmt = f'%(process)d.%(thread)d|{fmt}'
120
121     if config.config['logging_syslog']:
122         if sys.platform in ('win32', 'cygwin'):
123             print(
124                 "WARNING: Current platform does not support syslog; IGNORING.",
125                 file=sys.stderr
126             )
127         else:
128             handler = SysLogHandler()
129 #            for k, v in encoded_priorities.items():
130 #                handler.encodePriority(k, v)
131             handler.setFormatter(
132                 logging.Formatter(
133                     fmt=fmt,
134                     datefmt=config.config['logging_date_format'],
135                 )
136             )
137             handler.setLevel(numeric_level)
138             handlers.append(handler)
139
140     if config.config['logging_filename'] is not None:
141         handler = RotatingFileHandler(
142             config.config['logging_filename'],
143             maxBytes = config.config['logging_filename_maxsize'],
144             backupCount = config.config['logging_filename_count'],
145         )
146         handler.setLevel(numeric_level)
147         handler.setFormatter(
148             logging.Formatter(
149                 fmt=fmt,
150                 datefmt=config.config['logging_date_format'],
151             )
152         )
153         handlers.append(handler)
154
155     if config.config['logging_console']:
156         handler = logging.StreamHandler(sys.stderr)
157         handler.setLevel(numeric_level)
158         handler.setFormatter(
159             logging.Formatter(
160                 fmt=fmt,
161                 datefmt=config.config['logging_date_format'],
162             )
163         )
164         handlers.append(handler)
165
166     if len(handlers) == 0:
167         handlers.append(logging.NullHandler())
168
169     for handler in handlers:
170         logger.addHandler(handler)
171     if config.config['logging_info_is_print']:
172         handler = logging.StreamHandler(sys.stdout)
173         handler.addFilter(OnlyInfoFilter())
174         logger.addHandler(handler)
175     logger.setLevel(numeric_level)
176     logger.propagate = False
177     return logger
178
179
180 def get_logger(name: str = ""):
181     logger = logging.getLogger(name)
182     return initialize_logging(logger)
183
184
185 def tprint(*args, **kwargs) -> None:
186     if config.config['logging_debug_threads']:
187         print(f'{tu.current_thread_id()}', end="")
188         print(*args, **kwargs)
189     else:
190         pass
191
192
193 def dprint(*args, **kwargs) -> None:
194     print(*args, file=sys.stderr, **kwargs)
195
196
197 class OutputSink(object):
198
199     # Bits in the destination_bitv bitvector.  Used to indicate the
200     # output destination.
201     STDOUT = 0x1
202     STDERR = 0x2
203     LOG_DEBUG = 0x4          # -\
204     LOG_INFO = 0x8           #  |
205     LOG_WARNING = 0x10       #   > Should provide logger to the c'tor.
206     LOG_ERROR = 0x20         #  |
207     LOG_CRITICAL = 0x40      # _/
208     FILENAME = 0x80          # Must provide a filename to the c'tor.
209     HLOG = 0x100
210
211     ALL_LOG_DESTINATIONS = (
212         LOG_DEBUG | LOG_INFO | LOG_WARNING | LOG_ERROR | LOG_CRITICAL
213     )
214     ALL_OUTPUT_DESTINATIONS = 0x1FF
215
216     def __init__(self,
217                  destination_bitv: int,
218                  *,
219                  logger=None,
220                  filename=None):
221         if logger is None:
222             logger = logging.getLogger(None)
223         self.logger = logger
224
225         if filename is not None:
226             self.f = open(filename, "wb", buffering=0)
227         else:
228             if self.destination_bitv & OutputSink.FILENAME:
229                 raise ValueError(
230                     "Filename argument is required if bitv & FILENAME"
231                 )
232             self.f = None
233         self.set_destination_bitv(destination_bitv)
234
235     def get_destination_bitv(self):
236         return self.destination_bitv
237
238     def set_destination_bitv(self, destination_bitv: int):
239         if destination_bitv & self.FILENAME and self.f is None:
240             raise ValueError(
241                 "Filename argument is required if bitv & FILENAME"
242             )
243         self.destination_bitv = destination_bitv
244
245     def print(self, *args, **kwargs):
246         end = kwargs.pop("end", None)
247         if end is not None:
248             if not isinstance(end, str):
249                 raise TypeError("end must be None or a string")
250         sep = kwargs.pop("sep", None)
251         if sep is not None:
252             if not isinstance(sep, str):
253                 raise TypeError("sep must be None or a string")
254         if kwargs:
255             raise TypeError("invalid keyword arguments to print()")
256         buf = su.sprintf(*args, end="", sep=sep)
257         if sep is None:
258             sep = " "
259         if end is None:
260             end = "\n"
261         if self.destination_bitv & self.STDOUT:
262             print(buf, file=sys.stdout, sep=sep, end=end)
263         if self.destination_bitv & self.STDERR:
264             print(buf, file=sys.stderr, sep=sep, end=end)
265         if end == '\n':
266             buf += '\n'
267         if self.destination_bitv & self.FILENAME and self.f is not None:
268             self.f.write(buf.encode('utf-8'))
269             self.f.flush()
270         buf = su.strip_escape_sequences(buf)
271         if self.logger is not None:
272             if self.destination_bitv & self.LOG_DEBUG:
273                 self.logger.debug(buf)
274             if self.destination_bitv & self.LOG_INFO:
275                 self.logger.info(buf)
276             if self.destination_bitv & self.LOG_WARNING:
277                 self.logger.warning(buf)
278             if self.destination_bitv & self.LOG_ERROR:
279                 self.logger.error(buf)
280             if self.destination_bitv & self.LOG_CRITICAL:
281                 self.logger.critical(buf)
282         if self.destination_bitv & self.HLOG:
283             hlog(buf)
284
285     def close(self):
286         if self.f is not None:
287             self.f.close()
288
289
290 class OutputContext(OutputSink, contextlib.ContextDecorator):
291     def __init__(self,
292                  destination_bitv: int,
293                  *,
294                  logger=None,
295                  filename=None):
296         super().__init__(destination_bitv, logger=logger, filename=filename)
297
298     def __enter__(self):
299         return self
300
301     def __exit__(self, etype, value, traceback):
302         super().close()
303         if etype is not None:
304             return False
305         return True
306
307
308 def hlog(message: str) -> None:
309     message = message.replace("'", "'\"'\"'")
310     os.system(f"/usr/bin/logger -p local7.info -- '{message}'")