Version bump for 2.8.4 and NEWS update
[ganeti-local] / lib / utils / log.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2010, 2011 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21 """Utility functions for logging.
22
23 """
24
25 import os.path
26 import logging
27 import logging.handlers
28 from cStringIO import StringIO
29
30 from ganeti import constants
31 from ganeti import compat
32
33
34 class _ReopenableLogHandler(logging.handlers.BaseRotatingHandler):
35   """Log handler with ability to reopen log file on request.
36
37   In combination with a SIGHUP handler this class can reopen the log file on
38   user request.
39
40   """
41   def __init__(self, filename):
42     """Initializes this class.
43
44     @type filename: string
45     @param filename: Path to logfile
46
47     """
48     logging.handlers.BaseRotatingHandler.__init__(self, filename, "a")
49
50     assert self.encoding is None, "Encoding not supported for logging"
51     assert not hasattr(self, "_reopen"), "Base class has '_reopen' attribute"
52
53     self._reopen = False
54
55   def shouldRollover(self, _): # pylint: disable=C0103
56     """Determine whether log file should be reopened.
57
58     """
59     return self._reopen or not self.stream
60
61   def doRollover(self): # pylint: disable=C0103
62     """Reopens the log file.
63
64     """
65     if self.stream:
66       self.stream.flush()
67       self.stream.close()
68       self.stream = None
69
70     # Reopen file
71     # TODO: Handle errors?
72     self.stream = open(self.baseFilename, "a")
73
74     # Don't reopen on the next message
75     self._reopen = False
76
77   def RequestReopen(self):
78     """Register a request to reopen the file.
79
80     The file will be reopened before writing the next log record.
81
82     """
83     self._reopen = True
84
85
86 def _LogErrorsToConsole(base):
87   """Create wrapper class writing errors to console.
88
89   This needs to be in a function for unittesting.
90
91   """
92   class wrapped(base): # pylint: disable=C0103
93     """Log handler that doesn't fallback to stderr.
94
95     When an error occurs while writing on the logfile, logging.FileHandler
96     tries to log on stderr. This doesn't work in Ganeti since stderr is
97     redirected to a logfile. This class avoids failures by reporting errors to
98     /dev/console.
99
100     """
101     def __init__(self, console, *args, **kwargs):
102       """Initializes this class.
103
104       @type console: file-like object or None
105       @param console: Open file-like object for console
106
107       """
108       base.__init__(self, *args, **kwargs)
109       assert not hasattr(self, "_console")
110       self._console = console
111
112     def handleError(self, record): # pylint: disable=C0103
113       """Handle errors which occur during an emit() call.
114
115       Try to handle errors with FileHandler method, if it fails write to
116       /dev/console.
117
118       """
119       try:
120         base.handleError(record)
121       except Exception: # pylint: disable=W0703
122         if self._console:
123           try:
124             # Ignore warning about "self.format", pylint: disable=E1101
125             self._console.write("Cannot log message:\n%s\n" %
126                                 self.format(record))
127           except Exception: # pylint: disable=W0703
128             # Log handler tried everything it could, now just give up
129             pass
130
131   return wrapped
132
133
134 #: Custom log handler for writing to console with a reopenable handler
135 _LogHandler = _LogErrorsToConsole(_ReopenableLogHandler)
136
137
138 def _GetLogFormatter(program, multithreaded, debug, syslog):
139   """Build log formatter.
140
141   @param program: Program name
142   @param multithreaded: Whether to add thread name to log messages
143   @param debug: Whether to enable debug messages
144   @param syslog: Whether the formatter will be used for syslog
145
146   """
147   parts = []
148
149   if syslog:
150     parts.append(program + "[%(process)d]:")
151   else:
152     parts.append("%(asctime)s: " + program + " pid=%(process)d")
153
154   if multithreaded:
155     if syslog:
156       parts.append(" (%(threadName)s)")
157     else:
158       parts.append("/%(threadName)s")
159
160   # Add debug info for non-syslog loggers
161   if debug and not syslog:
162     parts.append(" %(module)s:%(lineno)s")
163
164   # Ses, we do want the textual level, as remote syslog will probably lose the
165   # error level, and it's easier to grep for it.
166   parts.append(" %(levelname)s %(message)s")
167
168   return logging.Formatter("".join(parts))
169
170
171 def _ReopenLogFiles(handlers):
172   """Wrapper for reopening all log handler's files in a sequence.
173
174   """
175   for handler in handlers:
176     handler.RequestReopen()
177   logging.info("Received request to reopen log files")
178
179
180 def SetupLogging(logfile, program, debug=0, stderr_logging=False,
181                  multithreaded=False, syslog=constants.SYSLOG_USAGE,
182                  console_logging=False, root_logger=None):
183   """Configures the logging module.
184
185   @type logfile: str
186   @param logfile: the filename to which we should log
187   @type program: str
188   @param program: the name under which we should log messages
189   @type debug: integer
190   @param debug: if greater than zero, enable debug messages, otherwise
191       only those at C{INFO} and above level
192   @type stderr_logging: boolean
193   @param stderr_logging: whether we should also log to the standard error
194   @type multithreaded: boolean
195   @param multithreaded: if True, will add the thread name to the log file
196   @type syslog: string
197   @param syslog: one of 'no', 'yes', 'only':
198       - if no, syslog is not used
199       - if yes, syslog is used (in addition to file-logging)
200       - if only, only syslog is used
201   @type console_logging: boolean
202   @param console_logging: if True, will use a FileHandler which falls back to
203       the system console if logging fails
204   @type root_logger: logging.Logger
205   @param root_logger: Root logger to use (for unittests)
206   @raise EnvironmentError: if we can't open the log file and
207       syslog/stderr logging is disabled
208   @rtype: callable
209   @return: Function reopening all open log files when called
210
211   """
212   progname = os.path.basename(program)
213
214   formatter = _GetLogFormatter(progname, multithreaded, debug, False)
215   syslog_fmt = _GetLogFormatter(progname, multithreaded, debug, True)
216
217   reopen_handlers = []
218
219   if root_logger is None:
220     root_logger = logging.getLogger("")
221   root_logger.setLevel(logging.NOTSET)
222
223   # Remove all previously setup handlers
224   for handler in root_logger.handlers:
225     handler.close()
226     root_logger.removeHandler(handler)
227
228   if stderr_logging:
229     stderr_handler = logging.StreamHandler()
230     stderr_handler.setFormatter(formatter)
231     if debug:
232       stderr_handler.setLevel(logging.NOTSET)
233     else:
234       stderr_handler.setLevel(logging.CRITICAL)
235     root_logger.addHandler(stderr_handler)
236
237   if syslog in (constants.SYSLOG_YES, constants.SYSLOG_ONLY):
238     facility = logging.handlers.SysLogHandler.LOG_DAEMON
239     syslog_handler = logging.handlers.SysLogHandler(constants.SYSLOG_SOCKET,
240                                                     facility)
241     syslog_handler.setFormatter(syslog_fmt)
242     # Never enable debug over syslog
243     syslog_handler.setLevel(logging.INFO)
244     root_logger.addHandler(syslog_handler)
245
246   if syslog != constants.SYSLOG_ONLY:
247     # this can fail, if the logging directories are not setup or we have
248     # a permisssion problem; in this case, it's best to log but ignore
249     # the error if stderr_logging is True, and if false we re-raise the
250     # exception since otherwise we could run but without any logs at all
251     try:
252       if console_logging:
253         logfile_handler = _LogHandler(open(constants.DEV_CONSOLE, "a"),
254                                       logfile)
255       else:
256         logfile_handler = _ReopenableLogHandler(logfile)
257
258       logfile_handler.setFormatter(formatter)
259       if debug:
260         logfile_handler.setLevel(logging.DEBUG)
261       else:
262         logfile_handler.setLevel(logging.INFO)
263       root_logger.addHandler(logfile_handler)
264       reopen_handlers.append(logfile_handler)
265     except EnvironmentError:
266       if stderr_logging or syslog == constants.SYSLOG_YES:
267         logging.exception("Failed to enable logging to file '%s'", logfile)
268       else:
269         # we need to re-raise the exception
270         raise
271
272   return compat.partial(_ReopenLogFiles, reopen_handlers)
273
274
275 def SetupToolLogging(debug, verbose, threadname=False,
276                      _root_logger=None, _stream=None):
277   """Configures the logging module for tools.
278
279   All log messages are sent to stderr.
280
281   @type debug: boolean
282   @param debug: Disable log message filtering
283   @type verbose: boolean
284   @param verbose: Enable verbose log messages
285   @type threadname: boolean
286   @param threadname: Whether to include thread name in output
287
288   """
289   if _root_logger is None:
290     root_logger = logging.getLogger("")
291   else:
292     root_logger = _root_logger
293
294   fmt = StringIO()
295   fmt.write("%(asctime)s:")
296
297   if threadname:
298     fmt.write(" %(threadName)s")
299
300   if debug or verbose:
301     fmt.write(" %(levelname)s")
302
303   fmt.write(" %(message)s")
304
305   formatter = logging.Formatter(fmt.getvalue())
306
307   stderr_handler = logging.StreamHandler(_stream)
308   stderr_handler.setFormatter(formatter)
309   if debug:
310     stderr_handler.setLevel(logging.NOTSET)
311   elif verbose:
312     stderr_handler.setLevel(logging.INFO)
313   else:
314     stderr_handler.setLevel(logging.WARNING)
315
316   root_logger.setLevel(logging.NOTSET)
317   root_logger.addHandler(stderr_handler)