Skip to content

Commit ac3694b

Browse files
authored
gh-84649: Make TimedRotatingFileHandler use CTIME instead of MTIME (GH-24660)
The TimedRotatingFileHandler previously only used st_mtime attribute of the log file to detect whether it has to be rotate yet or not. In cases when the file is changed within the rotatation period the st_mtime is also updated to the current time and the rotation never happens. It's more appropriate to check the file creation time (st_ctime) instead. Whenever available, the more appropriate st_birthtime will be in use. (This feature is available on FreeBSD, MacOS and Windows at the moment.) If the st_mtime would be newer than st_ctime (e.g.: because the inode related to the file has been changed without any file content modification), then the earliest attribute will be used.
1 parent fc9c4db commit ac3694b

5 files changed

Lines changed: 72 additions & 1 deletion

File tree

Lib/logging/handlers.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,16 @@ def __init__(self, filename, when='h', interval=1, backupCount=0,
282282
# path object (see Issue #27493), but self.baseFilename will be a string
283283
filename = self.baseFilename
284284
if os.path.exists(filename):
285-
t = int(os.stat(filename).st_mtime)
285+
# Use the minimum of file creation and modification time as
286+
# the base of the rollover calculation
287+
stat_result = os.stat(filename)
288+
# Use st_birthtime whenever it is available or use st_ctime
289+
# instead otherwise
290+
try:
291+
creation_time = stat_result.st_birthtime
292+
except AttributeError:
293+
creation_time = stat_result.st_ctime
294+
t = int(min(creation_time, stat_result.st_mtime))
286295
else:
287296
t = int(time.time())
288297
self.rolloverAt = self.computeRollover(t)

Lib/test/support/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"has_fork_support", "requires_fork",
4141
"has_subprocess_support", "requires_subprocess",
4242
"has_socket_support", "requires_working_socket",
43+
"has_st_birthtime",
4344
"has_remote_subprocess_debugging", "requires_remote_subprocess_debugging",
4445
"anticipate_failure", "load_package_tests", "detect_api_mismatch",
4546
"check__all__", "skip_if_buggy_ucrt_strfptime",
@@ -620,6 +621,10 @@ def skip_wasi_stack_overflow():
620621
or is_android
621622
)
622623

624+
# At the moment, st_birthtime attribute is only supported on Windows,
625+
# MacOS and FreeBSD.
626+
has_st_birthtime = sys.platform.startswith(("win", "freebsd", "darwin"))
627+
623628
def requires_fork():
624629
return unittest.skipUnless(has_fork_support, "requires working os.fork()")
625630

Lib/test/test_logging.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6615,6 +6615,56 @@ def test_rollover(self):
66156615
print(tf.read())
66166616
self.assertTrue(found, msg=msg)
66176617

6618+
@unittest.skipUnless(support.has_st_birthtime,
6619+
"st_birthtime not available or supported by Python on this OS")
6620+
def test_rollover_based_on_st_birthtime_only(self):
6621+
def add_record(message: str) -> None:
6622+
fh = logging.handlers.TimedRotatingFileHandler(
6623+
self.fn, when='S', interval=4, encoding="utf-8", backupCount=1)
6624+
fmt = logging.Formatter('%(asctime)s %(message)s')
6625+
fh.setFormatter(fmt)
6626+
record = logging.makeLogRecord({'msg': message})
6627+
fh.emit(record)
6628+
fh.close()
6629+
6630+
add_record('testing - initial')
6631+
self.assertLogFile(self.fn)
6632+
# Sleep a little over the half of rollover time - and this value
6633+
# must be over 2 seconds, since this is the mtime resolution on
6634+
# FAT32 filesystems.
6635+
time.sleep(2.1)
6636+
add_record('testing - update before rollover to renew the st_mtime')
6637+
time.sleep(2.1) # a little over the half of rollover time
6638+
add_record('testing - new record supposedly in the new file after rollover')
6639+
6640+
# At this point, the log file should be rotated if the rotation
6641+
# is based on creation time but should be not if it's based on
6642+
# creation time.
6643+
found = False
6644+
now = datetime.datetime.now()
6645+
GO_BACK = 5 # seconds
6646+
for secs in range(GO_BACK):
6647+
prev = now - datetime.timedelta(seconds=secs)
6648+
fn = self.fn + prev.strftime(".%Y-%m-%d_%H-%M-%S")
6649+
found = os.path.exists(fn)
6650+
if found:
6651+
self.rmfiles.append(fn)
6652+
break
6653+
msg = 'No rotated files found, went back %d seconds' % GO_BACK
6654+
if not found:
6655+
# print additional diagnostics
6656+
dn, fn = os.path.split(self.fn)
6657+
files = [f for f in os.listdir(dn) if f.startswith(fn)]
6658+
print('Test time: %s' % now.strftime("%Y-%m-%d %H-%M-%S"), file=sys.stderr)
6659+
print('The only matching files are: %s' % files, file=sys.stderr)
6660+
for f in files:
6661+
print('Contents of %s:' % f)
6662+
path = os.path.join(dn, f)
6663+
print(os.stat(path))
6664+
with open(path, 'r') as tf:
6665+
print(tf.read())
6666+
self.assertTrue(found, msg=msg)
6667+
66186668
def test_rollover_at_midnight(self, weekly=False):
66196669
os_helper.unlink(self.fn)
66206670
now = datetime.datetime.now()

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1226,6 +1226,7 @@ Owen Martin
12261226
Sidney San Martín
12271227
Westley Martínez
12281228
Sébastien Martini
1229+
Iván Márton
12291230
Roger Masse
12301231
Nick Mathewson
12311232
Simon Mathieu
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
A bug has been fixed that made the ``TimedRotatingFileHandler`` use the
2+
MTIME attribute of the configured log file to to detect whether it has to be
3+
rotated yet or not. In cases when the file was changed within the rotation
4+
period the value of the MTIME was also updated to the current time and as a
5+
result the rotation never happened. The file creation time (CTIME) is used
6+
instead that makes the rotation file modification independent.

0 commit comments

Comments
 (0)