Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(64)

Side by Side Diff: client/utils/logging_utils.py

Issue 1233303003: Migrate and merge logging_utils from swarming_bot into client/utils. (Closed) Base URL: git@github.com:luci/luci-py.git@master
Patch Set: Changed formatter Created 5 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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
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
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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698