Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 23b94d0

Browse files
committed
Refactored logging rotating handlers for improved flexibility.
1 parent 239a042 commit 23b94d0

4 files changed

Lines changed: 215 additions & 17 deletions

File tree

Doc/howto/logging-cookbook.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,3 +1102,31 @@ This dictionary is passed to :func:`~logging.config.dictConfig` to put the confi
11021102
For more information about this configuration, you can see the `relevant
11031103
section <https://docs.djangoproject.com/en/1.3/topics/logging/#configuring-logging>`_
11041104
of the Django documentation.
1105+
1106+
.. _cookbook-rotator-namer:
1107+
1108+
Using a rotator and namer to customise log rotation processing
1109+
--------------------------------------------------------------
1110+
1111+
An example of how you can define a namer and rotator is given in the following
1112+
snippet, which shows zlib-based compression of the log file::
1113+
1114+
def namer(name):
1115+
return name + ".gz"
1116+
1117+
def rotator(source, dest):
1118+
with open(source, "rb") as sf:
1119+
data = sf.read()
1120+
compressed = zlib.compress(data, 9)
1121+
with open(dest, "wb") as df:
1122+
df.write(compressed)
1123+
os.remove(source)
1124+
1125+
rh = logging.handlers.RotatingFileHandler(...)
1126+
rh.rotator = rotator
1127+
rh.namer = namer
1128+
1129+
These are not “true” .gz files, as they are bare compressed data, with no
1130+
“container” such as you’d find in an actual gzip file. This snippet is just
1131+
for illustration purposes.
1132+

Doc/library/logging.handlers.rst

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,87 @@ this value.
164164
changed. If it has, the existing stream is flushed and closed and the
165165
file opened again, before outputting the record to the file.
166166

167+
.. _base-rotating-handler:
168+
169+
BaseRotatingHandler
170+
^^^^^^^^^^^^^^^^^^^
171+
172+
The :class:`BaseRotatingHandler` class, located in the :mod:`logging.handlers`
173+
module, is the base class for the rotating file handlers,
174+
:class:`RotatingFileHandler` and :class:`TimedRotatingFileHandler`. You should
175+
not need to instantiate this class, but it has attributes and methods you may
176+
need to override.
177+
178+
.. class:: BaseRotatingHandler(filename, mode, encoding=None, delay=False)
179+
180+
The parameters are as for :class:`FileHandler`. The attributes are:
181+
182+
.. attribute:: namer
183+
184+
If this attribute is set to a callable, the :meth:`rotation_filename`
185+
method delegates to this callable. The parameters passed to the callable
186+
are those passed to :meth:`rotation_filename`.
187+
188+
.. note:: The namer function is called quite a few times during rollover,
189+
so it should be as simple and as fast as possible. It should also
190+
return the same output every time for a given input, otherwise the
191+
rollover behaviour may not work as expected.
192+
193+
.. versionadded:: 3.3
194+
195+
196+
.. attribute:: BaseRotatingHandler.rotator
197+
198+
If this attribute is set to a callable, the :meth:`rotate` method
199+
delegates to this callable. The parameters passed to the callable are
200+
those passed to :meth:`rotate`.
201+
202+
.. versionadded:: 3.3
203+
204+
.. method:: BaseRotatingHandler.rotation_filename(default_name)
205+
206+
Modify the filename of a log file when rotating.
207+
208+
This is provided so that a custom filename can be provided.
209+
210+
The default implementation calls the 'namer' attribute of the handler,
211+
if it's callable, passing the default name to it. If the attribute isn't
212+
callable (the default is `None`), the name is returned unchanged.
213+
214+
:param default_name: The default name for the log file.
215+
216+
.. versionadded:: 3.3
217+
218+
219+
.. method:: BaseRotatingHandler.rotate(source, dest)
220+
221+
When rotating, rotate the current log.
222+
223+
The default implementation calls the 'rotator' attribute of the handler,
224+
if it's callable, passing the source and dest arguments to it. If the
225+
attribute isn't callable (the default is `None`), the source is simply
226+
renamed to the destination.
227+
228+
:param source: The source filename. This is normally the base
229+
filename, e.g. 'test.log'
230+
:param dest: The destination filename. This is normally
231+
what the source is rotated to, e.g. 'test.log.1'.
232+
233+
.. versionadded:: 3.3
234+
235+
The reason the attributes exist is to save you having to subclass - you can use
236+
the same callables for instances of :class:`RotatingFileHandler` and
237+
:class:`TimedRotatingFileHandler`. If either the namer or rotator callable
238+
raises an exception, this will be handled in the same way as any other
239+
exception during an :meth:`emit` call, i.e. via the :meth:`handleError` method
240+
of the handler.
241+
242+
If you need to make more significant changes to rotation processing, you can
243+
override the methods.
244+
245+
For an example, see :ref:`cookbook-rotator-namer`.
246+
247+
167248
.. _rotating-file-handler:
168249

169250
RotatingFileHandler

Lib/logging/handlers.py

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,15 @@ class BaseRotatingHandler(logging.FileHandler):
5252
Not meant to be instantiated directly. Instead, use RotatingFileHandler
5353
or TimedRotatingFileHandler.
5454
"""
55-
def __init__(self, filename, mode, encoding=None, delay=0):
55+
def __init__(self, filename, mode, encoding=None, delay=False):
5656
"""
5757
Use the specified filename for streamed logging
5858
"""
5959
logging.FileHandler.__init__(self, filename, mode, encoding, delay)
6060
self.mode = mode
6161
self.encoding = encoding
62+
self.namer = None
63+
self.rotator = None
6264

6365
def emit(self, record):
6466
"""
@@ -76,12 +78,50 @@ def emit(self, record):
7678
except:
7779
self.handleError(record)
7880

81+
def rotation_filename(self, default_name):
82+
"""
83+
Modify the filename of a log file when rotating.
84+
85+
This is provided so that a custom filename can be provided.
86+
87+
The default implementation calls the 'namer' attribute of the
88+
handler, if it's callable, passing the default name to
89+
it. If the attribute isn't callable (the default is None), the name
90+
is returned unchanged.
91+
92+
:param default_name: The default name for the log file.
93+
"""
94+
if not callable(self.namer):
95+
result = default_name
96+
else:
97+
result = self.namer(default_name)
98+
return result
99+
100+
def rotate(self, source, dest):
101+
"""
102+
When rotating, rotate the current log.
103+
104+
The default implementation calls the 'rotator' attribute of the
105+
handler, if it's callable, passing the source and dest arguments to
106+
it. If the attribute isn't callable (the default is None), the source
107+
is simply renamed to the destination.
108+
109+
:param source: The source filename. This is normally the base
110+
filename, e.g. 'test.log'
111+
:param dest: The destination filename. This is normally
112+
what the source is rotated to, e.g. 'test.log.1'.
113+
"""
114+
if not callable(self.rotator):
115+
os.rename(source, dest)
116+
else:
117+
self.rotator(source, dest)
118+
79119
class RotatingFileHandler(BaseRotatingHandler):
80120
"""
81121
Handler for logging to a set of files, which switches from one file
82122
to the next when the current file reaches a certain size.
83123
"""
84-
def __init__(self, filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=0):
124+
def __init__(self, filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=False):
85125
"""
86126
Open the specified file and use it as the stream for logging.
87127
@@ -122,16 +162,17 @@ def doRollover(self):
122162
self.stream = None
123163
if self.backupCount > 0:
124164
for i in range(self.backupCount - 1, 0, -1):
125-
sfn = "%s.%d" % (self.baseFilename, i)
126-
dfn = "%s.%d" % (self.baseFilename, i + 1)
165+
sfn = self.rotation_filename("%s.%d" % (self.baseFilename, i))
166+
dfn = self.rotation_filename("%s.%d" % (self.baseFilename,
167+
i + 1))
127168
if os.path.exists(sfn):
128169
if os.path.exists(dfn):
129170
os.remove(dfn)
130171
os.rename(sfn, dfn)
131-
dfn = self.baseFilename + ".1"
172+
dfn = self.rotation_filename(self.baseFilename + ".1")
132173
if os.path.exists(dfn):
133174
os.remove(dfn)
134-
os.rename(self.baseFilename, dfn)
175+
self.rotate(self.baseFilename, dfn)
135176
self.mode = 'w'
136177
self.stream = self._open()
137178

@@ -179,19 +220,19 @@ def __init__(self, filename, when='h', interval=1, backupCount=0, encoding=None,
179220
if self.when == 'S':
180221
self.interval = 1 # one second
181222
self.suffix = "%Y-%m-%d_%H-%M-%S"
182-
self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$"
223+
self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(\.\w+)?$"
183224
elif self.when == 'M':
184225
self.interval = 60 # one minute
185226
self.suffix = "%Y-%m-%d_%H-%M"
186-
self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}$"
227+
self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}(\.\w+)?$"
187228
elif self.when == 'H':
188229
self.interval = 60 * 60 # one hour
189230
self.suffix = "%Y-%m-%d_%H"
190-
self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}$"
231+
self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}(\.\w+)?$"
191232
elif self.when == 'D' or self.when == 'MIDNIGHT':
192233
self.interval = 60 * 60 * 24 # one day
193234
self.suffix = "%Y-%m-%d"
194-
self.extMatch = r"^\d{4}-\d{2}-\d{2}$"
235+
self.extMatch = r"^\d{4}-\d{2}-\d{2}(\.\w+)?$"
195236
elif self.when.startswith('W'):
196237
self.interval = 60 * 60 * 24 * 7 # one week
197238
if len(self.when) != 2:
@@ -200,7 +241,7 @@ def __init__(self, filename, when='h', interval=1, backupCount=0, encoding=None,
200241
raise ValueError("Invalid day specified for weekly rollover: %s" % self.when)
201242
self.dayOfWeek = int(self.when[1])
202243
self.suffix = "%Y-%m-%d"
203-
self.extMatch = r"^\d{4}-\d{2}-\d{2}$"
244+
self.extMatch = r"^\d{4}-\d{2}-\d{2}(\.\w+)?$"
204245
else:
205246
raise ValueError("Invalid rollover interval specified: %s" % self.when)
206247

@@ -323,10 +364,11 @@ def doRollover(self):
323364
timeTuple = time.gmtime(t)
324365
else:
325366
timeTuple = time.localtime(t)
326-
dfn = self.baseFilename + "." + time.strftime(self.suffix, timeTuple)
367+
dfn = self.rotation_filename(self.baseFilename + "." +
368+
time.strftime(self.suffix, timeTuple))
327369
if os.path.exists(dfn):
328370
os.remove(dfn)
329-
os.rename(self.baseFilename, dfn)
371+
self.rotate(self.baseFilename, dfn)
330372
if self.backupCount > 0:
331373
for s in self.getFilesToDelete():
332374
os.remove(s)
@@ -367,7 +409,7 @@ class WatchedFileHandler(logging.FileHandler):
367409
This handler is based on a suggestion and patch by Chad J.
368410
Schroeder.
369411
"""
370-
def __init__(self, filename, mode='a', encoding=None, delay=0):
412+
def __init__(self, filename, mode='a', encoding=None, delay=False):
371413
logging.FileHandler.__init__(self, filename, mode, encoding, delay)
372414
if not os.path.exists(self.baseFilename):
373415
self.dev, self.ino = -1, -1

Lib/test/test_logging.py

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import unittest
4747
import warnings
4848
import weakref
49+
import zlib
4950
try:
5051
import threading
5152
# The following imports are needed only for tests which
@@ -3587,15 +3588,61 @@ def test_file_created(self):
35873588
rh.close()
35883589

35893590
def test_rollover_filenames(self):
3591+
def namer(name):
3592+
return name + ".test"
35903593
rh = logging.handlers.RotatingFileHandler(
35913594
self.fn, backupCount=2, maxBytes=1)
3595+
rh.namer = namer
35923596
rh.emit(self.next_rec())
35933597
self.assertLogFile(self.fn)
35943598
rh.emit(self.next_rec())
3595-
self.assertLogFile(self.fn + ".1")
3599+
self.assertLogFile(namer(self.fn + ".1"))
35963600
rh.emit(self.next_rec())
3597-
self.assertLogFile(self.fn + ".2")
3598-
self.assertFalse(os.path.exists(self.fn + ".3"))
3601+
self.assertLogFile(namer(self.fn + ".2"))
3602+
self.assertFalse(os.path.exists(namer(self.fn + ".3")))
3603+
rh.close()
3604+
3605+
def test_rotator(self):
3606+
def namer(name):
3607+
return name + ".gz"
3608+
3609+
def rotator(source, dest):
3610+
with open(source, "rb") as sf:
3611+
data = sf.read()
3612+
compressed = zlib.compress(data, 9)
3613+
with open(dest, "wb") as df:
3614+
df.write(compressed)
3615+
os.remove(source)
3616+
3617+
rh = logging.handlers.RotatingFileHandler(
3618+
self.fn, backupCount=2, maxBytes=1)
3619+
rh.rotator = rotator
3620+
rh.namer = namer
3621+
m1 = self.next_rec()
3622+
rh.emit(m1)
3623+
self.assertLogFile(self.fn)
3624+
m2 = self.next_rec()
3625+
rh.emit(m2)
3626+
fn = namer(self.fn + ".1")
3627+
self.assertLogFile(fn)
3628+
with open(fn, "rb") as f:
3629+
compressed = f.read()
3630+
data = zlib.decompress(compressed)
3631+
self.assertEqual(data.decode("ascii"), m1.msg + "\n")
3632+
rh.emit(self.next_rec())
3633+
fn = namer(self.fn + ".2")
3634+
self.assertLogFile(fn)
3635+
with open(fn, "rb") as f:
3636+
compressed = f.read()
3637+
data = zlib.decompress(compressed)
3638+
self.assertEqual(data.decode("ascii"), m1.msg + "\n")
3639+
rh.emit(self.next_rec())
3640+
fn = namer(self.fn + ".2")
3641+
with open(fn, "rb") as f:
3642+
compressed = f.read()
3643+
data = zlib.decompress(compressed)
3644+
self.assertEqual(data.decode("ascii"), m2.msg + "\n")
3645+
self.assertFalse(os.path.exists(namer(self.fn + ".3")))
35993646
rh.close()
36003647

36013648
class TimedRotatingFileHandlerTest(BaseFileTest):

0 commit comments

Comments
 (0)