Marc-Antoine Ruel | f74cffe | 2015-07-15 15:21:34 -0400 | [diff] [blame^] | 1 | # 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 | |
| 7 | import argparse |
| 8 | import codecs |
| 9 | import logging |
| 10 | import logging.handlers |
| 11 | import optparse |
| 12 | import os |
| 13 | import sys |
| 14 | import tempfile |
| 15 | import 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 |
| 28 | if 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 | |
| 53 | else: # Not Windows. |
| 54 | |
| 55 | |
| 56 | NoInheritRotatingFileHandler = logging.handlers.RotatingFileHandler |
| 57 | |
| 58 | |
| 59 | # Levels used for logging. |
| 60 | LEVELS = [logging.ERROR, logging.INFO, logging.DEBUG] |
| 61 | |
| 62 | |
| 63 | class 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 | |
| 113 | class 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 | |
| 126 | class 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 | |
| 138 | def 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 | |
| 145 | def 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() |
| 156 | logger.setLevel(logging.DEBUG) |
| 157 | |
| 158 | stderr = logging.StreamHandler() |
| 159 | stderr.setFormatter(formatter) |
| 160 | stderr.addFilter(Filter()) |
| 161 | # Default to ERROR. |
| 162 | stderr.setLevel(logging.ERROR) |
| 163 | logger.addHandler(stderr) |
| 164 | |
| 165 | # Setup up logging to a constant file so we can debug issues where |
| 166 | # the results aren't properly sent to the result URL. |
| 167 | if filename: |
| 168 | try: |
| 169 | rotating_file = NoInheritRotatingFileHandler( |
| 170 | filename, maxBytes=10 * 1024 * 1024, backupCount=5, |
| 171 | encoding='utf-8') |
| 172 | rotating_file.setLevel(logging.DEBUG) |
| 173 | rotating_file.setFormatter(formatter) |
| 174 | rotating_file.addFilter(Filter()) |
| 175 | logger.addHandler(rotating_file) |
| 176 | except Exception: |
| 177 | # May happen on cygwin. Do not crash. |
| 178 | logging.exception('Failed to open %s', filename) |
| 179 | |
| 180 | |
| 181 | def set_console_level(level, root=None): |
| 182 | """Reset the console (stderr) logging level.""" |
| 183 | handler = find_stderr(root) |
| 184 | handler.setLevel(level) |
| 185 | |
| 186 | |
| 187 | class OptionParserWithLogging(optparse.OptionParser): |
| 188 | """Adds --verbose option.""" |
| 189 | |
| 190 | # Set to True to enable --log-file options. |
| 191 | enable_log_file = True |
| 192 | |
| 193 | # Set in unit tests. |
| 194 | logger_root = None |
| 195 | |
| 196 | def __init__(self, verbose=0, log_file=None, **kwargs): |
| 197 | kwargs.setdefault('description', sys.modules['__main__'].__doc__) |
| 198 | optparse.OptionParser.__init__(self, **kwargs) |
| 199 | self.group_logging = optparse.OptionGroup(self, 'Logging') |
| 200 | self.group_logging.add_option( |
| 201 | '-v', '--verbose', |
| 202 | action='count', |
| 203 | default=verbose, |
| 204 | help='Use multiple times to increase verbosity') |
| 205 | if self.enable_log_file: |
| 206 | self.group_logging.add_option( |
| 207 | '-l', '--log-file', |
| 208 | default=log_file, |
| 209 | help='The name of the file to store rotating log details') |
| 210 | self.group_logging.add_option( |
| 211 | '--no-log', action='store_const', const='', dest='log_file', |
| 212 | help='Disable log file') |
| 213 | |
| 214 | def parse_args(self, *args, **kwargs): |
| 215 | # Make sure this group is always the last one. |
| 216 | self.add_option_group(self.group_logging) |
| 217 | |
| 218 | options, args = optparse.OptionParser.parse_args(self, *args, **kwargs) |
| 219 | prepare_logging(self.enable_log_file and options.log_file, self.logger_root) |
| 220 | set_console_level( |
| 221 | LEVELS[min(len(LEVELS) - 1, options.verbose)], self.logger_root) |
| 222 | return options, args |
| 223 | |
| 224 | |
| 225 | class ArgumentParserWithLogging(argparse.ArgumentParser): |
| 226 | """Adds --verbose option.""" |
| 227 | |
| 228 | # Set to True to enable --log-file options. |
| 229 | enable_log_file = True |
| 230 | |
| 231 | def __init__(self, verbose=0, log_file=None, **kwargs): |
| 232 | kwargs.setdefault('description', sys.modules['__main__'].__doc__) |
| 233 | kwargs.setdefault('conflict_handler', 'resolve') |
| 234 | self.__verbose = verbose |
| 235 | self.__log_file = log_file |
| 236 | super(ArgumentParserWithLogging, self).__init__(**kwargs) |
| 237 | |
| 238 | def _add_logging_group(self): |
| 239 | group = self.add_argument_group('Logging') |
| 240 | group.add_argument( |
| 241 | '-v', '--verbose', |
| 242 | action='count', |
| 243 | default=self.__verbose, |
| 244 | help='Use multiple times to increase verbosity') |
| 245 | if self.enable_log_file: |
| 246 | group.add_argument( |
| 247 | '-l', '--log-file', |
| 248 | default=self.__log_file, |
| 249 | help='The name of the file to store rotating log details') |
| 250 | group.add_argument( |
| 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_logging_group() |
| 257 | |
| 258 | args = super(ArgumentParserWithLogging, self).parse_args(*args, **kwargs) |
| 259 | prepare_logging(self.enable_log_file and args.log_file, self.logger_root) |
| 260 | set_console_level( |
| 261 | LEVELS[min(len(LEVELS) - 1, args.verbose)], self.logger_root) |
| 262 | return args |