Skip to content

Commit 1f4242a

Browse files
LarryHu0217miss-islington
authored andcommitted
gh-160433: Correct timed log backup names across DST changes (GH-160564)
(cherry picked from commit 457ba33) Co-authored-by: Liang Hu <35699841+LarryHu0217@users.noreply.github.com>
1 parent e0a3c81 commit 1f4242a

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
@@ -436,14 +436,17 @@ def doRollover(self):
436436
timeTuple = time.gmtime(t)
437437
else:
438438
timeTuple = time.localtime(t)
439-
dstNow = time.localtime(currentTime)[-1]
440-
dstThen = timeTuple[-1]
441-
if dstNow != dstThen:
442-
if dstNow:
443-
addend = 3600
444-
else:
445-
addend = -3600
446-
timeTuple = time.localtime(t + addend)
439+
# DST affects calendar-based intervals, not fixed-duration ones.
440+
if self.when == 'MIDNIGHT' or self.when.startswith('W'):
441+
# A record can arrive well after the scheduled rollover.
442+
dstAtRollover = time.localtime(self.rolloverAt)[-1]
443+
dstThen = timeTuple[-1]
444+
if dstAtRollover != dstThen:
445+
if dstAtRollover:
446+
addend = 3600
447+
else:
448+
addend = -3600
449+
timeTuple = time.localtime(t + addend)
447450
dfn = self.rotation_filename(self.baseFilename + "." +
448451
time.strftime(self.suffix, timeTuple))
449452
self.rolloverAt = self.computeRollover(currentTime)

‎Lib/test/test_logging.py‎

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

65866586
class TimedRotatingFileHandlerTest(BaseFileTest):
6587+
def assert_rollover_contents(self, when, timestamps, expected, **kwargs):
6588+
with tempfile.TemporaryDirectory() as dirname:
6589+
filename = os.path.join(dirname, 'app.log')
6590+
now = [timestamps[0]]
6591+
with patch('time.time', lambda: now[0]):
6592+
handler = logging.handlers.TimedRotatingFileHandler(
6593+
filename, when=when, delay=True, encoding='utf-8', **kwargs)
6594+
try:
6595+
for i, timestamp in enumerate(timestamps):
6596+
now[0] = timestamp
6597+
handler.emit(logging.makeLogRecord({'msg': str(i)}))
6598+
finally:
6599+
handler.close()
6600+
actual = {}
6601+
for name in os.listdir(dirname):
6602+
with open(os.path.join(dirname, name), encoding='utf-8') as f:
6603+
actual[name.removeprefix('app.log')] = f.read().splitlines()
6604+
self.assertEqual(actual, expected)
6605+
6606+
@support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
6607+
def test_rollover_fixed_interval_dst(self):
6608+
DT = datetime.datetime
6609+
cases = (
6610+
('H', DT(2026, 3, 8, 1, 30), 3),
6611+
('H', DT(2026, 11, 1, 1, 30), 2),
6612+
('D', DT(2026, 3, 7, 23, 30), 3),
6613+
('D', DT(2026, 11, 1, 0, 30), 2),
6614+
)
6615+
for when, start, count in cases:
6616+
interval = 3600 if when == 'H' else 86400
6617+
timestamps = [start.timestamp() + i * interval for i in range(count)]
6618+
suffix = '.%Y-%m-%d_%H' if when == 'H' else '.%Y-%m-%d'
6619+
for utc in (False, True):
6620+
with self.subTest(when=when, start=start, utc=utc):
6621+
convert = time.gmtime if utc else time.localtime
6622+
expected = {
6623+
time.strftime(suffix, convert(timestamp)): [str(i)]
6624+
for i, timestamp in enumerate(timestamps[:-1])
6625+
}
6626+
expected[''] = [str(count - 1)]
6627+
self.assert_rollover_contents(
6628+
when, timestamps, expected, utc=utc)
6629+
6630+
@support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
6631+
def test_rollover_delayed_dst(self):
6632+
DT = datetime.datetime
6633+
cases = (
6634+
('MIDNIGHT', None,
6635+
[DT(2026, 10, day, hour).timestamp()
6636+
for day in (30, 31) for hour in (9, 18)] +
6637+
[DT(2026, 11, day, hour).timestamp()
6638+
for day in (1, 2, 3) for hour in (9, 18)],
6639+
{'.2026-10-30': ['0', '1'], '.2026-10-31': ['2', '3'],
6640+
'.2026-11-01': ['4', '5'], '.2026-11-02': ['6', '7'],
6641+
'': ['8', '9']}),
6642+
('W6', None,
6643+
[DT(2026, 10, 23, 9).timestamp(),
6644+
DT(2026, 11, 1, 9).timestamp(),
6645+
DT(2026, 11, 2, 9).timestamp()],
6646+
{'.2026-10-19': ['0'], '.2026-10-26': ['1'], '': ['2']}),
6647+
('MIDNIGHT', datetime.time(23, 30),
6648+
[DT(2026, month, 1, 12).timestamp() for month in (3, 4, 10, 12)],
6649+
{'.2026-02-28': ['0'], '.2026-03-31': ['1'],
6650+
'.2026-09-30': ['2'], '': ['3']}),
6651+
)
6652+
for when, at_time, timestamps, expected in cases:
6653+
for utc in (False, True):
6654+
with self.subTest(when=when, atTime=at_time, utc=utc):
6655+
self.assert_rollover_contents(
6656+
when, timestamps, expected, atTime=at_time, utc=utc)
6657+
65876658
@unittest.skipIf(support.is_wasi, "WASI does not have /dev/null.")
65886659
def test_should_not_rollover(self):
65896660
# 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)