Make SetupToolsLogging use tools logfile
[ganeti-github.git] / lib / utils / log.py
index 58fc8e4..c39e96a 100644 (file)
 #
 
 # Copyright (C) 2006, 2007, 2010, 2011 Google Inc.
+# All rights reserved.
 #
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
 #
-# This program is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-# General Public License for more details.
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
 #
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
-# 02110-1301, USA.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 """Utility functions for logging.
 
 """
 
+import os.path
 import logging
 import logging.handlers
 
 from ganeti import constants
+from ganeti import compat
+from ganeti import pathutils
 
 
-class LogFileHandler(logging.FileHandler):
-  """Log handler that doesn't fallback to stderr.
+class _ReopenableLogHandler(logging.handlers.BaseRotatingHandler):
+  """Log handler with ability to reopen log file on request.
 
-  When an error occurs while writing on the logfile, logging.FileHandler tries
-  to log on stderr. This doesn't work in ganeti since stderr is redirected to
-  the logfile. This class avoids failures reporting errors to /dev/console.
+  In combination with a SIGHUP handler this class can reopen the log file on
+  user request.
 
   """
-  def __init__(self, filename, mode="a", encoding=None):
-    """Open the specified file and use it as the stream for logging.
+  def __init__(self, filename):
+    """Initializes this class.
+
+    @type filename: string
+    @param filename: Path to logfile
+
+    """
+    logging.handlers.BaseRotatingHandler.__init__(self, filename, "a")
+
+    assert self.encoding is None, "Encoding not supported for logging"
+    assert not hasattr(self, "_reopen"), "Base class has '_reopen' attribute"
+
+    self._reopen = False
+
+  def shouldRollover(self, _): # pylint: disable=C0103
+    """Determine whether log file should be reopened.
+
+    """
+    return self._reopen or not self.stream
 
-    Also open /dev/console to report errors while logging.
+  def doRollover(self): # pylint: disable=C0103
+    """Reopens the log file.
 
     """
-    logging.FileHandler.__init__(self, filename, mode, encoding)
-    self.console = open(constants.DEV_CONSOLE, "a")
+    if self.stream:
+      self.stream.flush()
+      self.stream.close()
+      self.stream = None
+
+    # Reopen file
+    # TODO: Handle errors?
+    self.stream = open(self.baseFilename, "a")
+
+    # Don't reopen on the next message
+    self._reopen = False
+
+  def RequestReopen(self):
+    """Register a request to reopen the file.
+
+    The file will be reopened before writing the next log record.
+
+    """
+    self._reopen = True
+
+
+def _LogErrorsToConsole(base):
+  """Create wrapper class writing errors to console.
 
-  def handleError(self, record): # pylint: disable-msg=C0103
-    """Handle errors which occur during an emit() call.
+  This needs to be in a function for unittesting.
 
-    Try to handle errors with FileHandler method, if it fails write to
+  """
+  class wrapped(base): # pylint: disable=C0103
+    """Log handler that doesn't fallback to stderr.
+
+    When an error occurs while writing on the logfile, logging.FileHandler
+    tries to log on stderr. This doesn't work in Ganeti since stderr is
+    redirected to a logfile. This class avoids failures by reporting errors to
     /dev/console.
 
     """
-    try:
-      logging.FileHandler.handleError(self, record)
-    except Exception: # pylint: disable-msg=W0703
+    def __init__(self, console, *args, **kwargs):
+      """Initializes this class.
+
+      @type console: file-like object or None
+      @param console: Open file-like object for console
+
+      """
+      base.__init__(self, *args, **kwargs)
+      assert not hasattr(self, "_console")
+      self._console = console
+
+    def handleError(self, record): # pylint: disable=C0103
+      """Handle errors which occur during an emit() call.
+
+      Try to handle errors with FileHandler method, if it fails write to
+      /dev/console.
+
+      """
       try:
-        self.console.write("Cannot log message:\n%s\n" % self.format(record))
-      except Exception: # pylint: disable-msg=W0703
-        # Log handler tried everything it could, now just give up
-        pass
+        base.handleError(record)
+      except Exception: # pylint: disable=W0703
+        if self._console:
+          try:
+            # Ignore warning about "self.format", pylint: disable=E1101
+            self._console.write("Cannot log message:\n%s\n" %
+                                self.format(record))
+          except Exception: # pylint: disable=W0703
+            # Log handler tried everything it could, now just give up
+            pass
+
+  return wrapped
+
+
+#: Custom log handler for writing to console with a reopenable handler
+_LogHandler = _LogErrorsToConsole(_ReopenableLogHandler)
+
+
+def _GetLogFormatter(program, multithreaded, debug, syslog):
+  """Build log formatter.
+
+  @param program: Program name
+  @param multithreaded: Whether to add thread name to log messages
+  @param debug: Whether to enable debug messages
+  @param syslog: Whether the formatter will be used for syslog
+
+  """
+  parts = []
 
+  if syslog:
+    parts.append(program + "[%(process)d]:")
+  else:
+    parts.append("%(asctime)s: " + program + " pid=%(process)d")
 
-def SetupLogging(logfile, debug=0, stderr_logging=False, program="",
+  if multithreaded:
+    if syslog:
+      parts.append(" (%(threadName)s)")
+    else:
+      parts.append("/%(threadName)s")
+
+  # Add debug info for non-syslog loggers
+  if debug and not syslog:
+    parts.append(" %(module)s:%(lineno)s")
+
+  # Ses, we do want the textual level, as remote syslog will probably lose the
+  # error level, and it's easier to grep for it.
+  parts.append(" %(levelname)s %(message)s")
+
+  return logging.Formatter("".join(parts))
+
+
+def _ReopenLogFiles(handlers):
+  """Wrapper for reopening all log handler's files in a sequence.
+
+  """
+  for handler in handlers:
+    handler.RequestReopen()
+  logging.info("Received request to reopen log files")
+
+
+def SetupLogging(logfile, program, debug=0, stderr_logging=False,
                  multithreaded=False, syslog=constants.SYSLOG_USAGE,
-                 console_logging=False):
+                 console_logging=False, root_logger=None,
+                 verbose=True):
   """Configures the logging module.
 
   @type logfile: str
   @param logfile: the filename to which we should log
+  @type program: str
+  @param program: the name under which we should log messages
   @type debug: integer
   @param debug: if greater than zero, enable debug messages, otherwise
       only those at C{INFO} and above level
   @type stderr_logging: boolean
   @param stderr_logging: whether we should also log to the standard error
-  @type program: str
-  @param program: the name under which we should log messages
   @type multithreaded: boolean
   @param multithreaded: if True, will add the thread name to the log file
   @type syslog: string
@@ -86,26 +211,25 @@ def SetupLogging(logfile, debug=0, stderr_logging=False, program="",
   @type console_logging: boolean
   @param console_logging: if True, will use a FileHandler which falls back to
       the system console if logging fails
+  @type root_logger: logging.Logger
+  @param root_logger: Root logger to use (for unittests)
+  @type verbose: boolean
+  @param verbose: whether to log at 'info' level already (logfile logging only)
   @raise EnvironmentError: if we can't open the log file and
       syslog/stderr logging is disabled
+  @rtype: callable
+  @return: Function reopening all open log files when called
 
   """
-  fmt = "%(asctime)s: " + program + " pid=%(process)d"
-  sft = program + "[%(process)d]:"
-  if multithreaded:
-    fmt += "/%(threadName)s"
-    sft += " (%(threadName)s)"
-  if debug:
-    fmt += " %(module)s:%(lineno)s"
-    # no debug info for syslog loggers
-  fmt += " %(levelname)s %(message)s"
-  # yes, we do want the textual level, as remote syslog will probably
-  # lose the error level, and it's easier to grep for it
-  sft += " %(levelname)s %(message)s"
-  formatter = logging.Formatter(fmt)
-  sys_fmt = logging.Formatter(sft)
-
-  root_logger = logging.getLogger("")
+  progname = os.path.basename(program)
+
+  formatter = _GetLogFormatter(progname, multithreaded, debug, False)
+  syslog_fmt = _GetLogFormatter(progname, multithreaded, debug, True)
+
+  reopen_handlers = []
+
+  if root_logger is None:
+    root_logger = logging.getLogger("")
   root_logger.setLevel(logging.NOTSET)
 
   # Remove all previously setup handlers
@@ -126,7 +250,7 @@ def SetupLogging(logfile, debug=0, stderr_logging=False, program="",
     facility = logging.handlers.SysLogHandler.LOG_DAEMON
     syslog_handler = logging.handlers.SysLogHandler(constants.SYSLOG_SOCKET,
                                                     facility)
-    syslog_handler.setFormatter(sys_fmt)
+    syslog_handler.setFormatter(syslog_fmt)
     # Never enable debug over syslog
     syslog_handler.setLevel(logging.INFO)
     root_logger.addHandler(syslog_handler)
@@ -138,18 +262,58 @@ def SetupLogging(logfile, debug=0, stderr_logging=False, program="",
     # exception since otherwise we could run but without any logs at all
     try:
       if console_logging:
-        logfile_handler = LogFileHandler(logfile)
+        logfile_handler = _LogHandler(open(constants.DEV_CONSOLE, "a"),
+                                      logfile)
       else:
-        logfile_handler = logging.FileHandler(logfile)
+        logfile_handler = _ReopenableLogHandler(logfile)
+
       logfile_handler.setFormatter(formatter)
       if debug:
         logfile_handler.setLevel(logging.DEBUG)
-      else:
+      elif verbose:
         logfile_handler.setLevel(logging.INFO)
+      else:
+        logfile_handler.setLevel(logging.WARN)
       root_logger.addHandler(logfile_handler)
+      reopen_handlers.append(logfile_handler)
     except EnvironmentError:
       if stderr_logging or syslog == constants.SYSLOG_YES:
         logging.exception("Failed to enable logging to file '%s'", logfile)
       else:
         # we need to re-raise the exception
         raise
+
+  return compat.partial(_ReopenLogFiles, reopen_handlers)
+
+
+def SetupToolLogging(debug, verbose, threadname=False,
+                     toolname=None):
+  """Configures the logging module for tools.
+
+  All log messages are sent to the tools.log logfile.
+
+  @type toolname: string
+  @param toolname: name of the tool that's logging
+  @type debug: boolean
+  @param debug: Disable log message filtering
+  @type verbose: boolean
+  @param verbose: Enable verbose log messages
+  @type threadname: boolean
+  @param threadname: Whether to include thread name in output
+
+  """
+  if not toolname:
+    toolname = "unspecified_tool"
+
+  # 'SetupLogging' takes a quite unintuitive 'debug' option that
+  # is '0' for 'log higher than debug level' and '1' for
+  # 'log at NOSET' level. Hence this conversion.
+  debug_int = 0
+  if debug:
+    debug_int = 1
+
+  SetupLogging(pathutils.LOG_TOOLS,
+               program=toolname,
+               debug=debug_int,
+               multithreaded=threadname,
+               verbose=verbose)