Skip to content

Commit 507c105

Browse files
committed
bpo-40469: Make TimedRotatingFileHandler use CTIME instead of MTIME
The TimedRotatingFileHandler previously used 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 MTIME is also updated to the current time and the rotation never happens. It's more appropriate to check the file creation time (CTIME) instead.
1 parent 213a6bb commit 507c105

File tree

3 files changed

+61
-2
lines changed

3 files changed

+61
-2
lines changed

Lib/logging/handlers.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"""
2525

2626
import io, logging, socket, os, pickle, struct, time, re
27-
from stat import ST_DEV, ST_INO, ST_MTIME
27+
from stat import ST_DEV, ST_INO
2828
import queue
2929
import threading
3030
import copy
@@ -263,7 +263,15 @@ def __init__(self, filename, when='h', interval=1, backupCount=0,
263263
# path object (see Issue #27493), but self.baseFilename will be a string
264264
filename = self.baseFilename
265265
if os.path.exists(filename):
266-
t = os.stat(filename)[ST_MTIME]
266+
# Use the minimum of file creation and modification time as the base of the
267+
# rollover calculation
268+
stat_result = os.stat(filename)
269+
# Use st_birthtime whenever it is available or use st_ctime instead otherwise
270+
try:
271+
creation_time = stat_result.st_birthtime
272+
except AttributeError:
273+
creation_time = stat_result.st_ctime
274+
t = int(min(creation_time, stat_result.st_mtime))
267275
else:
268276
t = int(time.time())
269277
self.rolloverAt = self.computeRollover(t)

Lib/test/test_logging.py

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

6083+
def test_rollover_based_on_creation_time_only(self):
6084+
def add_record(message: str) -> None:
6085+
fh = logging.handlers.TimedRotatingFileHandler(
6086+
self.fn, when='S', interval=4, encoding="utf-8", backupCount=1)
6087+
fmt = logging.Formatter('%(asctime)s %(message)s')
6088+
fh.setFormatter(fmt)
6089+
record = logging.makeLogRecord({'msg': message})
6090+
fh.emit(record)
6091+
fh.close()
6092+
print(f"Filename: {self.fn}")
6093+
import subprocess
6094+
subprocess.run(["stat", self.fn])
6095+
print(os.stat(self.fn))
6096+
6097+
add_record('testing - initial')
6098+
self.assertLogFile(self.fn)
6099+
# Sleep a little over the half of rollover time - and this value must be over
6100+
# 2 seconds, since this is the mtime resolution on FAT32 filesystems.
6101+
time.sleep(2.1)
6102+
add_record('testing - update before rollover to renew the st_mtime')
6103+
time.sleep(2.1) # a little over the half of rollover time
6104+
add_record('testing - new record supposedly in the new file after rollover')
6105+
6106+
# At this point, the log file should be rotated if the rotation is based
6107+
# on creation time but should be not if it's based on creation time
6108+
found = False
6109+
now = datetime.datetime.now()
6110+
GO_BACK = 5 # seconds
6111+
for secs in range(GO_BACK):
6112+
prev = now - datetime.timedelta(seconds=secs)
6113+
fn = self.fn + prev.strftime(".%Y-%m-%d_%H-%M-%S")
6114+
found = os.path.exists(fn)
6115+
if found:
6116+
self.rmfiles.append(fn)
6117+
break
6118+
msg = 'No rotated files found, went back %d seconds' % GO_BACK
6119+
if not found:
6120+
# print additional diagnostics
6121+
dn, fn = os.path.split(self.fn)
6122+
files = [f for f in os.listdir(dn) if f.startswith(fn)]
6123+
print('Test time: %s' % now.strftime("%Y-%m-%d %H-%M-%S"), file=sys.stderr)
6124+
print('The only matching files are: %s' % files, file=sys.stderr)
6125+
for f in files:
6126+
print('Contents of %s:' % f)
6127+
path = os.path.join(dn, f)
6128+
print(os.stat(path))
6129+
with open(path, 'r') as tf:
6130+
print(tf.read())
6131+
self.assertTrue(found, msg=msg)
6132+
60836133
def test_rollover_at_midnight(self):
60846134
atTime = datetime.datetime.now().time()
60856135
fmt = logging.Formatter('%(asctime)s %(message)s')

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,6 +1170,7 @@ Owen Martin
11701170
Sidney San Martín
11711171
Westley Martínez
11721172
Sébastien Martini
1173+
Iván Márton
11731174
Roger Masse
11741175
Nick Mathewson
11751176
Simon Mathieu

0 commit comments

Comments
 (0)