mirror of
https://github.com/python/cpython.git
synced 2025-10-17 12:18:23 +00:00
Refactored logging rotating handlers for improved flexibility.
This commit is contained in:
parent
239a0429fd
commit
23b94d0b98
4 changed files with 215 additions and 17 deletions
|
@ -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.
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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):
|
||||||
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
|
||||||
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):
|
||||||
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):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue