[Zconfig] SVN: ZConfig/trunk/ add support for reopening all log
files opened using the ZConfig FileHandler
Fred L. Drake, Jr.
fdrake at gmail.com
Wed Jun 20 15:12:32 EDT 2007
Log message for revision 76855:
add support for reopening all log files opened using the ZConfig FileHandler
Changed:
U ZConfig/trunk/ZConfig/components/logger/loghandler.py
U ZConfig/trunk/ZConfig/components/logger/tests/test_logger.py
U ZConfig/trunk/doc/zconfig.pdf
U ZConfig/trunk/doc/zconfig.tex
-=-
Modified: ZConfig/trunk/ZConfig/components/logger/loghandler.py
===================================================================
--- ZConfig/trunk/ZConfig/components/logger/loghandler.py 2007-06-20 18:45:27 UTC (rev 76854)
+++ ZConfig/trunk/ZConfig/components/logger/loghandler.py 2007-06-20 19:12:32 UTC (rev 76855)
@@ -15,6 +15,7 @@
import os
import sys
+import weakref
from logging import Handler, StreamHandler
from logging.handlers import SysLogHandler, BufferingHandler
@@ -22,6 +23,24 @@
from logging.handlers import NTEventLogHandler as Win32EventLogHandler
+_reopenable_handlers = []
+
+def reopenFiles():
+ """Reopen all logfiles managed by ZConfig configuration."""
+ for wr in _reopenable_handlers[:]:
+ h = wr()
+ if h is None:
+ try:
+ _reopenable_handlers.remove(wr)
+ except ValueError:
+ continue
+ else:
+ h.reopen()
+
+def _remove_from_reopenable(wr):
+ _reopenable_handlers.remove(wr)
+
+
class FileHandler(StreamHandler):
"""File handler which supports reopening of logs.
@@ -34,6 +53,8 @@
StreamHandler.__init__(self, open(filename, mode))
self.baseFilename = filename
self.mode = mode
+ self._wr = weakref.ref(self, _remove_from_reopenable)
+ _reopenable_handlers.append(self._wr)
def close(self):
self.stream.close()
Modified: ZConfig/trunk/ZConfig/components/logger/tests/test_logger.py
===================================================================
--- ZConfig/trunk/ZConfig/components/logger/tests/test_logger.py 2007-06-20 18:45:27 UTC (rev 76854)
+++ ZConfig/trunk/ZConfig/components/logger/tests/test_logger.py 2007-06-20 19:12:32 UTC (rev 76855)
@@ -45,22 +45,41 @@
# XXX This tries to save and restore the state of logging around
# the test. Somewhat surgical; there may be a better way.
- name = None
-
def setUp(self):
- self._old_logger = logging.getLogger(self.name)
+ self._created = []
+ self._old_logger = logging.getLogger()
self._old_level = self._old_logger.level
self._old_handlers = self._old_logger.handlers[:]
self._old_logger.handlers[:] = []
self._old_logger.setLevel(logging.WARN)
+ self._old_logger_dict = logging.root.manager.loggerDict.copy()
+ logging.root.manager.loggerDict.clear()
+
def tearDown(self):
+ logging.root.manager.loggerDict.clear()
+ logging.root.manager.loggerDict.update(self._old_logger_dict)
+
for h in self._old_logger.handlers:
self._old_logger.removeHandler(h)
for h in self._old_handlers:
self._old_logger.addHandler(h)
self._old_logger.setLevel(self._old_level)
+ while self._created:
+ os.unlink(self._created.pop())
+
+ def mktemp(self):
+ fd, fn = tempfile.mkstemp()
+ os.close(fd)
+ self._created.append(fn)
+ return fn
+
+ def move(self, fn):
+ nfn = self.mktemp()
+ os.rename(fn, nfn)
+ return nfn
+
_schema = None
def get_schema(self):
@@ -129,8 +148,7 @@
def test_with_logfile(self):
import os
- fd, fn = tempfile.mkstemp()
- os.close(fd)
+ fn = self.mktemp()
logger = self.check_simple_logger("<eventlog>\n"
" <logfile>\n"
" path %s\n"
@@ -140,21 +158,6 @@
logfile = logger.handlers[0]
self.assertEqual(logfile.level, logging.DEBUG)
self.assert_(isinstance(logfile, loghandler.FileHandler))
- # Test the move-and-reopen behavior:
- logger.propagate = False
- logger.error("message 1")
- os.rename(fn, fn + "-")
- logger.error("message 2")
- logfile.reopen()
- logger.error("message 3")
- logfile.close()
- text1 = open(fn + "-").read()
- text2 = open(fn).read()
- self.assert_("message 1" in text1)
- self.assert_("message 2" in text1)
- self.assert_("message 3" in text2)
- os.remove(fn)
- os.remove(fn + "-")
def test_with_stderr(self):
self.check_standard_stream("stderr")
@@ -277,8 +280,93 @@
return logger
+class TestReopeningLogfiles(LoggingTestBase):
+
+ # These tests should not be run on Windows.
+
+ _schematext = """
+ <schema>
+ <import package='ZConfig.components.logger'/>
+ <multisection type='logger' name='*' attribute='loggers'/>
+ </schema>
+ """
+
+ _sampleconfig_template = """
+ <logger>
+ name foo.bar
+ <logfile>
+ path %s
+ level debug
+ </logfile>
+ <logfile>
+ path %s
+ level info
+ </logfile>
+ </logger>
+
+ <logger>
+ name bar.foo
+ <logfile>
+ path %s
+ level info
+ </logfile>
+ </logger>
+ """
+
+ def test_filehandler_reopen(self):
+
+ def mkrecord(msg):
+ return logging.LogRecord(
+ "foo.bar", logging.ERROR, __file__, 42, msg, (), ())
+
+ fn = self.mktemp()
+ h = loghandler.FileHandler(fn)
+ h.handle(mkrecord("message 1"))
+ nfn = self.move(fn)
+ h.handle(mkrecord("message 2"))
+ h.reopen()
+ h.handle(mkrecord("message 3"))
+ h.close()
+
+ # Check that the messages are in the right files::
+ text1 = open(nfn).read()
+ text2 = open(fn).read()
+ self.assert_("message 1" in text1)
+ self.assert_("message 2" in text1)
+ self.assert_("message 3" in text2)
+
+ def test_logfile_reopening(self):
+ paths = self.mktemp(), self.mktemp(), self.mktemp()
+ text = self._sampleconfig_template % paths
+ conf = self.get_config(text)
+ assert len(conf.loggers) == 2
+ # Build the loggers from the configuration, and write to them:
+ conf.loggers[0]().info("message 1")
+ conf.loggers[1]().info("message 2")
+ npaths = [self.move(fn) for fn in paths]
+ #
+ # We expect this to re-open the original files, so we'll have
+ # six files instead of three.
+ #
+ loghandler.reopenFiles()
+ #
+ # Write to them again:
+ conf.loggers[0]().info("message 3")
+ conf.loggers[1]().info("message 4")
+ #
+ # We should not have all six files:
+ for fn in paths:
+ self.assert_(os.path.isfile(fn), "%r must exist" % fn)
+ for fn in npaths:
+ self.assert_(os.path.isfile(fn), "%r must exist" % fn)
+
+
def test_suite():
- return unittest.makeSuite(TestConfig)
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(TestConfig))
+ if os.name != "nt":
+ suite.addTest(unittest.makeSuite(TestReopeningLogfiles))
+ return suite
if __name__ == '__main__':
unittest.main(defaultTest="test_suite")
Modified: ZConfig/trunk/doc/zconfig.pdf
===================================================================
(Binary files differ)
Modified: ZConfig/trunk/doc/zconfig.tex
===================================================================
--- ZConfig/trunk/doc/zconfig.tex 2007-06-20 18:45:27 UTC (rev 76854)
+++ ZConfig/trunk/doc/zconfig.tex 2007-06-20 19:12:32 UTC (rev 76855)
@@ -1178,8 +1178,17 @@
method which may be called to close any log files and re-open them.
This is useful when using a \UNIX{} signal to effect log file
rotation: the signal handler can call this method, and not have to
-worry about what handlers have been registered for the logger.
+worry about what handlers have been registered for the logger. There
+is also a function in the
+\module{ZConfig.components.logger.loghandler} module that re-opens all
+open log files created using ZConfig configuraiton:
+\begin{funcdesc}{reopenFiles}{}
+ Closes and re-opens all the log files held open by handlers created
+ by the factories for \code{logfile} sections. This is intended to
+ help support log rotation for applications.
+\end{funcdesc}
+
Building an application that uses the logging components is fairly
straightforward. The schema needs to import the relevant components
and declare their use:
More information about the ZConfig
mailing list