Commit b91e1b86 authored by Vinay Sajip's avatar Vinay Sajip

Refactored logging rotating handlers for improved flexibility.

parent a184ece0
...@@ -1102,3 +1102,31 @@ This dictionary is passed to :func:`~logging.config.dictConfig` to put the confi ...@@ -1102,3 +1102,31 @@ This dictionary is passed to :func:`~logging.config.dictConfig` to put the confi
For more information about this configuration, you can see the `relevant For more information about this configuration, you can see the `relevant
section <https://docs.djangoproject.com/en/1.3/topics/logging/#configuring-logging>`_ section <https://docs.djangoproject.com/en/1.3/topics/logging/#configuring-logging>`_
of the Django documentation. of the Django documentation.
.. _cookbook-rotator-namer:
Using a rotator and namer to customise log rotation processing
--------------------------------------------------------------
An example of how you can define a namer and rotator is given in the following
snippet, which shows zlib-based compression of the log file::
def namer(name):
return name + ".gz"
def rotator(source, dest):
with open(source, "rb") as sf:
data = sf.read()
compressed = zlib.compress(data, 9)
with open(dest, "wb") as df:
df.write(compressed)
os.remove(source)
rh = logging.handlers.RotatingFileHandler(...)
rh.rotator = rotator
rh.namer = namer
These are not “true” .gz files, as they are bare compressed data, with no
“container” such as you’d find in an actual gzip file. This snippet is just
for illustration purposes.
...@@ -164,6 +164,87 @@ this value. ...@@ -164,6 +164,87 @@ this value.
changed. If it has, the existing stream is flushed and closed and the changed. If it has, the existing stream is flushed and closed and the
file opened again, before outputting the record to the file. file opened again, before outputting the record to the file.
.. _base-rotating-handler:
BaseRotatingHandler
^^^^^^^^^^^^^^^^^^^
The :class:`BaseRotatingHandler` class, located in the :mod:`logging.handlers`
module, is the base class for the rotating file handlers,
:class:`RotatingFileHandler` and :class:`TimedRotatingFileHandler`. You should
not need to instantiate this class, but it has attributes and methods you may
need to override.
.. class:: BaseRotatingHandler(filename, mode, encoding=None, delay=False)
The parameters are as for :class:`FileHandler`. The attributes are:
.. attribute:: namer
If this attribute is set to a callable, the :meth:`rotation_filename`
method delegates to this callable. The parameters passed to the callable
are those passed to :meth:`rotation_filename`.
.. note:: The namer function is called quite a few times during rollover,
so it should be as simple and as fast as possible. It should also
return the same output every time for a given input, otherwise the
rollover behaviour may not work as expected.
.. versionadded:: 3.3
.. attribute:: BaseRotatingHandler.rotator
If this attribute is set to a callable, the :meth:`rotate` method
delegates to this callable. The parameters passed to the callable are
those passed to :meth:`rotate`.
.. versionadded:: 3.3
.. method:: BaseRotatingHandler.rotation_filename(default_name)
Modify the filename of a log file when rotating.
This is provided so that a custom filename can be provided.
The default implementation calls the 'namer' attribute of the handler,
if it's callable, passing the default name to it. If the attribute isn't
callable (the default is `None`), the name is returned unchanged.
:param default_name: The default name for the log file.
.. versionadded:: 3.3
.. method:: BaseRotatingHandler.rotate(source, dest)
When rotating, rotate the current log.
The default implementation calls the 'rotator' attribute of the handler,
if it's callable, passing the source and dest arguments to it. If the
attribute isn't callable (the default is `None`), the source is simply
renamed to the destination.
:param source: The source filename. This is normally the base
filename, e.g. 'test.log'
:param dest: The destination filename. This is normally
what the source is rotated to, e.g. 'test.log.1'.
.. versionadded:: 3.3
The reason the attributes exist is to save you having to subclass - you can use
the same callables for instances of :class:`RotatingFileHandler` and
:class:`TimedRotatingFileHandler`. If either the namer or rotator callable
raises an exception, this will be handled in the same way as any other
exception during an :meth:`emit` call, i.e. via the :meth:`handleError` method
of the handler.
If you need to make more significant changes to rotation processing, you can
override the methods.
For an example, see :ref:`cookbook-rotator-namer`.
.. _rotating-file-handler: .. _rotating-file-handler:
RotatingFileHandler RotatingFileHandler
......
...@@ -52,13 +52,15 @@ class BaseRotatingHandler(logging.FileHandler): ...@@ -52,13 +52,15 @@ class BaseRotatingHandler(logging.FileHandler):
Not meant to be instantiated directly. Instead, use RotatingFileHandler Not meant to be instantiated directly. Instead, use RotatingFileHandler
or TimedRotatingFileHandler. or TimedRotatingFileHandler.
""" """
def __init__(self, filename, mode, encoding=None, delay=0): def __init__(self, filename, mode, encoding=None, delay=False):
""" """
Use the specified filename for streamed logging Use the specified filename for streamed logging
""" """
logging.FileHandler.__init__(self, filename, mode, encoding, delay) logging.FileHandler.__init__(self, filename, mode, encoding, delay)
self.mode = mode self.mode = mode
self.encoding = encoding self.encoding = encoding
self.namer = None
self.rotator = None
def emit(self, record): def emit(self, record):
""" """
...@@ -76,12 +78,50 @@ class BaseRotatingHandler(logging.FileHandler): ...@@ -76,12 +78,50 @@ class BaseRotatingHandler(logging.FileHandler):
except: except:
self.handleError(record) self.handleError(record)
def rotation_filename(self, default_name):
"""
Modify the filename of a log file when rotating.
This is provided so that a custom filename can be provided.
The default implementation calls the 'namer' attribute of the
handler, if it's callable, passing the default name to
it. If the attribute isn't callable (the default is None), the name
is returned unchanged.
:param default_name: The default name for the log file.
"""
if not callable(self.namer):
result = default_name
else:
result = self.namer(default_name)
return result
def rotate(self, source, dest):
"""
When rotating, rotate the current log.
The default implementation calls the 'rotator' attribute of the
handler, if it's callable, passing the source and dest arguments to
it. If the attribute isn't callable (the default is None), the source
is simply renamed to the destination.
:param source: The source filename. This is normally the base
filename, e.g. 'test.log'
:param dest: The destination filename. This is normally
what the source is rotated to, e.g. 'test.log.1'.
"""
if not callable(self.rotator):
os.rename(source, dest)
else:
self.rotator(source, dest)
class RotatingFileHandler(BaseRotatingHandler): class RotatingFileHandler(BaseRotatingHandler):
""" """
Handler for logging to a set of files, which switches from one file Handler for logging to a set of files, which switches from one file
to the next when the current file reaches a certain size. to the next when the current file reaches a certain size.
""" """
def __init__(self, filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=0): def __init__(self, filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=False):
""" """
Open the specified file and use it as the stream for logging. Open the specified file and use it as the stream for logging.
...@@ -122,16 +162,17 @@ class RotatingFileHandler(BaseRotatingHandler): ...@@ -122,16 +162,17 @@ class RotatingFileHandler(BaseRotatingHandler):
self.stream = None self.stream = None
if self.backupCount > 0: if self.backupCount > 0:
for i in range(self.backupCount - 1, 0, -1): for i in range(self.backupCount - 1, 0, -1):
sfn = "%s.%d" % (self.baseFilename, i) sfn = self.rotation_filename("%s.%d" % (self.baseFilename, i))
dfn = "%s.%d" % (self.baseFilename, i + 1) dfn = self.rotation_filename("%s.%d" % (self.baseFilename,
i + 1))
if os.path.exists(sfn): if os.path.exists(sfn):
if os.path.exists(dfn): if os.path.exists(dfn):
os.remove(dfn) os.remove(dfn)
os.rename(sfn, dfn) os.rename(sfn, dfn)
dfn = self.baseFilename + ".1" dfn = self.rotation_filename(self.baseFilename + ".1")
if os.path.exists(dfn): if os.path.exists(dfn):
os.remove(dfn) os.remove(dfn)
os.rename(self.baseFilename, dfn) self.rotate(self.baseFilename, dfn)
self.mode = 'w' self.mode = 'w'
self.stream = self._open() self.stream = self._open()
...@@ -179,19 +220,19 @@ class TimedRotatingFileHandler(BaseRotatingHandler): ...@@ -179,19 +220,19 @@ class TimedRotatingFileHandler(BaseRotatingHandler):
if self.when == 'S': if self.when == 'S':
self.interval = 1 # one second self.interval = 1 # one second
self.suffix = "%Y-%m-%d_%H-%M-%S" self.suffix = "%Y-%m-%d_%H-%M-%S"
self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$" self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(\.\w+)?$"
elif self.when == 'M': elif self.when == 'M':
self.interval = 60 # one minute self.interval = 60 # one minute
self.suffix = "%Y-%m-%d_%H-%M" self.suffix = "%Y-%m-%d_%H-%M"
self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}$" self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}(\.\w+)?$"
elif self.when == 'H': elif self.when == 'H':
self.interval = 60 * 60 # one hour self.interval = 60 * 60 # one hour
self.suffix = "%Y-%m-%d_%H" self.suffix = "%Y-%m-%d_%H"
self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}$" self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}(\.\w+)?$"
elif self.when == 'D' or self.when == 'MIDNIGHT': elif self.when == 'D' or self.when == 'MIDNIGHT':
self.interval = 60 * 60 * 24 # one day self.interval = 60 * 60 * 24 # one day
self.suffix = "%Y-%m-%d" self.suffix = "%Y-%m-%d"
self.extMatch = r"^\d{4}-\d{2}-\d{2}$" self.extMatch = r"^\d{4}-\d{2}-\d{2}(\.\w+)?$"
elif self.when.startswith('W'): elif self.when.startswith('W'):
self.interval = 60 * 60 * 24 * 7 # one week self.interval = 60 * 60 * 24 * 7 # one week
if len(self.when) != 2: if len(self.when) != 2:
...@@ -200,7 +241,7 @@ class TimedRotatingFileHandler(BaseRotatingHandler): ...@@ -200,7 +241,7 @@ class TimedRotatingFileHandler(BaseRotatingHandler):
raise ValueError("Invalid day specified for weekly rollover: %s" % self.when) raise ValueError("Invalid day specified for weekly rollover: %s" % self.when)
self.dayOfWeek = int(self.when[1]) self.dayOfWeek = int(self.when[1])
self.suffix = "%Y-%m-%d" self.suffix = "%Y-%m-%d"
self.extMatch = r"^\d{4}-\d{2}-\d{2}$" self.extMatch = r"^\d{4}-\d{2}-\d{2}(\.\w+)?$"
else: else:
raise ValueError("Invalid rollover interval specified: %s" % self.when) raise ValueError("Invalid rollover interval specified: %s" % self.when)
...@@ -323,10 +364,11 @@ class TimedRotatingFileHandler(BaseRotatingHandler): ...@@ -323,10 +364,11 @@ class TimedRotatingFileHandler(BaseRotatingHandler):
timeTuple = time.gmtime(t) timeTuple = time.gmtime(t)
else: else:
timeTuple = time.localtime(t) timeTuple = time.localtime(t)
dfn = self.baseFilename + "." + time.strftime(self.suffix, timeTuple) dfn = self.rotation_filename(self.baseFilename + "." +
time.strftime(self.suffix, timeTuple))
if os.path.exists(dfn): if os.path.exists(dfn):
os.remove(dfn) os.remove(dfn)
os.rename(self.baseFilename, dfn) self.rotate(self.baseFilename, dfn)
if self.backupCount > 0: if self.backupCount > 0:
for s in self.getFilesToDelete(): for s in self.getFilesToDelete():
os.remove(s) os.remove(s)
...@@ -367,7 +409,7 @@ class WatchedFileHandler(logging.FileHandler): ...@@ -367,7 +409,7 @@ class WatchedFileHandler(logging.FileHandler):
This handler is based on a suggestion and patch by Chad J. This handler is based on a suggestion and patch by Chad J.
Schroeder. Schroeder.
""" """
def __init__(self, filename, mode='a', encoding=None, delay=0): def __init__(self, filename, mode='a', encoding=None, delay=False):
logging.FileHandler.__init__(self, filename, mode, encoding, delay) logging.FileHandler.__init__(self, filename, mode, encoding, delay)
if not os.path.exists(self.baseFilename): if not os.path.exists(self.baseFilename):
self.dev, self.ino = -1, -1 self.dev, self.ino = -1, -1
......
...@@ -46,6 +46,7 @@ import time ...@@ -46,6 +46,7 @@ import time
import unittest import unittest
import warnings import warnings
import weakref import weakref
import zlib
try: try:
import threading import threading
# The following imports are needed only for tests which # The following imports are needed only for tests which
...@@ -3587,15 +3588,61 @@ class RotatingFileHandlerTest(BaseFileTest): ...@@ -3587,15 +3588,61 @@ class RotatingFileHandlerTest(BaseFileTest):
rh.close() rh.close()
def test_rollover_filenames(self): def test_rollover_filenames(self):
def namer(name):
return name + ".test"
rh = logging.handlers.RotatingFileHandler( rh = logging.handlers.RotatingFileHandler(
self.fn, backupCount=2, maxBytes=1) self.fn, backupCount=2, maxBytes=1)
rh.namer = namer
rh.emit(self.next_rec()) rh.emit(self.next_rec())
self.assertLogFile(self.fn) self.assertLogFile(self.fn)
rh.emit(self.next_rec()) rh.emit(self.next_rec())
self.assertLogFile(self.fn + ".1") self.assertLogFile(namer(self.fn + ".1"))
rh.emit(self.next_rec()) rh.emit(self.next_rec())
self.assertLogFile(self.fn + ".2") self.assertLogFile(namer(self.fn + ".2"))
self.assertFalse(os.path.exists(self.fn + ".3")) self.assertFalse(os.path.exists(namer(self.fn + ".3")))
rh.close()
def test_rotator(self):
def namer(name):
return name + ".gz"
def rotator(source, dest):
with open(source, "rb") as sf:
data = sf.read()
compressed = zlib.compress(data, 9)
with open(dest, "wb") as df:
df.write(compressed)
os.remove(source)
rh = logging.handlers.RotatingFileHandler(
self.fn, backupCount=2, maxBytes=1)
rh.rotator = rotator
rh.namer = namer
m1 = self.next_rec()
rh.emit(m1)
self.assertLogFile(self.fn)
m2 = self.next_rec()
rh.emit(m2)
fn = namer(self.fn + ".1")
self.assertLogFile(fn)
with open(fn, "rb") as f:
compressed = f.read()
data = zlib.decompress(compressed)
self.assertEqual(data.decode("ascii"), m1.msg + "\n")
rh.emit(self.next_rec())
fn = namer(self.fn + ".2")
self.assertLogFile(fn)
with open(fn, "rb") as f:
compressed = f.read()
data = zlib.decompress(compressed)
self.assertEqual(data.decode("ascii"), m1.msg + "\n")
rh.emit(self.next_rec())
fn = namer(self.fn + ".2")
with open(fn, "rb") as f:
compressed = f.read()
data = zlib.decompress(compressed)
self.assertEqual(data.decode("ascii"), m2.msg + "\n")
self.assertFalse(os.path.exists(namer(self.fn + ".3")))
rh.close() rh.close()
class TimedRotatingFileHandlerTest(BaseFileTest): class TimedRotatingFileHandlerTest(BaseFileTest):
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment