OLD | NEW |
1 # Copyright 2014 The Swarming Authors. All rights reserved. | 1 # Copyright 2015 The Swarming Authors. All rights reserved. |
2 # Use of this source code is governed by the Apache v2.0 license that can be | 2 # Use of this source code is governed under the Apache License, Version 2.0 that |
3 # found in the LICENSE file. | 3 # can be found in the LICENSE file. |
4 | 4 |
5 """Utility relating to logging. | 5 """Utility relating to logging.""" |
6 | 6 |
7 TODO(maruel): Merge buffering and output related code from client/utils/tools.py | 7 import argparse |
8 in a single file. | |
9 """ | |
10 | |
11 import codecs | 8 import codecs |
12 import logging | 9 import logging |
13 import logging.handlers | 10 import logging.handlers |
| 11 import optparse |
14 import os | 12 import os |
15 import sys | 13 import sys |
16 import tempfile | 14 import tempfile |
17 import time | 15 import time |
18 | 16 |
19 | 17 |
20 # This works around file locking issue on Windows specifically in the case of | 18 # This works around file locking issue on Windows specifically in the case of |
21 # long lived child processes. | 19 # long lived child processes. |
22 # | 20 # |
23 # Python opens files with inheritable handle and without file sharing by | 21 # Python opens files with inheritable handle and without file sharing by |
(...skipping 27 matching lines...) Expand all Loading... |
51 return os.fdopen(new_fd, self.mode) | 49 return os.fdopen(new_fd, self.mode) |
52 return codecs.open(self.baseFilename, self.mode, self.encoding) | 50 return codecs.open(self.baseFilename, self.mode, self.encoding) |
53 | 51 |
54 | 52 |
55 else: # Not Windows. | 53 else: # Not Windows. |
56 | 54 |
57 | 55 |
58 NoInheritRotatingFileHandler = logging.handlers.RotatingFileHandler | 56 NoInheritRotatingFileHandler = logging.handlers.RotatingFileHandler |
59 | 57 |
60 | 58 |
| 59 # Levels used for logging. |
| 60 LEVELS = [logging.ERROR, logging.INFO, logging.DEBUG] |
| 61 |
| 62 |
61 class CaptureLogs(object): | 63 class CaptureLogs(object): |
62 """Captures all the logs in a context.""" | 64 """Captures all the logs in a context.""" |
63 def __init__(self, prefix, root=None): | 65 def __init__(self, prefix, root=None): |
64 handle, self._path = tempfile.mkstemp(prefix=prefix, suffix='.log') | 66 handle, self._path = tempfile.mkstemp(prefix=prefix, suffix='.log') |
65 os.close(handle) | 67 os.close(handle) |
66 self._handler = logging.FileHandler(self._path, 'w') | 68 self._handler = logging.FileHandler(self._path, 'w') |
67 self._handler.setLevel(logging.DEBUG) | 69 self._handler.setLevel(logging.DEBUG) |
68 formatter = UTCFormatter( | 70 formatter = UTCFormatter( |
69 '%(process)d %(asctime)s: %(levelname)-5s %(message)s') | 71 '%(process)d %(asctime)s: %(levelname)-5s %(message)s') |
70 self._handler.setFormatter(formatter) | 72 self._handler.setFormatter(formatter) |
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
114 def formatTime(self, record, datefmt=None): | 116 def formatTime(self, record, datefmt=None): |
115 """Change is ',' to '.'.""" | 117 """Change is ',' to '.'.""" |
116 ct = self.converter(record.created) | 118 ct = self.converter(record.created) |
117 if datefmt: | 119 if datefmt: |
118 return time.strftime(datefmt, ct) | 120 return time.strftime(datefmt, ct) |
119 else: | 121 else: |
120 t = time.strftime("%Y-%m-%d %H:%M:%S", ct) | 122 t = time.strftime("%Y-%m-%d %H:%M:%S", ct) |
121 return "%s.%03d" % (t, record.msecs) | 123 return "%s.%03d" % (t, record.msecs) |
122 | 124 |
123 | 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 |
124 def find_stderr(root=None): | 138 def find_stderr(root=None): |
125 """Returns the logging.handler streaming to stderr, if any.""" | 139 """Returns the logging.handler streaming to stderr, if any.""" |
126 for log in (root or logging.getLogger()).handlers: | 140 for log in (root or logging.getLogger()).handlers: |
127 if getattr(log, 'stream', None) is sys.stderr: | 141 if getattr(log, 'stream', None) is sys.stderr: |
128 return log | 142 return log |
129 | 143 |
130 | 144 |
131 def prepare_logging(filename, root=None): | 145 def prepare_logging(filename, root=None): |
132 """Prepare logging for scripts. | 146 """Prepare logging for scripts. |
133 | 147 |
134 Makes it log in UTC all the time. Prepare a rotating file based log. | 148 Makes it log in UTC all the time. Prepare a rotating file based log. |
135 """ | 149 """ |
136 assert not find_stderr(root) | 150 assert not find_stderr(root) |
137 formatter = UTCFormatter( | 151 formatter = UTCFormatter('%(process)d %(asctime)s %(severity)s: %(message)s') |
138 '%(process)d %(asctime)s: %(levelname)-5s %(message)s') | |
139 | 152 |
140 # It is a requirement that the root logger is set to DEBUG, so the messages | 153 # It is a requirement that the root logger is set to DEBUG, so the messages |
141 # are not lost. It defaults to WARNING otherwise. | 154 # are not lost. It defaults to WARNING otherwise. |
142 logger = root or logging.getLogger() | 155 logger = root or logging.getLogger() |
143 logger.setLevel(logging.DEBUG) | 156 logger.setLevel(logging.DEBUG) |
144 | 157 |
145 stderr = logging.StreamHandler() | 158 stderr = logging.StreamHandler() |
146 stderr.setFormatter(formatter) | 159 stderr.setFormatter(formatter) |
| 160 stderr.addFilter(Filter()) |
147 # Default to ERROR. | 161 # Default to ERROR. |
148 stderr.setLevel(logging.ERROR) | 162 stderr.setLevel(logging.ERROR) |
149 logger.addHandler(stderr) | 163 logger.addHandler(stderr) |
150 | 164 |
151 # Setup up logging to a constant file so we can debug issues where | 165 # Setup up logging to a constant file so we can debug issues where |
152 # the results aren't properly sent to the result URL. | 166 # the results aren't properly sent to the result URL. |
153 if filename: | 167 if filename: |
154 try: | 168 try: |
155 rotating_file = NoInheritRotatingFileHandler( | 169 rotating_file = NoInheritRotatingFileHandler( |
156 filename, maxBytes=10 * 1024 * 1024, backupCount=5) | 170 filename, maxBytes=10 * 1024 * 1024, backupCount=5, |
| 171 encoding='utf-8') |
157 rotating_file.setLevel(logging.DEBUG) | 172 rotating_file.setLevel(logging.DEBUG) |
158 rotating_file.setFormatter(formatter) | 173 rotating_file.setFormatter(formatter) |
| 174 rotating_file.addFilter(Filter()) |
159 logger.addHandler(rotating_file) | 175 logger.addHandler(rotating_file) |
160 except Exception: | 176 except Exception: |
161 # May happen on cygwin. Do not crash. | 177 # May happen on cygwin. Do not crash. |
162 logging.exception('Failed to open %s', filename) | 178 logging.exception('Failed to open %s', filename) |
163 | 179 |
164 | 180 |
165 def set_console_level(level, root=None): | 181 def set_console_level(level, root=None): |
166 """Reset the console (stderr) logging level.""" | 182 """Reset the console (stderr) logging level.""" |
167 handler = find_stderr(root) | 183 handler = find_stderr(root) |
168 handler.setLevel(level) | 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 |
OLD | NEW |