Revision b6fa9a44
b/Makefile.am | ||
---|---|---|
504 | 504 |
test/ganeti.utils.filelock_unittest.py \ |
505 | 505 |
test/ganeti.utils.hash_unittest.py \ |
506 | 506 |
test/ganeti.utils.io_unittest.py \ |
507 |
test/ganeti.utils.log_unittest.py \ |
|
507 | 508 |
test/ganeti.utils.mlock_unittest.py \ |
508 | 509 |
test/ganeti.utils.nodesetup_unittest.py \ |
509 | 510 |
test/ganeti.utils.process_unittest.py \ |
b/lib/utils/log.py | ||
---|---|---|
28 | 28 |
from ganeti import constants |
29 | 29 |
|
30 | 30 |
|
31 |
class LogFileHandler(logging.FileHandler):
|
|
32 |
"""Log handler that doesn't fallback to stderr.
|
|
31 |
class _ReopenableLogHandler(logging.handlers.BaseRotatingHandler):
|
|
32 |
"""Log handler with ability to reopen log file on request.
|
|
33 | 33 |
|
34 |
When an error occurs while writing on the logfile, logging.FileHandler tries |
|
35 |
to log on stderr. This doesn't work in ganeti since stderr is redirected to |
|
36 |
the logfile. This class avoids failures reporting errors to /dev/console. |
|
34 |
In combination with a SIGHUP handler this class can reopen the log file on |
|
35 |
user request. |
|
37 | 36 |
|
38 | 37 |
""" |
39 |
def __init__(self, filename, mode="a", encoding=None):
|
|
40 |
"""Open the specified file and use it as the stream for logging.
|
|
38 |
def __init__(self, filename): |
|
39 |
"""Initializes this class.
|
|
41 | 40 |
|
42 |
Also open /dev/console to report errors while logging. |
|
41 |
@type filename: string |
|
42 |
@param filename: Path to logfile |
|
43 | 43 |
|
44 | 44 |
""" |
45 |
logging.FileHandler.__init__(self, filename, mode, encoding) |
|
46 |
self.console = open(constants.DEV_CONSOLE, "a") |
|
45 |
logging.handlers.BaseRotatingHandler.__init__(self, filename, "a") |
|
47 | 46 |
|
48 |
def handleError(self, record): # pylint: disable-msg=C0103
|
|
49 |
"""Handle errors which occur during an emit() call.
|
|
47 |
assert self.encoding is None, "Encoding not supported for logging"
|
|
48 |
assert not hasattr(self, "_reopen"), "Base class has '_reopen' attribute"
|
|
50 | 49 |
|
51 |
Try to handle errors with FileHandler method, if it fails write to |
|
50 |
self._reopen = False |
|
51 |
|
|
52 |
def shouldRollover(self, _): # pylint: disable-msg=C0103 |
|
53 |
"""Determine whether log file should be reopened. |
|
54 |
|
|
55 |
""" |
|
56 |
return self._reopen or not self.stream |
|
57 |
|
|
58 |
def doRollover(self): # pylint: disable-msg=C0103 |
|
59 |
"""Reopens the log file. |
|
60 |
|
|
61 |
""" |
|
62 |
if self.stream: |
|
63 |
self.stream.flush() |
|
64 |
self.stream.close() |
|
65 |
self.stream = None |
|
66 |
|
|
67 |
# Reopen file |
|
68 |
# TODO: Handle errors? |
|
69 |
self.stream = open(self.baseFilename, "a") |
|
70 |
|
|
71 |
def RequestReopen(self): |
|
72 |
"""Register a request to reopen the file. |
|
73 |
|
|
74 |
The file will be reopened before writing the next log record. |
|
75 |
|
|
76 |
""" |
|
77 |
self._reopen = True |
|
78 |
|
|
79 |
|
|
80 |
def _LogErrorsToConsole(base): |
|
81 |
"""Create wrapper class writing errors to console. |
|
82 |
|
|
83 |
This needs to be in a function for unittesting. |
|
84 |
|
|
85 |
""" |
|
86 |
class wrapped(base): # pylint: disable-msg=C0103 |
|
87 |
"""Log handler that doesn't fallback to stderr. |
|
88 |
|
|
89 |
When an error occurs while writing on the logfile, logging.FileHandler |
|
90 |
tries to log on stderr. This doesn't work in Ganeti since stderr is |
|
91 |
redirected to a logfile. This class avoids failures by reporting errors to |
|
52 | 92 |
/dev/console. |
53 | 93 |
|
54 | 94 |
""" |
55 |
try: |
|
56 |
logging.FileHandler.handleError(self, record) |
|
57 |
except Exception: # pylint: disable-msg=W0703 |
|
95 |
def __init__(self, console, *args, **kwargs): |
|
96 |
"""Initializes this class. |
|
97 |
|
|
98 |
@type console: file-like object or None |
|
99 |
@param console: Open file-like object for console |
|
100 |
|
|
101 |
""" |
|
102 |
base.__init__(self, *args, **kwargs) |
|
103 |
assert not hasattr(self, "_console") |
|
104 |
self._console = console |
|
105 |
|
|
106 |
def handleError(self, record): # pylint: disable-msg=C0103 |
|
107 |
"""Handle errors which occur during an emit() call. |
|
108 |
|
|
109 |
Try to handle errors with FileHandler method, if it fails write to |
|
110 |
/dev/console. |
|
111 |
|
|
112 |
""" |
|
58 | 113 |
try: |
59 |
self.console.write("Cannot log message:\n%s\n" % self.format(record))
|
|
114 |
base.handleError(record)
|
|
60 | 115 |
except Exception: # pylint: disable-msg=W0703 |
61 |
# Log handler tried everything it could, now just give up |
|
62 |
pass |
|
116 |
if self._console: |
|
117 |
try: |
|
118 |
# Ignore warning about "self.format", pylint: disable-msg=E1101 |
|
119 |
self._console.write("Cannot log message:\n%s\n" % |
|
120 |
self.format(record)) |
|
121 |
except Exception: # pylint: disable-msg=W0703 |
|
122 |
# Log handler tried everything it could, now just give up |
|
123 |
pass |
|
124 |
|
|
125 |
return wrapped |
|
126 |
|
|
127 |
|
|
128 |
#: Custom log handler for writing to console with a reopenable handler |
|
129 |
_LogHandler = _LogErrorsToConsole(_ReopenableLogHandler) |
|
63 | 130 |
|
64 | 131 |
|
65 | 132 |
def SetupLogging(logfile, debug=0, stderr_logging=False, program="", |
... | ... | |
138 | 205 |
# exception since otherwise we could run but without any logs at all |
139 | 206 |
try: |
140 | 207 |
if console_logging: |
141 |
logfile_handler = LogFileHandler(logfile)
|
|
208 |
logfile_handler = _LogHandler(open(constants.DEV_CONSOLE, "a"), logfile)
|
|
142 | 209 |
else: |
143 |
logfile_handler = logging.FileHandler(logfile) |
|
210 |
logfile_handler = _ReopenableLogHandler(logfile) |
|
211 |
|
|
144 | 212 |
logfile_handler.setFormatter(formatter) |
145 | 213 |
if debug: |
146 | 214 |
logfile_handler.setLevel(logging.DEBUG) |
b/test/ganeti.utils.log_unittest.py | ||
---|---|---|
1 |
#!/usr/bin/python |
|
2 |
# |
|
3 |
|
|
4 |
# Copyright (C) 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 |
|
|
22 |
"""Script for testing ganeti.utils.log""" |
|
23 |
|
|
24 |
import os |
|
25 |
import unittest |
|
26 |
import logging |
|
27 |
import tempfile |
|
28 |
|
|
29 |
from ganeti import constants |
|
30 |
from ganeti import errors |
|
31 |
from ganeti import utils |
|
32 |
|
|
33 |
import testutils |
|
34 |
|
|
35 |
|
|
36 |
class TestLogHandler(unittest.TestCase): |
|
37 |
def test(self): |
|
38 |
tmpfile = tempfile.NamedTemporaryFile() |
|
39 |
|
|
40 |
handler = utils.log._ReopenableLogHandler(tmpfile.name) |
|
41 |
handler.setFormatter(logging.Formatter("%(asctime)s: %(message)s")) |
|
42 |
|
|
43 |
logger = logging.Logger("TestLogger") |
|
44 |
logger.addHandler(handler) |
|
45 |
self.assertEqual(len(logger.handlers), 1) |
|
46 |
|
|
47 |
logger.error("Test message ERROR") |
|
48 |
logger.info("Test message INFO") |
|
49 |
|
|
50 |
logger.removeHandler(handler) |
|
51 |
self.assertFalse(logger.handlers) |
|
52 |
handler.close() |
|
53 |
|
|
54 |
self.assertEqual(len(utils.ReadFile(tmpfile.name).splitlines()), 2) |
|
55 |
|
|
56 |
def testReopen(self): |
|
57 |
tmpfile = tempfile.NamedTemporaryFile() |
|
58 |
tmpfile2 = tempfile.NamedTemporaryFile() |
|
59 |
|
|
60 |
handler = utils.log._ReopenableLogHandler(tmpfile.name) |
|
61 |
|
|
62 |
self.assertFalse(utils.ReadFile(tmpfile.name)) |
|
63 |
self.assertFalse(utils.ReadFile(tmpfile2.name)) |
|
64 |
|
|
65 |
logger = logging.Logger("TestLoggerReopen") |
|
66 |
logger.addHandler(handler) |
|
67 |
|
|
68 |
for _ in range(3): |
|
69 |
logger.error("Test message ERROR") |
|
70 |
handler.flush() |
|
71 |
self.assertEqual(len(utils.ReadFile(tmpfile.name).splitlines()), 3) |
|
72 |
before_id = utils.GetFileID(tmpfile.name) |
|
73 |
|
|
74 |
handler.RequestReopen() |
|
75 |
self.assertTrue(handler._reopen) |
|
76 |
self.assertTrue(utils.VerifyFileID(utils.GetFileID(tmpfile.name), |
|
77 |
before_id)) |
|
78 |
|
|
79 |
# Rename only after requesting reopen |
|
80 |
os.rename(tmpfile.name, tmpfile2.name) |
|
81 |
assert not os.path.exists(tmpfile.name) |
|
82 |
|
|
83 |
# Write another message, should reopen |
|
84 |
for _ in range(4): |
|
85 |
logger.info("Test message INFO") |
|
86 |
self.assertFalse(utils.VerifyFileID(utils.GetFileID(tmpfile.name), |
|
87 |
before_id)) |
|
88 |
|
|
89 |
logger.removeHandler(handler) |
|
90 |
self.assertFalse(logger.handlers) |
|
91 |
handler.close() |
|
92 |
|
|
93 |
self.assertEqual(len(utils.ReadFile(tmpfile.name).splitlines()), 4) |
|
94 |
self.assertEqual(len(utils.ReadFile(tmpfile2.name).splitlines()), 3) |
|
95 |
|
|
96 |
def testConsole(self): |
|
97 |
for (console, check) in [(None, False), |
|
98 |
(tempfile.NamedTemporaryFile(), True), |
|
99 |
(self._FailingFile(os.devnull), False)]: |
|
100 |
# Create a handler which will fail when handling errors |
|
101 |
cls = utils.log._LogErrorsToConsole(self._FailingHandler) |
|
102 |
|
|
103 |
# Instantiate handler with file which will fail when writing, |
|
104 |
# provoking a write to the console |
|
105 |
handler = cls(console, self._FailingFile(os.devnull)) |
|
106 |
|
|
107 |
logger = logging.Logger("TestLogger") |
|
108 |
logger.addHandler(handler) |
|
109 |
self.assertEqual(len(logger.handlers), 1) |
|
110 |
|
|
111 |
# Provoke write |
|
112 |
logger.error("Test message ERROR") |
|
113 |
|
|
114 |
# Take everything apart |
|
115 |
logger.removeHandler(handler) |
|
116 |
self.assertFalse(logger.handlers) |
|
117 |
handler.close() |
|
118 |
|
|
119 |
if console and check: |
|
120 |
console.flush() |
|
121 |
|
|
122 |
# Check console output |
|
123 |
consout = utils.ReadFile(console.name) |
|
124 |
self.assertTrue("Cannot log message" in consout) |
|
125 |
self.assertTrue("Test message ERROR" in consout) |
|
126 |
|
|
127 |
class _FailingFile(file): |
|
128 |
def write(self, _): |
|
129 |
raise Exception |
|
130 |
|
|
131 |
class _FailingHandler(logging.StreamHandler): |
|
132 |
def handleError(self, _): |
|
133 |
raise Exception |
|
134 |
|
|
135 |
|
|
136 |
if __name__ == "__main__": |
|
137 |
testutils.GanetiTestProgram() |
Also available in: Unified diff