Skip to content

Commit 457ba33

Browse files
authored
gh-160433: Correct timed log backup names across DST changes (GH-160564)
1 parent 7dd8b47 commit 457ba33

3 files changed

Lines changed: 84 additions & 8 deletions

File tree

‎Lib/logging/handlers.py‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -459,14 +459,17 @@ def doRollover(self):
459459
timeTuple = time.gmtime(t)
460460
else:
461461
timeTuple = time.localtime(t)
462-
dstNow = time.localtime(currentTime)[-1]
463-
dstThen = timeTuple[-1]
464-
if dstNow != dstThen:
465-
if dstNow:
466-
addend = 3600
467-
else:
468-
addend = -3600
469-
timeTuple = time.localtime(t + addend)
462+
# DST affects calendar-based intervals, not fixed-duration ones.
463+
if self.when == 'MIDNIGHT' or self.when.startswith('W'):
464+
# A record can arrive well after the scheduled rollover.
465+
dstAtRollover = time.localtime(self.rolloverAt)[-1]
466+
dstThen = timeTuple[-1]
467+
if dstAtRollover != dstThen:
468+
if dstAtRollover:
469+
addend = 3600
470+
else:
471+
addend = -3600
472+
timeTuple = time.localtime(t + addend)
470473
dfn = self.rotation_filename(self.baseFilename + "." +
471474
time.strftime(self.suffix, timeTuple))
472475
self.rolloverAt = self.computeRollover(currentTime)

‎Lib/test/test_logging.py‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6726,6 +6726,77 @@ def rotator(source, dest):
67266726
rh.close()
67276727

67286728
class TimedRotatingFileHandlerTest(BaseFileTest):
6729+
def assert_rollover_contents(self, when, timestamps, expected, **kwargs):
6730+
with tempfile.TemporaryDirectory() as dirname:
6731+
filename = os.path.join(dirname, 'app.log')
6732+
now = [timestamps[0]]
6733+
with patch('time.time', lambda: now[0]):
6734+
handler = logging.handlers.TimedRotatingFileHandler(
6735+
filename, when=when, delay=True, encoding='utf-8', **kwargs)
6736+
try:
6737+
for i, timestamp in enumerate(timestamps):
6738+
now[0] = timestamp
6739+
handler.emit(logging.makeLogRecord({'msg': str(i)}))
6740+
finally:
6741+
handler.close()
6742+
actual = {}
6743+
for name in os.listdir(dirname):
6744+
with open(os.path.join(dirname, name), encoding='utf-8') as f:
6745+
actual[name.removeprefix('app.log')] = f.read().splitlines()
6746+
self.assertEqual(actual, expected)
6747+
6748+
@support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
6749+
def test_rollover_fixed_interval_dst(self):
6750+
DT = datetime.datetime
6751+
cases = (
6752+
('H', DT(2026, 3, 8, 1, 30), 3),
6753+
('H', DT(2026, 11, 1, 1, 30), 2),
6754+
('D', DT(2026, 3, 7, 23, 30), 3),
6755+
('D', DT(2026, 11, 1, 0, 30), 2),
6756+
)
6757+
for when, start, count in cases:
6758+
interval = 3600 if when == 'H' else 86400
6759+
timestamps = [start.timestamp() + i * interval for i in range(count)]
6760+
suffix = '.%Y-%m-%d_%H' if when == 'H' else '.%Y-%m-%d'
6761+
for utc in (False, True):
6762+
with self.subTest(when=when, start=start, utc=utc):
6763+
convert = time.gmtime if utc else time.localtime
6764+
expected = {
6765+
time.strftime(suffix, convert(timestamp)): [str(i)]
6766+
for i, timestamp in enumerate(timestamps[:-1])
6767+
}
6768+
expected[''] = [str(count - 1)]
6769+
self.assert_rollover_contents(
6770+
when, timestamps, expected, utc=utc)
6771+
6772+
@support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
6773+
def test_rollover_delayed_dst(self):
6774+
DT = datetime.datetime
6775+
cases = (
6776+
('MIDNIGHT', None,
6777+
[DT(2026, 10, day, hour).timestamp()
6778+
for day in (30, 31) for hour in (9, 18)] +
6779+
[DT(2026, 11, day, hour).timestamp()
6780+
for day in (1, 2, 3) for hour in (9, 18)],
6781+
{'.2026-10-30': ['0', '1'], '.2026-10-31': ['2', '3'],
6782+
'.2026-11-01': ['4', '5'], '.2026-11-02': ['6', '7'],
6783+
'': ['8', '9']}),
6784+
('W6', None,
6785+
[DT(2026, 10, 23, 9).timestamp(),
6786+
DT(2026, 11, 1, 9).timestamp(),
6787+
DT(2026, 11, 2, 9).timestamp()],
6788+
{'.2026-10-19': ['0'], '.2026-10-26': ['1'], '': ['2']}),
6789+
('MIDNIGHT', datetime.time(23, 30),
6790+
[DT(2026, month, 1, 12).timestamp() for month in (3, 4, 10, 12)],
6791+
{'.2026-02-28': ['0'], '.2026-03-31': ['1'],
6792+
'.2026-09-30': ['2'], '': ['3']}),
6793+
)
6794+
for when, at_time, timestamps, expected in cases:
6795+
for utc in (False, True):
6796+
with self.subTest(when=when, atTime=at_time, utc=utc):
6797+
self.assert_rollover_contents(
6798+
when, timestamps, expected, atTime=at_time, utc=utc)
6799+
67296800
@unittest.skipIf(support.is_wasi, "WASI does not have /dev/null.")
67306801
def test_should_not_rollover(self):
67316802
# See bpo-45401. Should only ever rollover regular files
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix incorrect backup names around daylight saving time changes in
2+
:class:`logging.handlers.TimedRotatingFileHandler`, including delayed rollovers.

0 commit comments

Comments
 (0)