blob: 9b559f597eac1a38a453860ce49af3986c4f179f [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
7import argparse
8import codecs
maruel625c1ea2015-09-09 11:41:13 -07009import ctypes
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040010import logging
11import logging.handlers
12import optparse
13import os
14import sys
15import tempfile
16import time
17
nodir9130f072016-05-27 13:59:08 -070018from utils import file_path
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040019
20# This works around file locking issue on Windows specifically in the case of
21# long lived child processes.
22#
23# Python opens files with inheritable handle and without file sharing by
24# default. This causes the RotatingFileHandler file handle to be duplicated in
25# the subprocesses even if the log file is not used in it. Because of this
26# handle in the child process, when the RotatingFileHandler tries to os.rename()
27# the file in the parent process, it fails with:
28# WindowsError: [Error 32] The process cannot access the file because
29# it is being used by another process
30if sys.platform == 'win32':
maruel625c1ea2015-09-09 11:41:13 -070031 import ctypes
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040032 import msvcrt # pylint: disable=F0401
33 import _subprocess # pylint: disable=F0401
34
maruel625c1ea2015-09-09 11:41:13 -070035 FILE_ATTRIBUTE_NORMAL = 0x00000080
36 FILE_SHARE_READ = 1
37 FILE_SHARE_WRITE = 2
38 FILE_SHARE_DELETE = 4
39 GENERIC_READ = 0x80000000
40 GENERIC_WRITE = 0x40000000
41 OPEN_ALWAYS = 4
42
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -040043 # TODO(maruel): Make it work in cygwin too if necessary. This would have to
44 # use ctypes.cdll.kernel32 instead of _subprocess and msvcrt.
45
maruel625c1ea2015-09-09 11:41:13 -070046
47 def shared_open(path):
48 """Opens a file with full sharing mode and without inheritance.
49
50 The file is open for both read and write.
51
52 See https://bugs.python.org/issue15244 for inspiration.
53 """
54 path = unicode(path)
55 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
89
90# Levels used for logging.
91LEVELS = [logging.ERROR, logging.INFO, logging.DEBUG]
92
93
94class CaptureLogs(object):
95 """Captures all the logs in a context."""
96 def __init__(self, prefix, root=None):
97 handle, self._path = tempfile.mkstemp(prefix=prefix, suffix='.log')
98 os.close(handle)
99 self._handler = logging.FileHandler(self._path, 'w')
100 self._handler.setLevel(logging.DEBUG)
101 formatter = UTCFormatter(
102 '%(process)d %(asctime)s: %(levelname)-5s %(message)s')
103 self._handler.setFormatter(formatter)
104 self._root = root or logging.getLogger()
105 self._root.addHandler(self._handler)
106 assert self._root.isEnabledFor(logging.DEBUG)
107
108 def read(self):
109 """Returns the current content of the logs.
110
111 This also closes the log capture so future logs will not be captured.
112 """
113 self._disconnect()
114 assert self._path
115 try:
116 with open(self._path, 'rb') as f:
117 return f.read()
118 except IOError as e:
119 return 'Failed to read %s: %s' % (self._path, e)
120
121 def close(self):
122 """Closes and delete the log."""
123 self._disconnect()
124 if self._path:
125 try:
126 os.remove(self._path)
127 except OSError as e:
128 logging.error('Failed to delete log file %s: %s', self._path, e)
129 self._path = None
130
131 def __enter__(self):
132 return self
133
134 def __exit__(self, _exc_type, _exc_value, _traceback):
135 self.close()
136
137 def _disconnect(self):
138 if self._handler:
139 self._root.removeHandler(self._handler)
140 self._handler.close()
141 self._handler = None
142
143
144class UTCFormatter(logging.Formatter):
145 converter = time.gmtime
146
147 def formatTime(self, record, datefmt=None):
148 """Change is ',' to '.'."""
149 ct = self.converter(record.created)
150 if datefmt:
151 return time.strftime(datefmt, ct)
152 else:
153 t = time.strftime("%Y-%m-%d %H:%M:%S", ct)
154 return "%s.%03d" % (t, record.msecs)
155
156
maruel9fd83362015-10-01 10:51:27 -0700157class Filter(object):
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400158 """Adds fields used by the infra-specific formatter.
159
160 Fields added:
161 - 'severity': one-letter indicator of log level (first letter of levelname).
162 """
163
164 def filter(self, record):
165 record.severity = record.levelname[0]
166 return True
167
168
169def find_stderr(root=None):
170 """Returns the logging.handler streaming to stderr, if any."""
171 for log in (root or logging.getLogger()).handlers:
172 if getattr(log, 'stream', None) is sys.stderr:
173 return log
174
175
176def prepare_logging(filename, root=None):
177 """Prepare logging for scripts.
178
179 Makes it log in UTC all the time. Prepare a rotating file based log.
180 """
181 assert not find_stderr(root)
182 formatter = UTCFormatter('%(process)d %(asctime)s %(severity)s: %(message)s')
183
184 # It is a requirement that the root logger is set to DEBUG, so the messages
185 # are not lost. It defaults to WARNING otherwise.
186 logger = root or logging.getLogger()
maruel26cfc602015-09-04 19:12:55 -0700187 if not logger:
188 # Better print insanity than crash.
189 print >> sys.stderr, 'OMG NO ROOT'
190 return
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400191 logger.setLevel(logging.DEBUG)
192
193 stderr = logging.StreamHandler()
194 stderr.setFormatter(formatter)
195 stderr.addFilter(Filter())
196 # Default to ERROR.
197 stderr.setLevel(logging.ERROR)
198 logger.addHandler(stderr)
199
200 # Setup up logging to a constant file so we can debug issues where
201 # the results aren't properly sent to the result URL.
202 if filename:
nodir9130f072016-05-27 13:59:08 -0700203 file_path.ensure_tree(os.path.dirname(os.path.abspath(unicode(filename))))
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400204 try:
205 rotating_file = NoInheritRotatingFileHandler(
206 filename, maxBytes=10 * 1024 * 1024, backupCount=5,
207 encoding='utf-8')
208 rotating_file.setLevel(logging.DEBUG)
209 rotating_file.setFormatter(formatter)
210 rotating_file.addFilter(Filter())
211 logger.addHandler(rotating_file)
212 except Exception:
213 # May happen on cygwin. Do not crash.
214 logging.exception('Failed to open %s', filename)
215
216
217def set_console_level(level, root=None):
218 """Reset the console (stderr) logging level."""
219 handler = find_stderr(root)
maruel26cfc602015-09-04 19:12:55 -0700220 if not handler:
221 # Better print insanity than crash.
222 print >> sys.stderr, 'OMG NO STDERR'
223 return
Marc-Antoine Ruelf74cffe2015-07-15 15:21:34 -0400224 handler.setLevel(level)
225
226
227class OptionParserWithLogging(optparse.OptionParser):
228 """Adds --verbose option."""
229
230 # Set to True to enable --log-file options.
231 enable_log_file = True
232
233 # Set in unit tests.
234 logger_root = None
235
236 def __init__(self, verbose=0, log_file=None, **kwargs):
237 kwargs.setdefault('description', sys.modules['__main__'].__doc__)
238 optparse.OptionParser.__init__(self, **kwargs)
239 self.group_logging = optparse.OptionGroup(self, 'Logging')
240 self.group_logging.add_option(
241 '-v', '--verbose',
242 action='count',
243 default=verbose,
244 help='Use multiple times to increase verbosity')
245 if self.enable_log_file:
246 self.group_logging.add_option(
247 '-l', '--log-file',
248 default=log_file,
249 help='The name of the file to store rotating log details')
250 self.group_logging.add_option(
251 '--no-log', action='store_const', const='', dest='log_file',
252 help='Disable log file')
253
254 def parse_args(self, *args, **kwargs):
255 # Make sure this group is always the last one.
256 self.add_option_group(self.group_logging)
257
258 options, args = optparse.OptionParser.parse_args(self, *args, **kwargs)
259 prepare_logging(self.enable_log_file and options.log_file, self.logger_root)
260 set_console_level(
261 LEVELS[min(len(LEVELS) - 1, options.verbose)], self.logger_root)
262 return options, args
263
264
265class ArgumentParserWithLogging(argparse.ArgumentParser):
266 """Adds --verbose option."""
267
268 # Set to True to enable --log-file options.
269 enable_log_file = True
270
271 def __init__(self, verbose=0, log_file=None, **kwargs):
272 kwargs.setdefault('description', sys.modules['__main__'].__doc__)
273 kwargs.setdefault('conflict_handler', 'resolve')
274 self.__verbose = verbose
275 self.__log_file = log_file
276 super(ArgumentParserWithLogging, self).__init__(**kwargs)
277
278 def _add_logging_group(self):
279 group = self.add_argument_group('Logging')
280 group.add_argument(
281 '-v', '--verbose',
282 action='count',
283 default=self.__verbose,
284 help='Use multiple times to increase verbosity')
285 if self.enable_log_file:
286 group.add_argument(
287 '-l', '--log-file',
288 default=self.__log_file,
289 help='The name of the file to store rotating log details')
290 group.add_argument(
291 '--no-log', action='store_const', const='', dest='log_file',
292 help='Disable log file')
293
294 def parse_args(self, *args, **kwargs):
295 # Make sure this group is always the last one.
296 self._add_logging_group()
297
298 args = super(ArgumentParserWithLogging, self).parse_args(*args, **kwargs)
299 prepare_logging(self.enable_log_file and args.log_file, self.logger_root)
300 set_console_level(
301 LEVELS[min(len(LEVELS) - 1, args.verbose)], self.logger_root)
302 return args