Skip to content
Snippets Groups Projects
Commit b6fa9a44 authored by Michael Hanselmann's avatar Michael Hanselmann
Browse files

Introduce re-openable log record handler


This patch adds a new log handler class based on the standard library's
BaseRotatingHandler. This new class allows the log file to be re-opened,
e.g. upon receiving a SIGHUP signal. The latter will be implemented in
forthcoming patches. The patch does not change the behaviour regarding
writing to /dev/console.

Quite a bit of code had to be changed to unittest the log handlers.

Signed-off-by: default avatarMichael Hanselmann <hansmi@google.com>
Reviewed-by: default avatarIustin Pop <iustin@google.com>
parent c417e115
No related branches found
No related tags found
No related merge requests found
...@@ -504,6 +504,7 @@ python_tests = \ ...@@ -504,6 +504,7 @@ python_tests = \
test/ganeti.utils.filelock_unittest.py \ test/ganeti.utils.filelock_unittest.py \
test/ganeti.utils.hash_unittest.py \ test/ganeti.utils.hash_unittest.py \
test/ganeti.utils.io_unittest.py \ test/ganeti.utils.io_unittest.py \
test/ganeti.utils.log_unittest.py \
test/ganeti.utils.mlock_unittest.py \ test/ganeti.utils.mlock_unittest.py \
test/ganeti.utils.nodesetup_unittest.py \ test/ganeti.utils.nodesetup_unittest.py \
test/ganeti.utils.process_unittest.py \ test/ganeti.utils.process_unittest.py \
......
...@@ -28,38 +28,105 @@ import logging.handlers ...@@ -28,38 +28,105 @@ import logging.handlers
from ganeti import constants from ganeti import constants
class LogFileHandler(logging.FileHandler): class _ReopenableLogHandler(logging.handlers.BaseRotatingHandler):
"""Log handler that doesn't fallback to stderr. """Log handler with ability to reopen log file on request.
When an error occurs while writing on the logfile, logging.FileHandler tries In combination with a SIGHUP handler this class can reopen the log file on
to log on stderr. This doesn't work in ganeti since stderr is redirected to user request.
the logfile. This class avoids failures reporting errors to /dev/console.
""" """
def __init__(self, filename, mode="a", encoding=None): def __init__(self, filename):
"""Open the specified file and use it as the stream for logging. """Initializes this class.
Also open /dev/console to report errors while logging. @type filename: string
@param filename: Path to logfile
""" """
logging.FileHandler.__init__(self, filename, mode, encoding) logging.handlers.BaseRotatingHandler.__init__(self, filename, "a")
self.console = open(constants.DEV_CONSOLE, "a")
def handleError(self, record): # pylint: disable-msg=C0103 assert self.encoding is None, "Encoding not supported for logging"
"""Handle errors which occur during an emit() call. assert not hasattr(self, "_reopen"), "Base class has '_reopen' attribute"
Try to handle errors with FileHandler method, if it fails write to self._reopen = False
def shouldRollover(self, _): # pylint: disable-msg=C0103
"""Determine whether log file should be reopened.
"""
return self._reopen or not self.stream
def doRollover(self): # pylint: disable-msg=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")
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-msg=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. /dev/console.
""" """
try: def __init__(self, console, *args, **kwargs):
logging.FileHandler.handleError(self, record) """Initializes this class.
except Exception: # pylint: disable-msg=W0703
@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-msg=C0103
"""Handle errors which occur during an emit() call.
Try to handle errors with FileHandler method, if it fails write to
/dev/console.
"""
try: try:
self.console.write("Cannot log message:\n%s\n" % self.format(record)) base.handleError(record)
except Exception: # pylint: disable-msg=W0703 except Exception: # pylint: disable-msg=W0703
# Log handler tried everything it could, now just give up if self._console:
pass try:
# Ignore warning about "self.format", pylint: disable-msg=E1101
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
return wrapped
#: Custom log handler for writing to console with a reopenable handler
_LogHandler = _LogErrorsToConsole(_ReopenableLogHandler)
def SetupLogging(logfile, debug=0, stderr_logging=False, program="", def SetupLogging(logfile, debug=0, stderr_logging=False, program="",
...@@ -138,9 +205,10 @@ def SetupLogging(logfile, debug=0, stderr_logging=False, program="", ...@@ -138,9 +205,10 @@ def SetupLogging(logfile, debug=0, stderr_logging=False, program="",
# exception since otherwise we could run but without any logs at all # exception since otherwise we could run but without any logs at all
try: try:
if console_logging: if console_logging:
logfile_handler = LogFileHandler(logfile) logfile_handler = _LogHandler(open(constants.DEV_CONSOLE, "a"), logfile)
else: else:
logfile_handler = logging.FileHandler(logfile) logfile_handler = _ReopenableLogHandler(logfile)
logfile_handler.setFormatter(formatter) logfile_handler.setFormatter(formatter)
if debug: if debug:
logfile_handler.setLevel(logging.DEBUG) logfile_handler.setLevel(logging.DEBUG)
......
#!/usr/bin/python
#
# Copyright (C) 2011 Google Inc.
#
# 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.
#
# 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.
#
# 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.
"""Script for testing ganeti.utils.log"""
import os
import unittest
import logging
import tempfile
from ganeti import constants
from ganeti import errors
from ganeti import utils
import testutils
class TestLogHandler(unittest.TestCase):
def test(self):
tmpfile = tempfile.NamedTemporaryFile()
handler = utils.log._ReopenableLogHandler(tmpfile.name)
handler.setFormatter(logging.Formatter("%(asctime)s: %(message)s"))
logger = logging.Logger("TestLogger")
logger.addHandler(handler)
self.assertEqual(len(logger.handlers), 1)
logger.error("Test message ERROR")
logger.info("Test message INFO")
logger.removeHandler(handler)
self.assertFalse(logger.handlers)
handler.close()
self.assertEqual(len(utils.ReadFile(tmpfile.name).splitlines()), 2)
def testReopen(self):
tmpfile = tempfile.NamedTemporaryFile()
tmpfile2 = tempfile.NamedTemporaryFile()
handler = utils.log._ReopenableLogHandler(tmpfile.name)
self.assertFalse(utils.ReadFile(tmpfile.name))
self.assertFalse(utils.ReadFile(tmpfile2.name))
logger = logging.Logger("TestLoggerReopen")
logger.addHandler(handler)
for _ in range(3):
logger.error("Test message ERROR")
handler.flush()
self.assertEqual(len(utils.ReadFile(tmpfile.name).splitlines()), 3)
before_id = utils.GetFileID(tmpfile.name)
handler.RequestReopen()
self.assertTrue(handler._reopen)
self.assertTrue(utils.VerifyFileID(utils.GetFileID(tmpfile.name),
before_id))
# Rename only after requesting reopen
os.rename(tmpfile.name, tmpfile2.name)
assert not os.path.exists(tmpfile.name)
# Write another message, should reopen
for _ in range(4):
logger.info("Test message INFO")
self.assertFalse(utils.VerifyFileID(utils.GetFileID(tmpfile.name),
before_id))
logger.removeHandler(handler)
self.assertFalse(logger.handlers)
handler.close()
self.assertEqual(len(utils.ReadFile(tmpfile.name).splitlines()), 4)
self.assertEqual(len(utils.ReadFile(tmpfile2.name).splitlines()), 3)
def testConsole(self):
for (console, check) in [(None, False),
(tempfile.NamedTemporaryFile(), True),
(self._FailingFile(os.devnull), False)]:
# Create a handler which will fail when handling errors
cls = utils.log._LogErrorsToConsole(self._FailingHandler)
# Instantiate handler with file which will fail when writing,
# provoking a write to the console
handler = cls(console, self._FailingFile(os.devnull))
logger = logging.Logger("TestLogger")
logger.addHandler(handler)
self.assertEqual(len(logger.handlers), 1)
# Provoke write
logger.error("Test message ERROR")
# Take everything apart
logger.removeHandler(handler)
self.assertFalse(logger.handlers)
handler.close()
if console and check:
console.flush()
# Check console output
consout = utils.ReadFile(console.name)
self.assertTrue("Cannot log message" in consout)
self.assertTrue("Test message ERROR" in consout)
class _FailingFile(file):
def write(self, _):
raise Exception
class _FailingHandler(logging.StreamHandler):
def handleError(self, _):
raise Exception
if __name__ == "__main__":
testutils.GanetiTestProgram()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment