blob: daa955136bc365cc24230eb25ebe0ab78107dbdd [file] [log] [blame]
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -04001# Copyright 2015 The Swarming Authors. All rights reserved.
2# Use of this source code is governed under the Apache License, Version 2.0 that
3# can be found in the LICENSE file.
4
5"""Utility relating to logging."""
6
7import argparse
8import codecs
9import logging
10import logging.handlers
11import optparse
12import os
13import sys
14import tempfile
15import time
16
17
18# This works around file locking issue on Windows specifically in the case of
19# long lived child processes.
20#
21# Python opens files with inheritable handle and without file sharing by
22# default. This causes the RotatingFileHandler file handle to be duplicated in
23# the subprocesses even if the log file is not used in it. Because of this
24# handle in the child process, when the RotatingFileHandler tries to os.rename()
25# the file in the parent process, it fails with:
26# WindowsError: [Error 32] The process cannot access the file because
27# it is being used by another process
28if sys.platform == 'win32':
29 import msvcrt # pylint: disable=F0401
30 import _subprocess # pylint: disable=F0401
31
32 # TODO(maruel): Make it work in cygwin too if necessary. This would have to
33 # use ctypes.cdll.kernel32 instead of _subprocess and msvcrt.
34
35 def _duplicate(handle):
36 target_process = _subprocess.GetCurrentProcess()
37 return _subprocess.DuplicateHandle(
38 _subprocess.GetCurrentProcess(), handle, target_process,
39 0, False, _subprocess.DUPLICATE_SAME_ACCESS).Detach()
40
41
42 class NoInheritRotatingFileHandler(logging.handlers.RotatingFileHandler):
43 def _open(self):
44 """Opens the current file without handle inheritance."""
45 if self.encoding is None:
46 with open(self.baseFilename, self.mode) as stream:
47 newosf = _duplicate(msvcrt.get_osfhandle(stream.fileno()))
48 new_fd = msvcrt.open_osfhandle(newosf, os.O_APPEND)
49 return os.fdopen(new_fd, self.mode)
50 return codecs.open(self.baseFilename, self.mode, self.encoding)
51
52
53else: # Not Windows.
54
55
56 NoInheritRotatingFileHandler = logging.handlers.RotatingFileHandler
57
58
59# Levels used for logging.
60LEVELS = [logging.ERROR, logging.INFO, logging.DEBUG]
61
62
63class CaptureLogs(object):
64 """Captures all the logs in a context."""
65 def __init__(self, prefix, root=None):
66 handle, self._path = tempfile.mkstemp(prefix=prefix, suffix='.log')
67 os.close(handle)
68 self._handler = logging.FileHandler(self._path, 'w')
69 self._handler.setLevel(logging.DEBUG)
70 formatter = UTCFormatter(
71 '%(process)d %(asctime)s: %(levelname)-5s %(message)s')
72 self._handler.setFormatter(formatter)
73 self._root = root or logging.getLogger()
74 self._root.addHandler(self._handler)
75 assert self._root.isEnabledFor(logging.DEBUG)
76
77 def read(self):
78 """Returns the current content of the logs.
79
80 This also closes the log capture so future logs will not be captured.
81 """
82 self._disconnect()
83 assert self._path
84 try:
85 with open(self._path, 'rb') as f:
86 return f.read()
87 except IOError as e:
88 return 'Failed to read %s: %s' % (self._path, e)
89
90 def close(self):
91 """Closes and delete the log."""
92 self._disconnect()
93 if self._path:
94 try:
95 os.remove(self._path)
96 except OSError as e:
97 logging.error('Failed to delete log file %s: %s', self._path, e)
98 self._path = None
99
100 def __enter__(self):
101 return self
102
103 def __exit__(self, _exc_type, _exc_value, _traceback):
104 self.close()
105
106 def _disconnect(self):
107 if self._handler:
108 self._root.removeHandler(self._handler)
109 self._handler.close()
110 self._handler = None
111
112
113class UTCFormatter(logging.Formatter):
114 converter = time.gmtime
115
116 def formatTime(self, record, datefmt=None):
117 """Change is ',' to '.'."""
118 ct = self.converter(record.created)
119 if datefmt:
120 return time.strftime(datefmt, ct)
121 else:
122 t = time.strftime("%Y-%m-%d %H:%M:%S", ct)
123 return "%s.%03d" % (t, record.msecs)
124
125
126class Filter(logging.Filter):
127 """Adds fields used by the infra-specific formatter.
128
129 Fields added:
130 - 'severity': one-letter indicator of log level (first letter of levelname).
131 """
132
133 def filter(self, record):
134 record.severity = record.levelname[0]
135 return True
136
137
138def find_stderr(root=None):
139 """Returns the logging.handler streaming to stderr, if any."""
140 for log in (root or logging.getLogger()).handlers:
141 if getattr(log, 'stream', None) is sys.stderr:
142 return log
143
144
145def prepare_logging(filename, root=None):
146 """Prepare logging for scripts.
147
148 Makes it log in UTC all the time. Prepare a rotating file based log.
149 """
150 assert not find_stderr(root)
151 formatter = UTCFormatter('%(process)d %(asctime)s %(severity)s: %(message)s')
152
153 # It is a requirement that the root logger is set to DEBUG, so the messages
154 # are not lost. It defaults to WARNING otherwise.
155 logger = root or logging.getLogger()
maruel26cfc602015-09-04 19:12:55 -0700156 if not logger:
157 # Better print insanity than crash.
158 print >> sys.stderr, 'OMG NO ROOT'
159 return
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400160 logger.setLevel(logging.DEBUG)
161
162 stderr = logging.StreamHandler()
163 stderr.setFormatter(formatter)
164 stderr.addFilter(Filter())
165 # Default to ERROR.
166 stderr.setLevel(logging.ERROR)
167 logger.addHandler(stderr)
168
169 # Setup up logging to a constant file so we can debug issues where
170 # the results aren't properly sent to the result URL.
171 if filename:
172 try:
173 rotating_file = NoInheritRotatingFileHandler(
174 filename, maxBytes=10 * 1024 * 1024, backupCount=5,
175 encoding='utf-8')
176 rotating_file.setLevel(logging.DEBUG)
177 rotating_file.setFormatter(formatter)
178 rotating_file.addFilter(Filter())
179 logger.addHandler(rotating_file)
180 except Exception:
181 # May happen on cygwin. Do not crash.
182 logging.exception('Failed to open %s', filename)
183
184
185def set_console_level(level, root=None):
186 """Reset the console (stderr) logging level."""
187 handler = find_stderr(root)
maruel26cfc602015-09-04 19:12:55 -0700188 if not handler:
189 # Better print insanity than crash.
190 print >> sys.stderr, 'OMG NO STDERR'
191 return
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400192 handler.setLevel(level)
193
194
195class OptionParserWithLogging(optparse.OptionParser):
196 """Adds --verbose option."""
197
198 # Set to True to enable --log-file options.
199 enable_log_file = True
200
201 # Set in unit tests.
202 logger_root = None
203
204 def __init__(self, verbose=0, log_file=None, **kwargs):
205 kwargs.setdefault('description', sys.modules['__main__'].__doc__)
206 optparse.OptionParser.__init__(self, **kwargs)
207 self.group_logging = optparse.OptionGroup(self, 'Logging')
208 self.group_logging.add_option(
209 '-v', '--verbose',
210 action='count',
211 default=verbose,
212 help='Use multiple times to increase verbosity')
213 if self.enable_log_file:
214 self.group_logging.add_option(
215 '-l', '--log-file',
216 default=log_file,
217 help='The name of the file to store rotating log details')
218 self.group_logging.add_option(
219 '--no-log', action='store_const', const='', dest='log_file',
220 help='Disable log file')
221
222 def parse_args(self, *args, **kwargs):
223 # Make sure this group is always the last one.
224 self.add_option_group(self.group_logging)
225
226 options, args = optparse.OptionParser.parse_args(self, *args, **kwargs)
227 prepare_logging(self.enable_log_file and options.log_file, self.logger_root)
228 set_console_level(
229 LEVELS[min(len(LEVELS) - 1, options.verbose)], self.logger_root)
230 return options, args
231
232
233class ArgumentParserWithLogging(argparse.ArgumentParser):
234 """Adds --verbose option."""
235
236 # Set to True to enable --log-file options.
237 enable_log_file = True
238
239 def __init__(self, verbose=0, log_file=None, **kwargs):
240 kwargs.setdefault('description', sys.modules['__main__'].__doc__)
241 kwargs.setdefault('conflict_handler', 'resolve')
242 self.__verbose = verbose
243 self.__log_file = log_file
244 super(ArgumentParserWithLogging, self).__init__(**kwargs)
245
246 def _add_logging_group(self):
247 group = self.add_argument_group('Logging')
248 group.add_argument(
249 '-v', '--verbose',
250 action='count',
251 default=self.__verbose,
252 help='Use multiple times to increase verbosity')
253 if self.enable_log_file:
254 group.add_argument(
255 '-l', '--log-file',
256 default=self.__log_file,
257 help='The name of the file to store rotating log details')
258 group.add_argument(
259 '--no-log', action='store_const', const='', dest='log_file',
260 help='Disable log file')
261
262 def parse_args(self, *args, **kwargs):
263 # Make sure this group is always the last one.
264 self._add_logging_group()
265
266 args = super(ArgumentParserWithLogging, self).parse_args(*args, **kwargs)
267 prepare_logging(self.enable_log_file and args.log_file, self.logger_root)
268 set_console_level(
269 LEVELS[min(len(LEVELS) - 1, args.verbose)], self.logger_root)
270 return args