blob: 879e03c8a7a4e1fbe9037602d61ed1a247d31ca1 [file] [log] [blame]
maruelea586f32016-04-05 11:11:33 -07001# Copyright 2015 The LUCI Authors. All rights reserved.
maruelf1f5e2a2016-05-25 17:10:39 -07002# Use of this source code is governed under the Apache License, Version 2.0
3# that can be found in the LICENSE file.
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04004
5"""Utility relating to logging."""
6
Marc-Antoine Ruelf899c482019-10-10 23:32:06 +00007from __future__ import print_function
8
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04009import argparse
10import codecs
maruel625c1ea2015-09-09 11:41:13 -070011import ctypes
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040012import logging
13import logging.handlers
14import optparse
15import os
16import sys
17import tempfile
18import time
19
nodir9130f072016-05-27 13:59:08 -070020from utils import file_path
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040021
22# This works around file locking issue on Windows specifically in the case of
23# long lived child processes.
24#
25# Python opens files with inheritable handle and without file sharing by
26# default. This causes the RotatingFileHandler file handle to be duplicated in
27# the subprocesses even if the log file is not used in it. Because of this
28# handle in the child process, when the RotatingFileHandler tries to os.rename()
29# the file in the parent process, it fails with:
30# WindowsError: [Error 32] The process cannot access the file because
31# it is being used by another process
32if sys.platform == 'win32':
maruel625c1ea2015-09-09 11:41:13 -070033 import ctypes
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040034 import msvcrt # pylint: disable=F0401
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040035
Marc-Antoine Ruel0eb2eb22019-01-29 21:00:16 +000036 FILE_ATTRIBUTE_NORMAL = 0x80
maruel625c1ea2015-09-09 11:41:13 -070037 FILE_SHARE_READ = 1
38 FILE_SHARE_WRITE = 2
39 FILE_SHARE_DELETE = 4
40 GENERIC_READ = 0x80000000
41 GENERIC_WRITE = 0x40000000
42 OPEN_ALWAYS = 4
43
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040044 # TODO(maruel): Make it work in cygwin too if necessary. This would have to
Momo Sasaki049125c2021-02-26 06:14:41 +000045 # use ctypes.cdll.kernel32 instead of msvcrt.
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040046
maruel625c1ea2015-09-09 11:41:13 -070047
48 def shared_open(path):
49 """Opens a file with full sharing mode and without inheritance.
50
51 The file is open for both read and write.
52
53 See https://bugs.python.org/issue15244 for inspiration.
54 """
maruel625c1ea2015-09-09 11:41:13 -070055 handle = ctypes.windll.kernel32.CreateFileW(
56 path,
57 GENERIC_READ|GENERIC_WRITE,
58 FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
59 None,
60 OPEN_ALWAYS,
61 FILE_ATTRIBUTE_NORMAL,
62 None)
63 ctr_handle = msvcrt.open_osfhandle(handle, os.O_BINARY | os.O_NOINHERIT)
64 return os.fdopen(ctr_handle, 'r+b')
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040065
66
67 class NoInheritRotatingFileHandler(logging.handlers.RotatingFileHandler):
68 def _open(self):
maruel625c1ea2015-09-09 11:41:13 -070069 """Opens the log file without handle inheritance but with file sharing.
70
71 Ignores self.mode.
72 """
73 f = shared_open(self.baseFilename)
74 if self.encoding:
75 # Do the equivalent of
76 # codecs.open(self.baseFilename, self.mode, self.encoding)
77 info = codecs.lookup(self.encoding)
78 f = codecs.StreamReaderWriter(
79 f, info.streamreader, info.streamwriter, 'replace')
80 f.encoding = self.encoding
81 return f
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040082
83
84else: # Not Windows.
85
86
87 NoInheritRotatingFileHandler = logging.handlers.RotatingFileHandler
88
Jonah Hooper9b5bd8c2022-07-21 15:33:41 +000089# INFO has a priority of 20
90USER_LOGS = logging.INFO + 5
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040091
92# Levels used for logging.
93LEVELS = [logging.ERROR, logging.INFO, logging.DEBUG]
94
95
Junji Watanabeab2102a2022-01-12 01:44:04 +000096class CaptureLogs:
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040097 """Captures all the logs in a context."""
98 def __init__(self, prefix, root=None):
99 handle, self._path = tempfile.mkstemp(prefix=prefix, suffix='.log')
100 os.close(handle)
101 self._handler = logging.FileHandler(self._path, 'w')
102 self._handler.setLevel(logging.DEBUG)
103 formatter = UTCFormatter(
104 '%(process)d %(asctime)s: %(levelname)-5s %(message)s')
105 self._handler.setFormatter(formatter)
106 self._root = root or logging.getLogger()
107 self._root.addHandler(self._handler)
108 assert self._root.isEnabledFor(logging.DEBUG)
109
110 def read(self):
111 """Returns the current content of the logs.
112
113 This also closes the log capture so future logs will not be captured.
114 """
115 self._disconnect()
116 assert self._path
117 try:
118 with open(self._path, 'rb') as f:
119 return f.read()
120 except IOError as e:
121 return 'Failed to read %s: %s' % (self._path, e)
122
123 def close(self):
124 """Closes and delete the log."""
125 self._disconnect()
126 if self._path:
127 try:
128 os.remove(self._path)
129 except OSError as e:
130 logging.error('Failed to delete log file %s: %s', self._path, e)
131 self._path = None
132
133 def __enter__(self):
134 return self
135
136 def __exit__(self, _exc_type, _exc_value, _traceback):
137 self.close()
138
139 def _disconnect(self):
140 if self._handler:
141 self._root.removeHandler(self._handler)
142 self._handler.close()
143 self._handler = None
144
145
146class UTCFormatter(logging.Formatter):
147 converter = time.gmtime
148
149 def formatTime(self, record, datefmt=None):
150 """Change is ',' to '.'."""
151 ct = self.converter(record.created)
152 if datefmt:
153 return time.strftime(datefmt, ct)
Junji Watanabeab2102a2022-01-12 01:44:04 +0000154 t = time.strftime("%Y-%m-%d %H:%M:%S", ct)
155 return "%s.%03d" % (t, record.msecs)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400156
157
Jonah Hooper9b5bd8c2022-07-21 15:33:41 +0000158class SeverityFilter(logging.Filter):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400159 """Adds fields used by the infra-specific formatter.
160
161 Fields added:
162 - 'severity': one-letter indicator of log level (first letter of levelname).
163 """
164
165 def filter(self, record):
166 record.severity = record.levelname[0]
167 return True
168
169
170def find_stderr(root=None):
171 """Returns the logging.handler streaming to stderr, if any."""
172 for log in (root or logging.getLogger()).handlers:
173 if getattr(log, 'stream', None) is sys.stderr:
174 return log
175
176
Justin Luong716b4b02023-02-08 02:47:52 +0000177def new_rotating_file_handler(
178 filename,
179 utc_formatter=UTCFormatter(
180 '%(process)d %(asctime)s %(severity)s: %(message)s')):
181 file_path.ensure_tree(os.path.dirname(os.path.abspath(filename)))
182 rotating_file = NoInheritRotatingFileHandler(filename,
183 maxBytes=10 * 1024 * 1024,
184 backupCount=5,
185 encoding='utf-8')
186 rotating_file.setLevel(logging.DEBUG)
187 rotating_file.setFormatter(utc_formatter)
188 rotating_file.addFilter(SeverityFilter())
189 return rotating_file
190
Jonah Hooper9b5bd8c2022-07-21 15:33:41 +0000191
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400192def prepare_logging(filename, root=None):
193 """Prepare logging for scripts.
194
195 Makes it log in UTC all the time. Prepare a rotating file based log.
196 """
197 assert not find_stderr(root)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400198
199 # It is a requirement that the root logger is set to DEBUG, so the messages
200 # are not lost. It defaults to WARNING otherwise.
201 logger = root or logging.getLogger()
maruel26cfc602015-09-04 19:12:55 -0700202 if not logger:
203 # Better print insanity than crash.
Marc-Antoine Ruelf899c482019-10-10 23:32:06 +0000204 print('OMG NO ROOT', file=sys.stderr)
maruel26cfc602015-09-04 19:12:55 -0700205 return
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400206 logger.setLevel(logging.DEBUG)
207
Jonah Hooper9b5bd8c2022-07-21 15:33:41 +0000208 utc_formatter = UTCFormatter(
209 '%(process)d %(asctime)s %(severity)s: %(message)s')
210
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400211 stderr = logging.StreamHandler()
Jonah Hooper9b5bd8c2022-07-21 15:33:41 +0000212 stderr.setFormatter(utc_formatter)
213 stderr.addFilter(SeverityFilter())
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400214 # Default to ERROR.
215 stderr.setLevel(logging.ERROR)
216 logger.addHandler(stderr)
217
218 # Setup up logging to a constant file so we can debug issues where
219 # the results aren't properly sent to the result URL.
220 if filename:
221 try:
Justin Luong716b4b02023-02-08 02:47:52 +0000222 logger.addHandler(new_rotating_file_handler(filename, utc_formatter))
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400223 except Exception:
224 # May happen on cygwin. Do not crash.
225 logging.exception('Failed to open %s', filename)
226
227
228def set_console_level(level, root=None):
229 """Reset the console (stderr) logging level."""
230 handler = find_stderr(root)
maruel26cfc602015-09-04 19:12:55 -0700231 if not handler:
232 # Better print insanity than crash.
Marc-Antoine Ruelf899c482019-10-10 23:32:06 +0000233 print('OMG NO STDERR', file=sys.stderr)
maruel26cfc602015-09-04 19:12:55 -0700234 return
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400235 handler.setLevel(level)
236
237
Jonah Hooper9b5bd8c2022-07-21 15:33:41 +0000238class ExactLevelFilter(logging.Filter):
239 """
240 A log filter which filters out log messages
241 which are not an exact level.
242 """
243
244 def __init__(self, level):
245 super(ExactLevelFilter, self).__init__()
246 self._level = level
247
248 def filter(self, record):
249 return record.levelno == self._level
250
251
252def set_user_level_logging(logger=None):
253 """
254 Creates a logger which writes logger.level = USER_LOGS to stdout.
255 Some scripts (run_isolated.py) have all their output displayed to
256 users in the swarming UI. As a result we may want to display some
257 logging as more important than others. These are not errors so should
258 not be logged to stderr.
259
260 USER_LOGS are seen as more important than INFO logs but less
261 important than error logs.
262
263 That may lead to double logging if say another stream is already
264 configured to listen to logging for stdout.
265 """
266 if logger is None:
267 logger = logging.getLogger()
268
269 logging.addLevelName(USER_LOGS, "USER_LOGS")
270 user_logs_formatter = UTCFormatter(
271 "swarming_bot_logs: %(asctime)s: %(message)s")
272
273 stdout_handler = logging.StreamHandler(stream=sys.stdout)
274 stdout_handler.setLevel(USER_LOGS)
275 stdout_handler.addFilter(ExactLevelFilter(USER_LOGS))
276 stdout_handler.addFilter(SeverityFilter())
277 stdout_handler.setFormatter(user_logs_formatter)
278 logger.addHandler(stdout_handler)
279
280
281def user_logs(message, *args, **kwargs):
282 logging.log(USER_LOGS, message, *args, **kwargs)
283
284
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400285class OptionParserWithLogging(optparse.OptionParser):
286 """Adds --verbose option."""
287
288 # Set to True to enable --log-file options.
289 enable_log_file = True
290
291 # Set in unit tests.
292 logger_root = None
293
294 def __init__(self, verbose=0, log_file=None, **kwargs):
295 kwargs.setdefault('description', sys.modules['__main__'].__doc__)
296 optparse.OptionParser.__init__(self, **kwargs)
297 self.group_logging = optparse.OptionGroup(self, 'Logging')
298 self.group_logging.add_option(
299 '-v', '--verbose',
300 action='count',
301 default=verbose,
302 help='Use multiple times to increase verbosity')
303 if self.enable_log_file:
304 self.group_logging.add_option(
305 '-l', '--log-file',
306 default=log_file,
307 help='The name of the file to store rotating log details')
308 self.group_logging.add_option(
309 '--no-log', action='store_const', const='', dest='log_file',
310 help='Disable log file')
311
312 def parse_args(self, *args, **kwargs):
313 # Make sure this group is always the last one.
314 self.add_option_group(self.group_logging)
315
316 options, args = optparse.OptionParser.parse_args(self, *args, **kwargs)
317 prepare_logging(self.enable_log_file and options.log_file, self.logger_root)
318 set_console_level(
319 LEVELS[min(len(LEVELS) - 1, options.verbose)], self.logger_root)
Jonah Hooper9b5bd8c2022-07-21 15:33:41 +0000320
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400321 return options, args
322
323
324class ArgumentParserWithLogging(argparse.ArgumentParser):
325 """Adds --verbose option."""
326
327 # Set to True to enable --log-file options.
328 enable_log_file = True
329
330 def __init__(self, verbose=0, log_file=None, **kwargs):
331 kwargs.setdefault('description', sys.modules['__main__'].__doc__)
332 kwargs.setdefault('conflict_handler', 'resolve')
333 self.__verbose = verbose
334 self.__log_file = log_file
335 super(ArgumentParserWithLogging, self).__init__(**kwargs)
336
337 def _add_logging_group(self):
338 group = self.add_argument_group('Logging')
339 group.add_argument(
340 '-v', '--verbose',
341 action='count',
342 default=self.__verbose,
343 help='Use multiple times to increase verbosity')
344 if self.enable_log_file:
345 group.add_argument(
346 '-l', '--log-file',
347 default=self.__log_file,
348 help='The name of the file to store rotating log details')
349 group.add_argument(
350 '--no-log', action='store_const', const='', dest='log_file',
351 help='Disable log file')
352
353 def parse_args(self, *args, **kwargs):
354 # Make sure this group is always the last one.
355 self._add_logging_group()
356
357 args = super(ArgumentParserWithLogging, self).parse_args(*args, **kwargs)
358 prepare_logging(self.enable_log_file and args.log_file, self.logger_root)
359 set_console_level(
360 LEVELS[min(len(LEVELS) - 1, args.verbose)], self.logger_root)
Jonah Hooper9b5bd8c2022-07-21 15:33:41 +0000361
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400362 return args