blob: 903d993202ca5a0db051198753a997a0e755c955 [file] [log] [blame]
#
#
# Copyright (C) 2006, 2007, 2010, 2011 Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 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 _ReopenableLogHandler(logging.handlers.BaseRotatingHandler):
"""Log handler with ability to reopen log file on request.
In combination with a SIGHUP handler this class can reopen the log file on
user request.
"""
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
def doRollover(self): # pylint: disable=C0103
"""Reopens the log file.
"""
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.
This needs to be in a function for unittesting.
"""
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.
"""
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:
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")
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, 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 multithreaded: boolean
@param multithreaded: if True, will add the thread name to the log file
@type syslog: string
@param syslog: one of 'no', 'yes', 'only':
- if no, syslog is not used
- if yes, syslog is used (in addition to file-logging)
- if only, only syslog is used
@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
"""
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
for handler in root_logger.handlers:
handler.close()
root_logger.removeHandler(handler)
if stderr_logging:
stderr_handler = logging.StreamHandler()
stderr_handler.setFormatter(formatter)
if debug:
stderr_handler.setLevel(logging.NOTSET)
else:
stderr_handler.setLevel(logging.CRITICAL)
root_logger.addHandler(stderr_handler)
if syslog in (constants.SYSLOG_YES, constants.SYSLOG_ONLY):
facility = logging.handlers.SysLogHandler.LOG_DAEMON
syslog_handler = logging.handlers.SysLogHandler(constants.SYSLOG_SOCKET,
facility)
syslog_handler.setFormatter(syslog_fmt)
# Never enable debug over syslog
syslog_handler.setLevel(logging.INFO)
root_logger.addHandler(syslog_handler)
if syslog != constants.SYSLOG_ONLY and logfile:
# this can fail, if the logging directories are not setup or we have
# a permisssion problem; in this case, it's best to log but ignore
# the error if stderr_logging is True, and if false we re-raise the
# exception since otherwise we could run but without any logs at all
try:
if console_logging:
logfile_handler = _LogHandler(open(constants.DEV_CONSOLE, "a"),
logfile)
else:
logfile_handler = _ReopenableLogHandler(logfile)
logfile_handler.setFormatter(formatter)
if debug:
logfile_handler.setLevel(logging.DEBUG)
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, logfile=pathutils.LOG_TOOLS):
"""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
@type logfile: string
@param logfile: the path of the log file to use, use "None"
for tools which don't necessarily run on Ganeti nodes (and
thus don't have the Ganeti log directory).
"""
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(logfile,
program=toolname,
debug=debug_int,
multithreaded=threadname,
verbose=verbose)