blob: fd928c69e35d970dd7f6eb06067f7dfd0be06eb2 [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
Jonah Hooper9b5bd8c2022-07-21 15:33:41 +0000177
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400178def prepare_logging(filename, root=None):
179 """Prepare logging for scripts.
180
181 Makes it log in UTC all the time. Prepare a rotating file based log.
182 """
183 assert not find_stderr(root)
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400184
185 # It is a requirement that the root logger is set to DEBUG, so the messages
186 # are not lost. It defaults to WARNING otherwise.
187 logger = root or logging.getLogger()
maruel26cfc602015-09-04 19:12:55 -0700188 if not logger:
189 # Better print insanity than crash.
Marc-Antoine Ruelf899c482019-10-10 23:32:06 +0000190 print('OMG NO ROOT', file=sys.stderr)
maruel26cfc602015-09-04 19:12:55 -0700191 return
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400192 logger.setLevel(logging.DEBUG)
193
Jonah Hooper9b5bd8c2022-07-21 15:33:41 +0000194 utc_formatter = UTCFormatter(
195 '%(process)d %(asctime)s %(severity)s: %(message)s')
196
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400197 stderr = logging.StreamHandler()
Jonah Hooper9b5bd8c2022-07-21 15:33:41 +0000198 stderr.setFormatter(utc_formatter)
199 stderr.addFilter(SeverityFilter())
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400200 # Default to ERROR.
201 stderr.setLevel(logging.ERROR)
202 logger.addHandler(stderr)
203
204 # Setup up logging to a constant file so we can debug issues where
205 # the results aren't properly sent to the result URL.
206 if filename:
Junji Watanabee1ab20f2022-01-13 03:51:15 +0000207 file_path.ensure_tree(os.path.dirname(os.path.abspath(filename)))
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400208 try:
209 rotating_file = NoInheritRotatingFileHandler(
210 filename, maxBytes=10 * 1024 * 1024, backupCount=5,
211 encoding='utf-8')
212 rotating_file.setLevel(logging.DEBUG)
Jonah Hooper9b5bd8c2022-07-21 15:33:41 +0000213 rotating_file.setFormatter(utc_formatter)
214 rotating_file.addFilter(SeverityFilter())
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400215 logger.addHandler(rotating_file)
216 except Exception:
217 # May happen on cygwin. Do not crash.
218 logging.exception('Failed to open %s', filename)
219
220
221def set_console_level(level, root=None):
222 """Reset the console (stderr) logging level."""
223 handler = find_stderr(root)
maruel26cfc602015-09-04 19:12:55 -0700224 if not handler:
225 # Better print insanity than crash.
Marc-Antoine Ruelf899c482019-10-10 23:32:06 +0000226 print('OMG NO STDERR', file=sys.stderr)
maruel26cfc602015-09-04 19:12:55 -0700227 return
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400228 handler.setLevel(level)
229
230
Jonah Hooper9b5bd8c2022-07-21 15:33:41 +0000231class ExactLevelFilter(logging.Filter):
232 """
233 A log filter which filters out log messages
234 which are not an exact level.
235 """
236
237 def __init__(self, level):
238 super(ExactLevelFilter, self).__init__()
239 self._level = level
240
241 def filter(self, record):
242 return record.levelno == self._level
243
244
245def set_user_level_logging(logger=None):
246 """
247 Creates a logger which writes logger.level = USER_LOGS to stdout.
248 Some scripts (run_isolated.py) have all their output displayed to
249 users in the swarming UI. As a result we may want to display some
250 logging as more important than others. These are not errors so should
251 not be logged to stderr.
252
253 USER_LOGS are seen as more important than INFO logs but less
254 important than error logs.
255
256 That may lead to double logging if say another stream is already
257 configured to listen to logging for stdout.
258 """
259 if logger is None:
260 logger = logging.getLogger()
261
262 logging.addLevelName(USER_LOGS, "USER_LOGS")
263 user_logs_formatter = UTCFormatter(
264 "swarming_bot_logs: %(asctime)s: %(message)s")
265
266 stdout_handler = logging.StreamHandler(stream=sys.stdout)
267 stdout_handler.setLevel(USER_LOGS)
268 stdout_handler.addFilter(ExactLevelFilter(USER_LOGS))
269 stdout_handler.addFilter(SeverityFilter())
270 stdout_handler.setFormatter(user_logs_formatter)
271 logger.addHandler(stdout_handler)
272
273
274def user_logs(message, *args, **kwargs):
275 logging.log(USER_LOGS, message, *args, **kwargs)
276
277
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400278class OptionParserWithLogging(optparse.OptionParser):
279 """Adds --verbose option."""
280
281 # Set to True to enable --log-file options.
282 enable_log_file = True
283
284 # Set in unit tests.
285 logger_root = None
286
287 def __init__(self, verbose=0, log_file=None, **kwargs):
288 kwargs.setdefault('description', sys.modules['__main__'].__doc__)
289 optparse.OptionParser.__init__(self, **kwargs)
290 self.group_logging = optparse.OptionGroup(self, 'Logging')
291 self.group_logging.add_option(
292 '-v', '--verbose',
293 action='count',
294 default=verbose,
295 help='Use multiple times to increase verbosity')
296 if self.enable_log_file:
297 self.group_logging.add_option(
298 '-l', '--log-file',
299 default=log_file,
300 help='The name of the file to store rotating log details')
301 self.group_logging.add_option(
302 '--no-log', action='store_const', const='', dest='log_file',
303 help='Disable log file')
304
305 def parse_args(self, *args, **kwargs):
306 # Make sure this group is always the last one.
307 self.add_option_group(self.group_logging)
308
309 options, args = optparse.OptionParser.parse_args(self, *args, **kwargs)
310 prepare_logging(self.enable_log_file and options.log_file, self.logger_root)
311 set_console_level(
312 LEVELS[min(len(LEVELS) - 1, options.verbose)], self.logger_root)
Jonah Hooper9b5bd8c2022-07-21 15:33:41 +0000313
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400314 return options, args
315
316
317class ArgumentParserWithLogging(argparse.ArgumentParser):
318 """Adds --verbose option."""
319
320 # Set to True to enable --log-file options.
321 enable_log_file = True
322
323 def __init__(self, verbose=0, log_file=None, **kwargs):
324 kwargs.setdefault('description', sys.modules['__main__'].__doc__)
325 kwargs.setdefault('conflict_handler', 'resolve')
326 self.__verbose = verbose
327 self.__log_file = log_file
328 super(ArgumentParserWithLogging, self).__init__(**kwargs)
329
330 def _add_logging_group(self):
331 group = self.add_argument_group('Logging')
332 group.add_argument(
333 '-v', '--verbose',
334 action='count',
335 default=self.__verbose,
336 help='Use multiple times to increase verbosity')
337 if self.enable_log_file:
338 group.add_argument(
339 '-l', '--log-file',
340 default=self.__log_file,
341 help='The name of the file to store rotating log details')
342 group.add_argument(
343 '--no-log', action='store_const', const='', dest='log_file',
344 help='Disable log file')
345
346 def parse_args(self, *args, **kwargs):
347 # Make sure this group is always the last one.
348 self._add_logging_group()
349
350 args = super(ArgumentParserWithLogging, self).parse_args(*args, **kwargs)
351 prepare_logging(self.enable_log_file and args.log_file, self.logger_root)
352 set_console_level(
353 LEVELS[min(len(LEVELS) - 1, args.verbose)], self.logger_root)
Jonah Hooper9b5bd8c2022-07-21 15:33:41 +0000354
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400355 return args