From 27fe9e5bd215aa77915689db2411cc0111d547d3 Mon Sep 17 00:00:00 2001 From: Snotface Date: Sun, 4 Oct 2026 20:31:41 +0200 Subject: [PATCH 1/4] count only the current date's backups when RollingFileAppender starts On start-up the appender counts the size backups already on disk and continues from the highest. It read the number after the last "." of any matching file, which went wrong in three ways: - a date pattern without separators (.yyyyMMdd) was read as a backup number: counting up the next backup was named .20261004, counting down every size roll walked twenty million names - with staticLogFileName false, a date pattern that supplies the extension (.yyyy-MM-dd'.log') and preserveLogFileNameExtension, the current date's backups were skipped as "from a different date period", so the count restarted at 0 and the next roll deleted the existing backups - with a static file name, backups already rolled to an earlier date raised the current date's count GetBackupIndex now builds the name the current date's backups must have, as the appender names them, and counts only files of that name plus .N. With a static file name, a suffix that parses as a date in DatePattern is a dated file, not a backup. A date pattern that formats as a short number (.dd, .MM, .HH, .yyyy) names the dated file log.txt.10 exactly like backup 10, so one overwrites the other. For such configurations no numbered backups are made: size rolling is switched off, the existing file is appended to at start-up when appendToFile is false, and the reason is written as a FATAL line at the top of each file and as an internal warning. The new IgnoreDateWarnings property keeps the configured behaviour for those who accept the risk. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Appender/RollingFileAppenderTest.cs | 271 +++++++++++++++ src/log4net/Appender/RollingFileAppender.cs | 321 +++++++++++++----- .../appenders/rollingfileappender.adoc | 37 ++ 3 files changed, 550 insertions(+), 79 deletions(-) diff --git a/src/log4net.Tests/Appender/RollingFileAppenderTest.cs b/src/log4net.Tests/Appender/RollingFileAppenderTest.cs index f67c8818a..47ef505bd 100644 --- a/src/log4net.Tests/Appender/RollingFileAppenderTest.cs +++ b/src/log4net.Tests/Appender/RollingFileAppenderTest.cs @@ -1664,6 +1664,277 @@ public void TestInitializeRollBackups4() VerifyInitializeRollBackups(8, maxRollBackups); } + /// + /// Tests that a date roll file using a date pattern without "-" (yyyyMMdd) + /// is not mistaken for a backup index, whatever the backup limit + /// + [TestCase(5, 1)] + [TestCase(5, -1)] + [TestCase(-1, 1)] + [TestCase(-1, -1)] + public void TestInitializeRollBackupsIgnoresDateWithoutDashes(int maxSizeRollBackups, int countDirection) + { + const string baseFile = "LogFile.log"; + List files = ["LogFile.log", "LogFile.log.20261003", "LogFile.log.1", "LogFile.log.2"]; + + RollingFileAppender rfa = new() + { + RollingStyle = RollingFileAppender.RollingMode.Composite, + DatePattern = ".yyyyMMdd", + CountDirection = countDirection, + MaxSizeRollBackups = maxSizeRollBackups, + CurrentSizeRollBackups = 0 + }; + rfa.InitializeRollBackups(baseFile, files); + + Assert.That(rfa.CurrentSizeRollBackups, Is.EqualTo(2)); + } + + /// + /// Tests that counting up continues from the highest index, even when + /// the oldest backups were deleted and the indexes passed the backup limit + /// + [Test] + public void TestInitializeRollBackupsCountsUpPastLimit() + { + const string baseFile = "LogFile.log"; + List files = ["LogFile.log", "LogFile.log.5", "LogFile.log.6", "LogFile.log.7"]; + + RollingFileAppender rfa = new() + { + RollingStyle = RollingFileAppender.RollingMode.Composite, + CountDirection = 1, + MaxSizeRollBackups = 3, + CurrentSizeRollBackups = 0 + }; + rfa.InitializeRollBackups(baseFile, files); + + Assert.That(rfa.CurrentSizeRollBackups, Is.EqualTo(7)); + } + + /// + /// Tests that backups already rolled to an earlier date are not counted + /// as backups of the current date + /// + [Test] + public void TestInitializeRollBackupsIgnoresOtherDatePeriods() + { + const string baseFile = "LogFile.log"; + List files = + [ + "LogFile.log", + "LogFile.log.20261003", + "LogFile.log.20261003.1", + "LogFile.log.20261003.2", + "LogFile.log.20261003.3", + "LogFile.log.1", + ]; + + RollingFileAppender rfa = new() + { + RollingStyle = RollingFileAppender.RollingMode.Composite, + DatePattern = ".yyyyMMdd", + MaxSizeRollBackups = -1, + CurrentSizeRollBackups = 0 + }; + rfa.InitializeRollBackups(baseFile, files); + + Assert.That(rfa.CurrentSizeRollBackups, Is.EqualTo(1)); + } + + /// + /// Tests that backups of the current date are found when the date pattern + /// supplies the extension and the extension is preserved + /// + [Test] + public void TestInitializeRollBackupsDatePatternWithExtension() + { + const string baseFile = "log4net"; + List files = + [ + "log4net.2026-10-04.log", + "log4net.2026-10-04.1.log", + "log4net.2026-10-04.2.log", + "log4net.2026-10-03.log", + "log4net.2026-10-03.5.log", + ]; + + RollingFileAppender rfa = new() + { + RollingStyle = RollingFileAppender.RollingMode.Composite, + DatePattern = ".yyyy-MM-dd'.log'", + DateTimeStrategy = new Integration.MockDateTime(new DateTime(2026, 10, 4, 9, 0, 0)), + StaticLogFileName = false, + PreserveLogFileNameExtension = true, + MaxSizeRollBackups = -1, + CurrentSizeRollBackups = 0 + }; + rfa.InitializeRollBackups(baseFile, files); + + Assert.That(rfa.CurrentSizeRollBackups, Is.EqualTo(2)); + } + + /// + /// Tests that a short date pattern does not hide backups when the date is part of + /// the file name before the index (staticLogFileName false) + /// + [Test] + public void TestInitializeRollBackupsShortDatePatternNotStatic() + { + const string baseFile = "log.txt"; + List files = ["log.txt.04", "log.txt.04.10", "log.txt.04.11", "log.txt.03.12"]; + + RollingFileAppender rfa = new() + { + RollingStyle = RollingFileAppender.RollingMode.Composite, + DatePattern = ".dd", + DateTimeStrategy = new Integration.MockDateTime(new DateTime(2026, 10, 4, 9, 0, 0)), + StaticLogFileName = false, + MaxSizeRollBackups = -1, + CurrentSizeRollBackups = 0 + }; + rfa.InitializeRollBackups(baseFile, files); + + Assert.That(rfa.CurrentSizeRollBackups, Is.EqualTo(11)); + } + + /// + /// Tests that a short date pattern with a static file name switches off size rolling, + /// and writes why at the top of the file even when the filters would drop it + /// + [Test] + public void TestShortDatePatternSwitchesOffSizeRolling() + { + string[] lines = LogWithDatePattern(".dd", out int fileCount); + + Assert.That(fileCount, Is.EqualTo(1), "no size backups"); + Assert.That(lines, Has.Length.EqualTo(101)); + Assert.That(lines[0], Does.StartWith("FATAL ").And.Contain("size rolling has been switched off")); + } + + /// + /// Tests that a short date pattern with appendToFile false appends at start-up instead of + /// rolling the previous file to a numbered backup, for both styles that roll by date + /// + [TestCase(RollingFileAppender.RollingMode.Date)] + [TestCase(RollingFileAppender.RollingMode.Composite)] + public void TestShortDatePatternAppendsInsteadOfRollingAtStartup(RollingFileAppender.RollingMode rollingStyle) + { + string[] lines = LogWithDatePattern(".dd", out int fileCount, rollingStyle: rollingStyle, appendToFile: false, runs: 2); + + Assert.That(fileCount, Is.EqualTo(1), "no numbered backups"); + Assert.That(lines, Has.Length.EqualTo(202), "both runs kept"); + Assert.That(lines[0], Does.StartWith("FATAL ").And.Contain("appended to at start-up")); + Assert.That(lines[101], Does.StartWith("FATAL ")); + } + + /// + /// Tests that rolling by date only, appending, with a short date pattern makes no numbered + /// backups, so it writes no warning + /// + [Test] + public void TestShortDatePatternDateStyleAppendingHasNoWarning() + { + string[] lines = LogWithDatePattern(".dd", out int fileCount, rollingStyle: RollingFileAppender.RollingMode.Date); + + Assert.That(fileCount, Is.EqualTo(1)); + Assert.That(lines, Has.Length.EqualTo(100)); + Assert.That(lines[0], Does.StartWith("ERROR ")); + } + + /// + /// Tests that with a short date pattern the suffixes of a static file name count as backup + /// indexes, as a date like .10 cannot be told apart from backup 10 + /// + [Test] + public void TestInitializeRollBackupsShortDatePatternCountsIndexes() + { + const string baseFile = "log.txt"; + List files = ["log.txt", .. Enumerable.Range(1, 10).Select(i => $"log.txt.{i}")]; + + RollingFileAppender rfa = new() + { + RollingStyle = RollingFileAppender.RollingMode.Date, + DatePattern = ".dd", + MaxSizeRollBackups = -1, + CurrentSizeRollBackups = 0 + }; + rfa.InitializeRollBackups(baseFile, files); + + Assert.That(rfa.CurrentSizeRollBackups, Is.EqualTo(10)); + } + + /// + /// Tests that a long date pattern keeps size rolling and writes no warning + /// + [Test] + public void TestLongDatePatternKeepsSizeRolling() + { + string[] lines = LogWithDatePattern(".yyyy-MM-dd", out int fileCount); + + Assert.That(fileCount, Is.GreaterThan(1), "size backups"); + Assert.That(lines[0], Does.StartWith("ERROR ")); + } + + /// + /// Tests that IgnoreDateWarnings keeps size rolling for a short date pattern, without a warning + /// + [Test] + public void TestShortDatePatternIgnoreDateWarnings() + { + string[] lines = LogWithDatePattern(".dd", out int fileCount, ignoreDateWarnings: true); + + Assert.That(fileCount, Is.GreaterThan(1), "size backups"); + Assert.That(lines[0], Does.StartWith("ERROR ")); + } + + /// + /// Logs 100 lines of about 100 bytes per run through a static, date rolling appender + /// limited to 1KB, whose filters accept only ERROR. Each run is a new appender, as at start-up. + /// + /// the lines of the current log file + private static string[] LogWithDatePattern(string datePattern, out int fileCount, bool ignoreDateWarnings = false, + RollingFileAppender.RollingMode rollingStyle = RollingFileAppender.RollingMode.Composite, + bool appendToFile = true, int runs = 1) + { + string dir = Path.Combine(Path.GetTempPath(), "l4n-datepattern-" + Guid.NewGuid().ToString("N")); + try + { + for (int run = 0; run < runs; run++) + { + RollingFileAppender rfa = new() + { + File = Path.Combine(dir, "log.txt"), + AppendToFile = appendToFile, + RollingStyle = rollingStyle, + DatePattern = datePattern, + MaximumFileSize = "1KB", + MaxSizeRollBackups = -1, + IgnoreDateWarnings = ignoreDateWarnings, + Layout = new PatternLayout("%level %message%newline") + }; + rfa.AddFilter(new log4net.Filter.LevelMatchFilter { LevelToMatch = Level.Error }); + rfa.AddFilter(new log4net.Filter.DenyAllFilter()); + rfa.ActivateOptions(); + for (int i = 0; i < 100; i++) + { + rfa.DoAppend(new LoggingEvent(typeof(RollingFileAppenderTest), null, "x", Level.Error, $"line {i:D3} " + new string('x', 80), null)); + } + rfa.Close(); + } + + fileCount = Directory.GetFiles(dir).Length; + return File.ReadAllLines(Path.Combine(dir, "log.txt")); + } + finally + { + if (Directory.Exists(dir)) + { + Directory.Delete(dir, true); + } + } + } + /// /// Ensures that no problems result from creating and then closing the appender /// when it has not also been initialized with ActivateOptions(). diff --git a/src/log4net/Appender/RollingFileAppender.cs b/src/log4net/Appender/RollingFileAppender.cs index d6ad43f65..0245185bd 100644 --- a/src/log4net/Appender/RollingFileAppender.cs +++ b/src/log4net/Appender/RollingFileAppender.cs @@ -462,6 +462,26 @@ public RollingMode RollingStyle /// public bool StaticLogFileName { get; set; } = true; + /// + /// Gets or sets a value indicating whether to keep making numbered backups when the + /// makes dated files and numbered backups share names. + /// + /// + /// to roll as configured regardless, otherwise . The default is . + /// + /// + /// + /// With set, a short numeric date pattern such as .dd names + /// the dated file file.log.10, which is also the name of numbered backup 10, so one roll + /// overwrites the other. Numbered backups are made by size rolls, and at start-up when + /// is false. By default neither happens for such a + /// configuration: size rolling is switched off, the existing file is appended to, and the reason is + /// written at the top of each log file. Set this to to accept the risk: the + /// appender rolls as configured and no warning is written. + /// + /// + public bool IgnoreDateWarnings { get; set; } + /// /// The fully qualified type of the RollingFileAppender class. /// @@ -503,6 +523,7 @@ protected override void SetQWForFiles(TextWriter writer) protected override void Append(LoggingEvent loggingEvent) { AdjustFileBeforeAppend(); + AppendSizeRollConflict(); base.Append(loggingEvent); } @@ -520,9 +541,27 @@ protected override void Append(LoggingEvent loggingEvent) protected override void Append(LoggingEvent[] loggingEvents) { AdjustFileBeforeAppend(); + AppendSizeRollConflict(); base.Append(loggingEvents); } + /// + /// Writes the reason numbered backups were switched off at the top of a newly opened file. + /// + /// + /// Written past the filters and threshold, so it reaches the file whatever the configuration. + /// + private void AppendSizeRollConflict() + { + if (_sizeRollConflict is null || !_sizeRollConflictPending) + { + return; + } + + _sizeRollConflictPending = false; + base.Append(new LoggingEvent(_declaringType, null, _declaringType.FullName, Level.Fatal, _sizeRollConflict, null)); + } + /// /// Performs any required rolling before outputting the next event /// @@ -583,7 +622,7 @@ protected virtual void AdjustFileBeforeAppend() RetryFailedRoll(); } } - else if (_rollSize && (File is not null) && CountingWriter.Count >= MaxFileSize) + else if (_rollSize && _sizeRollConflict is null && (File is not null) && CountingWriter.Count >= MaxFileSize) { RollOverSize(); } @@ -618,7 +657,9 @@ protected override void OpenFile(string fileName, bool append) // AppendToFile says, or the roll destroys what it could not move. append = append || (_pendingRename is not null - && string.Equals(fileName, _pendingRename.From, StringComparison.Ordinal)); + && string.Equals(fileName, _pendingRename.From, StringComparison.Ordinal)) + // nothing can be rolled out of the way, see GetSizeRollConflict + || _sizeRollConflict is not null; // Calculate the current size of the file long currentCount = 0; @@ -666,6 +707,8 @@ protected override void OpenFile(string fileName, bool append) // Set the file size onto the counting writer. A refused lock leaves none. (QuietWriter as CountingQuietTextWriter)?.Count = currentCount; + + _sizeRollConflictPending = true; } } @@ -708,6 +751,12 @@ private void DetermineCurSizeRollBackups() { CurrentSizeRollBackups = 0; + if (_sizeRollConflict is not null) + { + // no numbered backups are made, and a numbered name may be a dated file, see GetSizeRollConflict + return; + } + string? fullPath; string? fileName; @@ -819,8 +868,9 @@ protected void ExistingInit() return; } - // If file exists, and we are not appending then roll it out of the way - if (AppendToFile) + // If file exists, and we are not appending then roll it out of the way, + // unless that would make a numbered backup named like a dated file + if (AppendToFile || _sizeRollConflict is not null) { return; } @@ -860,113 +910,194 @@ protected void ExistingInit() /// private void InitializeFromOneFile(string baseFile, string curFileName) { - curFileName = curFileName.ToLowerInvariant(); - baseFile = baseFile.ToLowerInvariant(); - string dir = string.IsNullOrEmpty(baseFile) ? "" : Path.GetDirectoryName(baseFile); - string baseFileWithoutExtension = Path.Combine(dir ?? "", Path.GetFileNameWithoutExtension(baseFile)); - - if (curFileName.StartsWith(baseFileWithoutExtension) == false) - { - return; // This is not a log file, so ignore - } + // Bump the counter up to the highest count seen so far + int backup = GetBackupIndex(baseFile, curFileName); - if (curFileName.Equals(baseFile, StringComparison.Ordinal)) + if (backup > CurrentSizeRollBackups) { - return; // Base log file is not an incremented logfile (.1 or .2, etc.) - } - - // Only look for files in the current roll point - if (_rollDate && !StaticLogFileName) - { - string date = DateTimeStrategy.Now.ToString(DatePattern, DateTimeFormatInfo.InvariantInfo).ToLowerInvariant(); - string prefix = (PreserveLogFileNameExtension - ? Path.GetFileNameWithoutExtension(baseFile) + date - : baseFile + date).ToLowerInvariant(); - string suffix = PreserveLogFileNameExtension - ? Path.GetExtension(baseFile).ToLowerInvariant() - : ""; - string curFileNameWithoutDir = Path.GetFileName(curFileName); - if (!curFileNameWithoutDir.StartsWith(prefix) || !curFileNameWithoutDir.EndsWith(suffix)) + if (0 == MaxSizeRollBackups) { - LogLog.Debug(_declaringType, $"Ignoring file [{curFileName}] because it is from a different date period"); - return; + // Stay at zero when zero backups are desired } - } - - try - { - // Bump the counter up to the highest count seen so far - int backup = GetBackupIndex(curFileName); - - // caution: we might get a false positive when certain - // date patterns such as yyyyMMdd are used...those are - // valid number but aren't the kind of back up index - // we're looking for - if (backup > CurrentSizeRollBackups) + else if (-1 == MaxSizeRollBackups) { - if (0 == MaxSizeRollBackups) - { - // Stay at zero when zero backups are desired - } - else if (-1 == MaxSizeRollBackups) + // Infinite backups, so go as high as the highest value + CurrentSizeRollBackups = backup; + } + else + { + // Backups limited to a finite number + if (CountDirection >= 0) { - // Infinite backups, so go as high as the highest value + // Go with the highest file when counting up CurrentSizeRollBackups = backup; } else { - // Backups limited to a finite number - if (CountDirection >= 0) + // Clip to the limit when counting down + if (backup <= MaxSizeRollBackups) { - // Go with the highest file when counting up CurrentSizeRollBackups = backup; } - else - { - // Clip to the limit when counting down - if (backup <= MaxSizeRollBackups) - { - CurrentSizeRollBackups = backup; - } - } } - LogLog.Debug(_declaringType, $"File name [{curFileName}] moves current count to [{CurrentSizeRollBackups}]"); } + LogLog.Debug(_declaringType, $"File name [{curFileName}] moves current count to [{CurrentSizeRollBackups}]"); + } + } + + /// + /// Gets the size backup index of within the current roll point. + /// + /// the base log file name + /// the existing file to examine + /// the backup index, or -1 when the file is not a size backup of the current roll point + /// + /// + /// A size backup is named CombinePath(group, ".N"), where the group is the base file, + /// or the base file plus the current date when is false. + /// Only files of exactly that shape count, so files from other date periods are ignored. + /// + /// + /// A date in a pattern without separators (e.g. .yyyyMMdd) also looks like .N, + /// so a suffix that parses as a date in is not taken as an index. + /// + /// + private int GetBackupIndex(string baseFile, string curFileName) + { + string fileName = Path.GetFileName(curFileName).ToLowerInvariant(); + string group = Path.GetFileName(baseFile).ToLowerInvariant(); + if (_rollDate && !StaticLogFileName) + { + group = CombinePath(group, DateTimeStrategy.Now.ToString(DatePattern, DateTimeFormatInfo.InvariantInfo).ToLowerInvariant()); + } + + // CombinePath puts the suffix before the extension when it is preserved + string extension = PreserveLogFileNameExtension ? Path.GetExtension(group) : string.Empty; + string head = group.Substring(0, group.Length - extension.Length); + if (fileName.Length <= group.Length + || !fileName.StartsWith(head, StringComparison.Ordinal) + || !fileName.EndsWith(extension, StringComparison.Ordinal)) + { + return -1; } - catch (FormatException) + + string suffix = fileName.Substring(head.Length, fileName.Length - head.Length - extension.Length); + + // only a static name puts the date where the index goes; otherwise the date is part of the group. + // A short numeric date (.dd) cannot be told from an index at all, so its suffixes count as + // indexes; no numbered files are made with one unless IgnoreDateWarnings is set. + if (_rollDate && StaticLogFileName && GetShortNumericDate() is null && IsDateSuffix(suffix)) { - //this happens when file.log -> file.log.yyyy-MM-dd which is normal - //when staticLogFileName == false - LogLog.Debug(_declaringType, $"Encountered a backup file not ending in .x [{curFileName}]"); + LogLog.Debug(_declaringType, $"File name [{curFileName}] ends in a date, not a backup index"); + return -1; } + + if (suffix.Length < 2 || suffix[0] != '.' + || !int.TryParse(suffix.Substring(1), NumberStyles.None, CultureInfo.InvariantCulture, out int result)) + { + return -1; + } + + // no check against MaxSizeRollBackups: counting up, real indexes keep climbing past it + return result; } /// - /// Attempts to extract a number from the end of the file name that indicates - /// the number of the times the file has been rolled over. + /// Determines whether is a date written with . /// + private bool IsDateSuffix(string suffix) + => DatePattern is not null + && DateTime.TryParseExact(suffix, DatePattern, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out _); + + /// + /// Checks whether dated files and numbered backups can end up with the same name. + /// + /// + /// a message explaining the conflict, or null when there is none or is set + /// /// - /// Certain date pattern extensions like yyyyMMdd will be parsed as valid backup indexes. + /// + /// With a static file name, a dated file is CombinePath(file, date) and a numbered backup is + /// CombinePath(file, ".N"). A short numeric date such as .dd makes the two identical: + /// log.txt.10 is both the 10th and backup 10, so rolling one overwrites the other. + /// Numbered backups are made by size rolls, and at start-up when + /// is false. For such configurations neither happens: size rolling is switched off, the existing + /// file is appended to, and the reason is written to each file. When + /// is set the appender rolls as configured and the conflict is only noted in the internal debug output. + /// /// - private int GetBackupIndex(string curFileName) + private string? GetSizeRollConflict() { - int result = -1; - string fileName = curFileName; + if (!StaticLogFileName || !_rollDate || MaxSizeRollBackups == 0 || (!_rollSize && AppendToFile)) + { + return null; + } - if (PreserveLogFileNameExtension) + string? date = GetShortNumericDate(); + if (date is null) { - fileName = Path.GetFileNameWithoutExtension(fileName); + return null; } - int index = fileName.LastIndexOf(".", StringComparison.Ordinal); - if (index > 0) + string fileName = Path.GetFileName(File?.Trim() ?? "log.txt"); + if (IgnoreDateWarnings) { - // if the "yyyy-MM-dd" component of file.log.yyyy-MM-dd is passed to TryParse - // it will gracefully fail and return backUpIndex will be 0 - _ = SystemInfo.TryParse(fileName.Substring(index + 1), out result); + LogLog.Debug(_declaringType, $"RollingFileAppender [{Name}]: datePattern [{DatePattern}] names dated files like" + + $" [{CombinePath(fileName, date)}], the name of numbered backup {date.Substring(1)}; rolling as configured, as ignoreDateWarnings is set."); + return null; } - return result; + return $"RollingFileAppender [{Name}]: datePattern [{DatePattern}] names dated files like" + + $" [{CombinePath(fileName, date)}], which is also the name of numbered backup {date.Substring(1)}." + + " So that no log files are overwritten, no numbered backups are made:" + + (_rollSize ? " size rolling has been switched off and maximumFileSize is ignored," : string.Empty) + + (AppendToFile ? string.Empty : " the existing file is appended to at start-up although appendToFile is false,") + + " and each file grows until the date rolls over. To roll as configured, use a longer date" + + " pattern (e.g. .yyyy-MM-dd) or set staticLogFileName to false. To roll as configured anyway and" + + " accept the risk, set ignoreDateWarnings to true."; + } + + /// + /// The short numeric date can write, cached per pattern. + /// See . + /// + private string? GetShortNumericDate() + { + if (DatePattern is null) + { + return null; + } + + if (!string.Equals(_shortNumericDatePattern, DatePattern, StringComparison.Ordinal)) + { + _shortNumericDate = FindShortNumericDate(DatePattern); + _shortNumericDatePattern = DatePattern; + } + return _shortNumericDate; + } + + /// + /// Finds a date that writes as a dot and a short number + /// without a leading zero, the same text as a size backup suffix. + /// + /// the formatted date, e.g. .10, or null when the pattern never writes one + private static string? FindShortNumericDate(string datePattern) + { + // single-letter tokens (d, M, H, m, s) vary in width, so sample every day at several times + TimeSpan[] times = [TimeSpan.Zero, new(10, 10, 10), new(23, 59, 59)]; + for (DateTime day = new(2024, 1, 1); day.Year == 2024; day = day.AddDays(1)) + { + foreach (TimeSpan time in times) + { + string date = (day + time).ToString(datePattern, DateTimeFormatInfo.InvariantInfo); + if (date.Length >= 2 && date.Length <= ShortDateDigits + 1 && date[0] == '.' && date[1] != '0' + && date.Skip(1).All(c => c is >= '0' and <= '9')) + { + return date; + } + } + } + return null; } /// @@ -1072,6 +1203,12 @@ public override void ActivateOptions() } } + _sizeRollConflict = GetSizeRollConflict(); + if (_sizeRollConflict is not null) + { + LogLog.Warn(_declaringType, _sizeRollConflict); + } + SecurityContext ??= SecurityContextProvider.DefaultProvider.CreateSecurityContext(this); using (SecurityContext.Impersonate(this)) @@ -1690,6 +1827,32 @@ protected static DateTime NextCheckDate(DateTime currentDateTime, RollPoint roll /// private bool _rollSize = true; + /// + /// Why numbered backups were switched off for this configuration, or null when they were not + /// (including when is set). See . + /// + private string? _sizeRollConflict; + + /// + /// Set when a file is opened, so is written at the top of each file. + /// + private bool _sizeRollConflictPending; + + /// + /// Date suffixes of at most this many digits are close enough to backup indexes to collide. + /// + private const int ShortDateDigits = 4; + + /// + /// The that was found for. + /// + private string? _shortNumericDatePattern; + + /// + /// Cached result of . See . + /// + private string? _shortNumericDate; + /// /// FileName provided in configuration. Used for rolling properly /// diff --git a/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/rollingfileappender.adoc b/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/rollingfileappender.adoc index fd169688b..74d158841 100644 --- a/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/rollingfileappender.adoc +++ b/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/rollingfileappender.adoc @@ -108,4 +108,41 @@ The file size does have to be limited but here it is set to 50 Gigabytes which, +---- + +[#short-date-patterns] +== Short date patterns + +When `StaticLogFileName` is true, a file rolled by date is named by appending the formatted `DatePattern` to the file name, and a numbered backup is named by appending a number. +Numbered backups are made when the file is rolled by size, and at start-up when `AppendToFile` is false and the previous file is rolled out of the way. +A date pattern that formats as a short number, such as `.dd`, `.MM`, `.HH` or `.yyyy`, makes the two indistinguishable: `MyApp.log.10` is both the file for the 10th and backup 10, so rolling one overwrites the other. + +To prevent log files from being lost, the `RollingFileAppender` makes no numbered backups when it is configured with a static file name, the `Date` or `Composite` rolling style, backups enabled (`MaxSizeRollBackups` other than 0) and a date pattern that can format as a dot followed by at most four digits: + +* with the `Composite` rolling style, rolling by size is switched off and `MaximumFileSize` is ignored, +* when `AppendToFile` is false, the existing file is appended to at start-up instead of being rolled out of the way. + +Each file then grows until the date rolls over. +The reason is written as a `FATAL` line at the top of each new log file, bypassing the appender's filters and threshold, and is also reported as a log4net internal warning. +A configuration that makes no numbered backups anyway, such as the `Date` rolling style with `AppendToFile` set to true, is not affected. + +To roll as configured, use a longer date pattern such as `.yyyy-MM-dd`, or set `StaticLogFileName` to false. + +To roll as configured anyway and accept that files may be overwritten, set `IgnoreDateWarnings` to true. +The appender then behaves as it would without this check and writes no warning. + +[source,xml] +---- + + + + + + + + + + + + ---- \ No newline at end of file From dd9416a53965fefd49d791a25b13d17714784fc0 Mon Sep 17 00:00:00 2001 From: Snotface Date: Sun, 4 Oct 2026 20:33:33 +0200 Subject: [PATCH 2/4] add changelog entry for the RollingFileAppender backup count fix Co-Authored-By: Claude Opus 5.5 (1M context) --- ...27fe9e5-rollingfileappender-backup-index.xml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/changelog/3.5.1/27fe9e5-rollingfileappender-backup-index.xml diff --git a/src/changelog/3.5.1/27fe9e5-rollingfileappender-backup-index.xml b/src/changelog/3.5.1/27fe9e5-rollingfileappender-backup-index.xml new file mode 100644 index 000000000..9d9f04490 --- /dev/null +++ b/src/changelog/3.5.1/27fe9e5-rollingfileappender-backup-index.xml @@ -0,0 +1,17 @@ + + + + + count only the current date's backups when `RollingFileAppender` starts. A date pattern without + separators (`.yyyyMMdd`) was read as a backup number, a date pattern supplying the extension with + `PreserveLogFileNameExtension` lost the current date's backups on restart, and backups of earlier dates + raised the current count. With a static file name, a date pattern that formats as a short number + (`.dd`, `.MM`, `.HH`, `.yyyy`) names dated files exactly like numbered backups, so no numbered backups + are made for it: size rolling is switched off, the existing file is appended to at start-up, and a + `FATAL` line explains why. The new `IgnoreDateWarnings` property keeps the configured behaviour + (fixed by @Snotface) + + From 9ebb6d9b7916e7b08ddea81e7e9e9b22f6330646 Mon Sep 17 00:00:00 2001 From: Snotface Date: Sun, 4 Oct 2026 20:36:04 +0200 Subject: [PATCH 3/4] name the changelog entry after the pull request #334 Co-Authored-By: Claude Opus 5.5 (1M context) --- ...kup-index.xml => 334-rollingfileappender-backup-index.xml} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/changelog/3.5.1/{27fe9e5-rollingfileappender-backup-index.xml => 334-rollingfileappender-backup-index.xml} (86%) diff --git a/src/changelog/3.5.1/27fe9e5-rollingfileappender-backup-index.xml b/src/changelog/3.5.1/334-rollingfileappender-backup-index.xml similarity index 86% rename from src/changelog/3.5.1/27fe9e5-rollingfileappender-backup-index.xml rename to src/changelog/3.5.1/334-rollingfileappender-backup-index.xml index 9d9f04490..f47f521de 100644 --- a/src/changelog/3.5.1/27fe9e5-rollingfileappender-backup-index.xml +++ b/src/changelog/3.5.1/334-rollingfileappender-backup-index.xml @@ -3,7 +3,7 @@ xmlns="https://logging.apache.org/xml/ns" xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd" type="fixed"> - + count only the current date's backups when `RollingFileAppender` starts. A date pattern without separators (`.yyyyMMdd`) was read as a backup number, a date pattern supplying the extension with @@ -12,6 +12,6 @@ (`.dd`, `.MM`, `.HH`, `.yyyy`) names dated files exactly like numbered backups, so no numbered backups are made for it: size rolling is switched off, the existing file is appended to at start-up, and a `FATAL` line explains why. The new `IgnoreDateWarnings` property keeps the configured behaviour - (fixed by @Snotface) + (fixed by @Snotface in https://github.com/apache/logging-log4net/pull/334[#334]) From ba511c5cf23a453751a7ba9aed16801f6fc48289 Mon Sep 17 00:00:00 2001 From: Jan Friedrich Date: Sun, 4 Oct 2026 22:52:56 +0200 Subject: [PATCH 4/4] narrow the fix to counting only the current date's backups #334 InitializeRollBackups took dated files and other dates' backups for backups of the current roll point: * A date without separators (.yyyyMMdd) parsed as a backup index, with a static or a non-static name, so the count jumped to 20261004. * With a static name, log.txt.20261003.1 counted as backup 1. * A date pattern supplying the extension (.yyyy-MM-dd'.log' with PreserveLogFileNameExtension) hid the current date's backups. Dates of up to four characters (.dd) still count as indexes, they cannot be told apart. Tests by @Snotface. --- .../334-rollingfileappender-backup-index.xml | 13 +- .../Appender/RollingFileAppenderTest.cs | 138 +------- src/log4net/Appender/RollingFileAppender.cs | 327 +++++------------- .../appenders/rollingfileappender.adoc | 37 -- 4 files changed, 105 insertions(+), 410 deletions(-) diff --git a/src/changelog/3.5.1/334-rollingfileappender-backup-index.xml b/src/changelog/3.5.1/334-rollingfileappender-backup-index.xml index f47f521de..761488373 100644 --- a/src/changelog/3.5.1/334-rollingfileappender-backup-index.xml +++ b/src/changelog/3.5.1/334-rollingfileappender-backup-index.xml @@ -4,14 +4,7 @@ xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd" type="fixed"> - - count only the current date's backups when `RollingFileAppender` starts. A date pattern without - separators (`.yyyyMMdd`) was read as a backup number, a date pattern supplying the extension with - `PreserveLogFileNameExtension` lost the current date's backups on restart, and backups of earlier dates - raised the current count. With a static file name, a date pattern that formats as a short number - (`.dd`, `.MM`, `.HH`, `.yyyy`) names dated files exactly like numbered backups, so no numbered backups - are made for it: size rolling is switched off, the existing file is appended to at start-up, and a - `FATAL` line explains why. The new `IgnoreDateWarnings` property keeps the configured behaviour - (fixed by @Snotface in https://github.com/apache/logging-log4net/pull/334[#334]) - + Count only the current date's backups when `RollingFileAppender` + starts. A date pattern without separators (`.yyyyMMdd`) no longer counts as a backup number, and + backups of earlier dates no longer raise the count (reported by @Snotface, fixed by @FreeAndNil) diff --git a/src/log4net.Tests/Appender/RollingFileAppenderTest.cs b/src/log4net.Tests/Appender/RollingFileAppenderTest.cs index 47ef505bd..8ef012574 100644 --- a/src/log4net.Tests/Appender/RollingFileAppenderTest.cs +++ b/src/log4net.Tests/Appender/RollingFileAppenderTest.cs @@ -1672,7 +1672,8 @@ public void TestInitializeRollBackups4() [TestCase(5, -1)] [TestCase(-1, 1)] [TestCase(-1, -1)] - public void TestInitializeRollBackupsIgnoresDateWithoutDashes(int maxSizeRollBackups, int countDirection) + public void TestInitializeRollBackupsIgnoresDateWithoutDashes(int maxSizeRollBackups, + int countDirection) { const string baseFile = "LogFile.log"; List files = ["LogFile.log", "LogFile.log.20261003", "LogFile.log.1", "LogFile.log.2"]; @@ -1775,71 +1776,27 @@ public void TestInitializeRollBackupsDatePatternWithExtension() } /// - /// Tests that a short date pattern does not hide backups when the date is part of - /// the file name before the index (staticLogFileName false) + /// Tests that the current date's file of a non-static name is not mistaken for a backup index + /// when the date pattern has no separators /// - [Test] - public void TestInitializeRollBackupsShortDatePatternNotStatic() + [TestCase("log.txt", ".yyyyMMdd", false, "log.txt.20261004", "log.txt.20261004.1")] + [TestCase("log", ".yyyyMMdd'.log'", true, "log.20261004.log", "log.20261004.1.log")] + public void TestInitializeRollBackupsNotStaticDateWithoutDashes(string baseFile, + string datePattern, bool preserveExtension, string datedFile, string backupFile) { - const string baseFile = "log.txt"; - List files = ["log.txt.04", "log.txt.04.10", "log.txt.04.11", "log.txt.03.12"]; - RollingFileAppender rfa = new() { RollingStyle = RollingFileAppender.RollingMode.Composite, - DatePattern = ".dd", + DatePattern = datePattern, DateTimeStrategy = new Integration.MockDateTime(new DateTime(2026, 10, 4, 9, 0, 0)), StaticLogFileName = false, + PreserveLogFileNameExtension = preserveExtension, MaxSizeRollBackups = -1, CurrentSizeRollBackups = 0 }; - rfa.InitializeRollBackups(baseFile, files); - - Assert.That(rfa.CurrentSizeRollBackups, Is.EqualTo(11)); - } - - /// - /// Tests that a short date pattern with a static file name switches off size rolling, - /// and writes why at the top of the file even when the filters would drop it - /// - [Test] - public void TestShortDatePatternSwitchesOffSizeRolling() - { - string[] lines = LogWithDatePattern(".dd", out int fileCount); - - Assert.That(fileCount, Is.EqualTo(1), "no size backups"); - Assert.That(lines, Has.Length.EqualTo(101)); - Assert.That(lines[0], Does.StartWith("FATAL ").And.Contain("size rolling has been switched off")); - } - - /// - /// Tests that a short date pattern with appendToFile false appends at start-up instead of - /// rolling the previous file to a numbered backup, for both styles that roll by date - /// - [TestCase(RollingFileAppender.RollingMode.Date)] - [TestCase(RollingFileAppender.RollingMode.Composite)] - public void TestShortDatePatternAppendsInsteadOfRollingAtStartup(RollingFileAppender.RollingMode rollingStyle) - { - string[] lines = LogWithDatePattern(".dd", out int fileCount, rollingStyle: rollingStyle, appendToFile: false, runs: 2); - - Assert.That(fileCount, Is.EqualTo(1), "no numbered backups"); - Assert.That(lines, Has.Length.EqualTo(202), "both runs kept"); - Assert.That(lines[0], Does.StartWith("FATAL ").And.Contain("appended to at start-up")); - Assert.That(lines[101], Does.StartWith("FATAL ")); - } + rfa.InitializeRollBackups(baseFile, [datedFile, backupFile]); - /// - /// Tests that rolling by date only, appending, with a short date pattern makes no numbered - /// backups, so it writes no warning - /// - [Test] - public void TestShortDatePatternDateStyleAppendingHasNoWarning() - { - string[] lines = LogWithDatePattern(".dd", out int fileCount, rollingStyle: RollingFileAppender.RollingMode.Date); - - Assert.That(fileCount, Is.EqualTo(1)); - Assert.That(lines, Has.Length.EqualTo(100)); - Assert.That(lines[0], Does.StartWith("ERROR ")); + Assert.That(rfa.CurrentSizeRollBackups, Is.EqualTo(1)); } /// @@ -1864,77 +1821,6 @@ public void TestInitializeRollBackupsShortDatePatternCountsIndexes() Assert.That(rfa.CurrentSizeRollBackups, Is.EqualTo(10)); } - /// - /// Tests that a long date pattern keeps size rolling and writes no warning - /// - [Test] - public void TestLongDatePatternKeepsSizeRolling() - { - string[] lines = LogWithDatePattern(".yyyy-MM-dd", out int fileCount); - - Assert.That(fileCount, Is.GreaterThan(1), "size backups"); - Assert.That(lines[0], Does.StartWith("ERROR ")); - } - - /// - /// Tests that IgnoreDateWarnings keeps size rolling for a short date pattern, without a warning - /// - [Test] - public void TestShortDatePatternIgnoreDateWarnings() - { - string[] lines = LogWithDatePattern(".dd", out int fileCount, ignoreDateWarnings: true); - - Assert.That(fileCount, Is.GreaterThan(1), "size backups"); - Assert.That(lines[0], Does.StartWith("ERROR ")); - } - - /// - /// Logs 100 lines of about 100 bytes per run through a static, date rolling appender - /// limited to 1KB, whose filters accept only ERROR. Each run is a new appender, as at start-up. - /// - /// the lines of the current log file - private static string[] LogWithDatePattern(string datePattern, out int fileCount, bool ignoreDateWarnings = false, - RollingFileAppender.RollingMode rollingStyle = RollingFileAppender.RollingMode.Composite, - bool appendToFile = true, int runs = 1) - { - string dir = Path.Combine(Path.GetTempPath(), "l4n-datepattern-" + Guid.NewGuid().ToString("N")); - try - { - for (int run = 0; run < runs; run++) - { - RollingFileAppender rfa = new() - { - File = Path.Combine(dir, "log.txt"), - AppendToFile = appendToFile, - RollingStyle = rollingStyle, - DatePattern = datePattern, - MaximumFileSize = "1KB", - MaxSizeRollBackups = -1, - IgnoreDateWarnings = ignoreDateWarnings, - Layout = new PatternLayout("%level %message%newline") - }; - rfa.AddFilter(new log4net.Filter.LevelMatchFilter { LevelToMatch = Level.Error }); - rfa.AddFilter(new log4net.Filter.DenyAllFilter()); - rfa.ActivateOptions(); - for (int i = 0; i < 100; i++) - { - rfa.DoAppend(new LoggingEvent(typeof(RollingFileAppenderTest), null, "x", Level.Error, $"line {i:D3} " + new string('x', 80), null)); - } - rfa.Close(); - } - - fileCount = Directory.GetFiles(dir).Length; - return File.ReadAllLines(Path.Combine(dir, "log.txt")); - } - finally - { - if (Directory.Exists(dir)) - { - Directory.Delete(dir, true); - } - } - } - /// /// Ensures that no problems result from creating and then closing the appender /// when it has not also been initialized with ActivateOptions(). diff --git a/src/log4net/Appender/RollingFileAppender.cs b/src/log4net/Appender/RollingFileAppender.cs index 0245185bd..0cbdd1446 100644 --- a/src/log4net/Appender/RollingFileAppender.cs +++ b/src/log4net/Appender/RollingFileAppender.cs @@ -462,26 +462,6 @@ public RollingMode RollingStyle /// public bool StaticLogFileName { get; set; } = true; - /// - /// Gets or sets a value indicating whether to keep making numbered backups when the - /// makes dated files and numbered backups share names. - /// - /// - /// to roll as configured regardless, otherwise . The default is . - /// - /// - /// - /// With set, a short numeric date pattern such as .dd names - /// the dated file file.log.10, which is also the name of numbered backup 10, so one roll - /// overwrites the other. Numbered backups are made by size rolls, and at start-up when - /// is false. By default neither happens for such a - /// configuration: size rolling is switched off, the existing file is appended to, and the reason is - /// written at the top of each log file. Set this to to accept the risk: the - /// appender rolls as configured and no warning is written. - /// - /// - public bool IgnoreDateWarnings { get; set; } - /// /// The fully qualified type of the RollingFileAppender class. /// @@ -523,7 +503,6 @@ protected override void SetQWForFiles(TextWriter writer) protected override void Append(LoggingEvent loggingEvent) { AdjustFileBeforeAppend(); - AppendSizeRollConflict(); base.Append(loggingEvent); } @@ -541,27 +520,9 @@ protected override void Append(LoggingEvent loggingEvent) protected override void Append(LoggingEvent[] loggingEvents) { AdjustFileBeforeAppend(); - AppendSizeRollConflict(); base.Append(loggingEvents); } - /// - /// Writes the reason numbered backups were switched off at the top of a newly opened file. - /// - /// - /// Written past the filters and threshold, so it reaches the file whatever the configuration. - /// - private void AppendSizeRollConflict() - { - if (_sizeRollConflict is null || !_sizeRollConflictPending) - { - return; - } - - _sizeRollConflictPending = false; - base.Append(new LoggingEvent(_declaringType, null, _declaringType.FullName, Level.Fatal, _sizeRollConflict, null)); - } - /// /// Performs any required rolling before outputting the next event /// @@ -622,7 +583,7 @@ protected virtual void AdjustFileBeforeAppend() RetryFailedRoll(); } } - else if (_rollSize && _sizeRollConflict is null && (File is not null) && CountingWriter.Count >= MaxFileSize) + else if (_rollSize && (File is not null) && CountingWriter.Count >= MaxFileSize) { RollOverSize(); } @@ -657,9 +618,7 @@ protected override void OpenFile(string fileName, bool append) // AppendToFile says, or the roll destroys what it could not move. append = append || (_pendingRename is not null - && string.Equals(fileName, _pendingRename.From, StringComparison.Ordinal)) - // nothing can be rolled out of the way, see GetSizeRollConflict - || _sizeRollConflict is not null; + && string.Equals(fileName, _pendingRename.From, StringComparison.Ordinal)); // Calculate the current size of the file long currentCount = 0; @@ -707,8 +666,6 @@ protected override void OpenFile(string fileName, bool append) // Set the file size onto the counting writer. A refused lock leaves none. (QuietWriter as CountingQuietTextWriter)?.Count = currentCount; - - _sizeRollConflictPending = true; } } @@ -751,12 +708,6 @@ private void DetermineCurSizeRollBackups() { CurrentSizeRollBackups = 0; - if (_sizeRollConflict is not null) - { - // no numbered backups are made, and a numbered name may be a dated file, see GetSizeRollConflict - return; - } - string? fullPath; string? fileName; @@ -868,9 +819,8 @@ protected void ExistingInit() return; } - // If file exists, and we are not appending then roll it out of the way, - // unless that would make a numbered backup named like a dated file - if (AppendToFile || _sizeRollConflict is not null) + // If file exists, and we are not appending then roll it out of the way + if (AppendToFile) { return; } @@ -910,195 +860,130 @@ protected void ExistingInit() /// private void InitializeFromOneFile(string baseFile, string curFileName) { - // Bump the counter up to the highest count seen so far - int backup = GetBackupIndex(baseFile, curFileName); + curFileName = curFileName.ToLowerInvariant(); + baseFile = baseFile.ToLowerInvariant(); + string dir = string.IsNullOrEmpty(baseFile) ? "" : Path.GetDirectoryName(baseFile); + string baseFileWithoutExtension = Path.Combine(dir ?? "", Path.GetFileNameWithoutExtension(baseFile)); + + if (curFileName.StartsWith(baseFileWithoutExtension) == false) + { + return; // This is not a log file, so ignore + } - if (backup > CurrentSizeRollBackups) + if (curFileName.Equals(baseFile, StringComparison.Ordinal)) + { + return; // Base log file is not an incremented logfile (.1 or .2, etc.) + } + + // Only look for files in the current roll point + if (_rollDate && !StaticLogFileName) { - if (0 == MaxSizeRollBackups) + string date = DateTimeStrategy.Now.ToString(DatePattern, DateTimeFormatInfo.InvariantInfo).ToLowerInvariant(); + // the date pattern may supply the extension, so take it from the dated name + string dated = Path.GetFileName(CombinePath(baseFile, date)); + string suffix = PreserveLogFileNameExtension ? Path.GetExtension(dated) : ""; + string prefix = dated.Substring(0, dated.Length - suffix.Length); + string curFileNameWithoutDir = Path.GetFileName(curFileName); + if (!curFileNameWithoutDir.StartsWith(prefix) || !curFileNameWithoutDir.EndsWith(suffix)) { - // Stay at zero when zero backups are desired + LogLog.Debug(_declaringType, $"Ignoring file [{curFileName}] because it is from a different date period"); + return; } - else if (-1 == MaxSizeRollBackups) + + // the dated file itself has no index, and a date like .yyyyMMdd would parse as one + if (curFileNameWithoutDir.Length <= dated.Length) { - // Infinite backups, so go as high as the highest value - CurrentSizeRollBackups = backup; + return; } - else + } + + try + { + // Bump the counter up to the highest count seen so far + int backup = GetBackupIndex(baseFile, curFileName); + + if (backup > CurrentSizeRollBackups) { - // Backups limited to a finite number - if (CountDirection >= 0) + if (0 == MaxSizeRollBackups) + { + // Stay at zero when zero backups are desired + } + else if (-1 == MaxSizeRollBackups) { - // Go with the highest file when counting up + // Infinite backups, so go as high as the highest value CurrentSizeRollBackups = backup; } else { - // Clip to the limit when counting down - if (backup <= MaxSizeRollBackups) + // Backups limited to a finite number + if (CountDirection >= 0) { + // Go with the highest file when counting up CurrentSizeRollBackups = backup; } + else + { + // Clip to the limit when counting down + if (backup <= MaxSizeRollBackups) + { + CurrentSizeRollBackups = backup; + } + } } + LogLog.Debug(_declaringType, $"File name [{curFileName}] moves current count to [{CurrentSizeRollBackups}]"); } - LogLog.Debug(_declaringType, $"File name [{curFileName}] moves current count to [{CurrentSizeRollBackups}]"); + } + catch (FormatException) + { + //this happens when file.log -> file.log.yyyy-MM-dd which is normal + //when staticLogFileName == false + LogLog.Debug(_declaringType, $"Encountered a backup file not ending in .x [{curFileName}]"); } } /// - /// Gets the size backup index of within the current roll point. + /// Attempts to extract a number from the end of the file name that indicates + /// the number of the times the file has been rolled over. /// - /// the base log file name - /// the existing file to examine - /// the backup index, or -1 when the file is not a size backup of the current roll point - /// - /// - /// A size backup is named CombinePath(group, ".N"), where the group is the base file, - /// or the base file plus the current date when is false. - /// Only files of exactly that shape count, so files from other date periods are ignored. - /// - /// - /// A date in a pattern without separators (e.g. .yyyyMMdd) also looks like .N, - /// so a suffix that parses as a date in is not taken as an index. - /// - /// private int GetBackupIndex(string baseFile, string curFileName) { - string fileName = Path.GetFileName(curFileName).ToLowerInvariant(); - string group = Path.GetFileName(baseFile).ToLowerInvariant(); - if (_rollDate && !StaticLogFileName) - { - group = CombinePath(group, DateTimeStrategy.Now.ToString(DatePattern, DateTimeFormatInfo.InvariantInfo).ToLowerInvariant()); - } + int result = -1; + string fileName = curFileName; - // CombinePath puts the suffix before the extension when it is preserved - string extension = PreserveLogFileNameExtension ? Path.GetExtension(group) : string.Empty; - string head = group.Substring(0, group.Length - extension.Length); - if (fileName.Length <= group.Length - || !fileName.StartsWith(head, StringComparison.Ordinal) - || !fileName.EndsWith(extension, StringComparison.Ordinal)) + if (PreserveLogFileNameExtension) { - return -1; + fileName = Path.GetFileNameWithoutExtension(fileName); + baseFile = Path.GetFileNameWithoutExtension(baseFile); } - string suffix = fileName.Substring(head.Length, fileName.Length - head.Length - extension.Length); - - // only a static name puts the date where the index goes; otherwise the date is part of the group. - // A short numeric date (.dd) cannot be told from an index at all, so its suffixes count as - // indexes; no numbered files are made with one unless IgnoreDateWarnings is set. - if (_rollDate && StaticLogFileName && GetShortNumericDate() is null && IsDateSuffix(suffix)) + int index = fileName.LastIndexOf(".", StringComparison.Ordinal); + if (index > 0) { - LogLog.Debug(_declaringType, $"File name [{curFileName}] ends in a date, not a backup index"); - return -1; - } + // with a static name, log.txt.20261003.1 is a backup of an earlier date + if (StaticLogFileName + && (!string.Equals(fileName.Substring(0, index), baseFile, StringComparison.Ordinal) + || IsDateSuffix(fileName.Substring(index)))) + { + return -1; + } - if (suffix.Length < 2 || suffix[0] != '.' - || !int.TryParse(suffix.Substring(1), NumberStyles.None, CultureInfo.InvariantCulture, out int result)) - { - return -1; + // if the "yyyy-MM-dd" component of file.log.yyyy-MM-dd is passed to TryParse + // it will gracefully fail and return backUpIndex will be 0 + _ = SystemInfo.TryParse(fileName.Substring(index + 1), out result); } - // no check against MaxSizeRollBackups: counting up, real indexes keep climbing past it return result; } /// - /// Determines whether is a date written with . + /// Determines whether is a date longer than four characters after + /// the dot, such as .yyyyMMdd. Shorter dates (.dd writes .10) cannot be + /// told from a backup index and keep counting as one. /// private bool IsDateSuffix(string suffix) - => DatePattern is not null - && DateTime.TryParseExact(suffix, DatePattern, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out _); - - /// - /// Checks whether dated files and numbered backups can end up with the same name. - /// - /// - /// a message explaining the conflict, or null when there is none or is set - /// - /// - /// - /// With a static file name, a dated file is CombinePath(file, date) and a numbered backup is - /// CombinePath(file, ".N"). A short numeric date such as .dd makes the two identical: - /// log.txt.10 is both the 10th and backup 10, so rolling one overwrites the other. - /// Numbered backups are made by size rolls, and at start-up when - /// is false. For such configurations neither happens: size rolling is switched off, the existing - /// file is appended to, and the reason is written to each file. When - /// is set the appender rolls as configured and the conflict is only noted in the internal debug output. - /// - /// - private string? GetSizeRollConflict() - { - if (!StaticLogFileName || !_rollDate || MaxSizeRollBackups == 0 || (!_rollSize && AppendToFile)) - { - return null; - } - - string? date = GetShortNumericDate(); - if (date is null) - { - return null; - } - - string fileName = Path.GetFileName(File?.Trim() ?? "log.txt"); - if (IgnoreDateWarnings) - { - LogLog.Debug(_declaringType, $"RollingFileAppender [{Name}]: datePattern [{DatePattern}] names dated files like" - + $" [{CombinePath(fileName, date)}], the name of numbered backup {date.Substring(1)}; rolling as configured, as ignoreDateWarnings is set."); - return null; - } - - return $"RollingFileAppender [{Name}]: datePattern [{DatePattern}] names dated files like" - + $" [{CombinePath(fileName, date)}], which is also the name of numbered backup {date.Substring(1)}." - + " So that no log files are overwritten, no numbered backups are made:" - + (_rollSize ? " size rolling has been switched off and maximumFileSize is ignored," : string.Empty) - + (AppendToFile ? string.Empty : " the existing file is appended to at start-up although appendToFile is false,") - + " and each file grows until the date rolls over. To roll as configured, use a longer date" - + " pattern (e.g. .yyyy-MM-dd) or set staticLogFileName to false. To roll as configured anyway and" - + " accept the risk, set ignoreDateWarnings to true."; - } - - /// - /// The short numeric date can write, cached per pattern. - /// See . - /// - private string? GetShortNumericDate() - { - if (DatePattern is null) - { - return null; - } - - if (!string.Equals(_shortNumericDatePattern, DatePattern, StringComparison.Ordinal)) - { - _shortNumericDate = FindShortNumericDate(DatePattern); - _shortNumericDatePattern = DatePattern; - } - return _shortNumericDate; - } - - /// - /// Finds a date that writes as a dot and a short number - /// without a leading zero, the same text as a size backup suffix. - /// - /// the formatted date, e.g. .10, or null when the pattern never writes one - private static string? FindShortNumericDate(string datePattern) - { - // single-letter tokens (d, M, H, m, s) vary in width, so sample every day at several times - TimeSpan[] times = [TimeSpan.Zero, new(10, 10, 10), new(23, 59, 59)]; - for (DateTime day = new(2024, 1, 1); day.Year == 2024; day = day.AddDays(1)) - { - foreach (TimeSpan time in times) - { - string date = (day + time).ToString(datePattern, DateTimeFormatInfo.InvariantInfo); - if (date.Length >= 2 && date.Length <= ShortDateDigits + 1 && date[0] == '.' && date[1] != '0' - && date.Skip(1).All(c => c is >= '0' and <= '9')) - { - return date; - } - } - } - return null; - } + => _rollDate && DatePattern is not null && suffix.Length > 5 + && DateTime.TryParseExact(suffix, DatePattern, DateTimeFormatInfo.InvariantInfo, + DateTimeStyles.None, out _); /// /// Takes a list of files and a base file name, and looks for 'incremented' versions of the base file. @@ -1203,12 +1088,6 @@ public override void ActivateOptions() } } - _sizeRollConflict = GetSizeRollConflict(); - if (_sizeRollConflict is not null) - { - LogLog.Warn(_declaringType, _sizeRollConflict); - } - SecurityContext ??= SecurityContextProvider.DefaultProvider.CreateSecurityContext(this); using (SecurityContext.Impersonate(this)) @@ -1827,32 +1706,6 @@ protected static DateTime NextCheckDate(DateTime currentDateTime, RollPoint roll /// private bool _rollSize = true; - /// - /// Why numbered backups were switched off for this configuration, or null when they were not - /// (including when is set). See . - /// - private string? _sizeRollConflict; - - /// - /// Set when a file is opened, so is written at the top of each file. - /// - private bool _sizeRollConflictPending; - - /// - /// Date suffixes of at most this many digits are close enough to backup indexes to collide. - /// - private const int ShortDateDigits = 4; - - /// - /// The that was found for. - /// - private string? _shortNumericDatePattern; - - /// - /// Cached result of . See . - /// - private string? _shortNumericDate; - /// /// FileName provided in configuration. Used for rolling properly /// diff --git a/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/rollingfileappender.adoc b/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/rollingfileappender.adoc index 74d158841..fd169688b 100644 --- a/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/rollingfileappender.adoc +++ b/src/site/antora/modules/ROOT/pages/manual/configuration/appenders/rollingfileappender.adoc @@ -108,41 +108,4 @@ The file size does have to be limited but here it is set to 50 Gigabytes which, ----- - -[#short-date-patterns] -== Short date patterns - -When `StaticLogFileName` is true, a file rolled by date is named by appending the formatted `DatePattern` to the file name, and a numbered backup is named by appending a number. -Numbered backups are made when the file is rolled by size, and at start-up when `AppendToFile` is false and the previous file is rolled out of the way. -A date pattern that formats as a short number, such as `.dd`, `.MM`, `.HH` or `.yyyy`, makes the two indistinguishable: `MyApp.log.10` is both the file for the 10th and backup 10, so rolling one overwrites the other. - -To prevent log files from being lost, the `RollingFileAppender` makes no numbered backups when it is configured with a static file name, the `Date` or `Composite` rolling style, backups enabled (`MaxSizeRollBackups` other than 0) and a date pattern that can format as a dot followed by at most four digits: - -* with the `Composite` rolling style, rolling by size is switched off and `MaximumFileSize` is ignored, -* when `AppendToFile` is false, the existing file is appended to at start-up instead of being rolled out of the way. - -Each file then grows until the date rolls over. -The reason is written as a `FATAL` line at the top of each new log file, bypassing the appender's filters and threshold, and is also reported as a log4net internal warning. -A configuration that makes no numbered backups anyway, such as the `Date` rolling style with `AppendToFile` set to true, is not affected. - -To roll as configured, use a longer date pattern such as `.yyyy-MM-dd`, or set `StaticLogFileName` to false. - -To roll as configured anyway and accept that files may be overwritten, set `IgnoreDateWarnings` to true. -The appender then behaves as it would without this check and writes no warning. - -[source,xml] ----- - - - - - - - - - - - - ---- \ No newline at end of file