From 2818667fe5fa1c70fe201bc894bd24019a2e8ffc Mon Sep 17 00:00:00 2001 From: Alexander Haugg Date: Wed, 18 Jun 2025 15:34:29 +0200 Subject: [PATCH 1/2] Creation DateTime in the log file name added --- .../FileLoggerConfigurationExtensions.cs | 23 +++--- .../Sinks/File/PathRoller.cs | 69 ++++++++++++----- .../Sinks/File/RollingFileSink.cs | 76 +++++++++++++++---- .../TemplatedPathRollerTests.cs | 46 ++++++++++- 4 files changed, 172 insertions(+), 42 deletions(-) diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index e3e8bcf..b9f074d 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -164,7 +164,7 @@ public static LoggerConfiguration File( /// Logger sink configuration. /// A formatter, such as , to convert the log events into /// text for the file. If control of regular text formatting is required, use the other - /// overload of + /// overload of /// and specify the outputTemplate parameter instead. /// /// Path to the file. @@ -236,6 +236,7 @@ public static LoggerConfiguration File( /// Must be greater than or equal to . /// Ignored if is . /// The default is to retain files indefinitely. + /// /// Configuration object allowing method chaining. /// When is null /// When is null @@ -262,7 +263,8 @@ public static LoggerConfiguration File( int? retainedFileCountLimit = DefaultRetainedFileCountLimit, Encoding? encoding = null, FileLifecycleHooks? hooks = null, - TimeSpan? retainedFileTimeLimit = null) + TimeSpan? retainedFileTimeLimit = null, + string? dateTimeFormatFileName = null) { if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); if (path == null) throw new ArgumentNullException(nameof(path)); @@ -271,7 +273,7 @@ public static LoggerConfiguration File( var formatter = new MessageTemplateTextFormatter(outputTemplate, formatProvider); return File(sinkConfiguration, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered, shared, flushToDiskInterval, - rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding, hooks, retainedFileTimeLimit); + rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding, hooks, retainedFileTimeLimit, dateTimeFormatFileName); } /// @@ -280,7 +282,7 @@ public static LoggerConfiguration File( /// Logger sink configuration. /// A formatter, such as , to convert the log events into /// text for the file. If control of regular text formatting is required, use the other - /// overload of + /// overload of /// and specify the outputTemplate parameter instead. /// /// Path to the file. @@ -306,6 +308,7 @@ public static LoggerConfiguration File( /// Must be greater than or equal to . /// Ignored if is . /// The default is to retain files indefinitely. + /// /// Configuration object allowing method chaining. /// When is null /// When is null @@ -331,7 +334,8 @@ public static LoggerConfiguration File( int? retainedFileCountLimit = DefaultRetainedFileCountLimit, Encoding? encoding = null, FileLifecycleHooks? hooks = null, - TimeSpan? retainedFileTimeLimit = null) + TimeSpan? retainedFileTimeLimit = null, + string? dateTimeFormatFileName = null) { if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration)); if (formatter == null) throw new ArgumentNullException(nameof(formatter)); @@ -339,7 +343,7 @@ public static LoggerConfiguration File( return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch, buffered, false, shared, flushToDiskInterval, encoding, rollingInterval, rollOnFileSizeLimit, - retainedFileCountLimit, hooks, retainedFileTimeLimit); + retainedFileCountLimit, hooks, retainedFileTimeLimit, dateTimeFormatFileName); } /// @@ -494,7 +498,7 @@ public static LoggerConfiguration File( if (path == null) throw new ArgumentNullException(nameof(path)); return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, null, levelSwitch, false, true, - false, null, encoding, RollingInterval.Infinite, false, null, hooks, null); + false, null, encoding, RollingInterval.Infinite, false, null, hooks, null, null); } static LoggerConfiguration ConfigureFile( @@ -513,7 +517,8 @@ static LoggerConfiguration ConfigureFile( bool rollOnFileSizeLimit, int? retainedFileCountLimit, FileLifecycleHooks? hooks, - TimeSpan? retainedFileTimeLimit) + TimeSpan? retainedFileTimeLimit, + string? dateTimeFormatFileName) { if (addSink == null) throw new ArgumentNullException(nameof(addSink)); if (formatter == null) throw new ArgumentNullException(nameof(formatter)); @@ -530,7 +535,7 @@ static LoggerConfiguration ConfigureFile( { if (rollOnFileSizeLimit || rollingInterval != RollingInterval.Infinite) { - sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, hooks, retainedFileTimeLimit); + sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, hooks, retainedFileTimeLimit, dateTimeFormatFileName); } else { diff --git a/src/Serilog.Sinks.File/Sinks/File/PathRoller.cs b/src/Serilog.Sinks.File/Sinks/File/PathRoller.cs index e6773eb..8536083 100644 --- a/src/Serilog.Sinks.File/Sinks/File/PathRoller.cs +++ b/src/Serilog.Sinks.File/Sinks/File/PathRoller.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2016 Serilog Contributors +// Copyright 2013-2016 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,16 +21,19 @@ sealed class PathRoller { const string PeriodMatchGroup = "period"; const string SequenceNumberMatchGroup = "sequence"; + const string DateTimeMatchGroup = "datetime"; readonly string _directory; readonly string _filenamePrefix; readonly string _filenameSuffix; - readonly Regex _filenameMatcher; + readonly Regex _filenameMatcher = null!; readonly RollingInterval _interval; readonly string _periodFormat; + string _dateTimeFormat = String.Empty; + public bool UseDateTimeFormat => !String.IsNullOrEmpty(_dateTimeFormat); - public PathRoller(string path, RollingInterval interval) + public PathRoller(string path, RollingInterval interval, string? dateTimeFormatFileName = null) { if (path == null) throw new ArgumentNullException(nameof(path)); _interval = interval; @@ -43,14 +46,29 @@ public PathRoller(string path, RollingInterval interval) _directory = Path.GetFullPath(pathDirectory); _filenamePrefix = Path.GetFileNameWithoutExtension(path); _filenameSuffix = Path.GetExtension(path); - _filenameMatcher = new Regex( - "^" + - Regex.Escape(_filenamePrefix) + - "(?<" + PeriodMatchGroup + ">\\d{" + _periodFormat.Length + "})" + - "(?<" + SequenceNumberMatchGroup + ">_[0-9]{3,}){0,1}" + - Regex.Escape(_filenameSuffix) + - "$", - RegexOptions.Compiled); + + if (dateTimeFormatFileName != null) + { + Regex dateTimeFormatCheck = new Regex(@"^([_\-a-zA-Z]{14,19})$"); + Match match = dateTimeFormatCheck.Match(dateTimeFormatFileName); + if (match.Groups.Count == 2) + { + _dateTimeFormat = match.Groups[1].Value; + _filenameMatcher = new Regex($@"^{Regex.Escape(_filenamePrefix)}(?<{DateTimeMatchGroup}>[\-_0-9]{{14,19}})(?<{SequenceNumberMatchGroup}>_[0-9]{{3,}})?{Regex.Escape(_filenameSuffix)}$", RegexOptions.Compiled); + } + } + + if (!UseDateTimeFormat) + { + _filenameMatcher = new Regex( + "^" + + Regex.Escape(_filenamePrefix) + + "(?<" + PeriodMatchGroup + ">\\d{" + _periodFormat.Length + "})" + + "(?<" + SequenceNumberMatchGroup + ">_[0-9]{3,}){0,1}" + + Regex.Escape(_filenameSuffix) + + "$", + RegexOptions.Compiled); + } DirectorySearchPattern = $"{_filenamePrefix}*{_filenameSuffix}"; } @@ -61,6 +79,16 @@ public PathRoller(string path, RollingInterval interval) public void GetLogFilePath(DateTime date, int? sequenceNumber, out string path) { + if (UseDateTimeFormat) + { + string seqNo = sequenceNumber != null + ? $"_{sequenceNumber.Value.ToString("000", CultureInfo.InvariantCulture)}" + : String.Empty; + + path = Path.Combine(_directory, $"{_filenamePrefix}{date.ToString(_dateTimeFormat)}{seqNo}{_filenameSuffix}"); + return; + } + var currentCheckpoint = GetCurrentCheckpoint(date); var tok = currentCheckpoint?.ToString(_periodFormat, CultureInfo.InvariantCulture) ?? ""; @@ -88,16 +116,21 @@ public IEnumerable SelectMatches(IEnumerable filenames) } DateTime? period = null; - var periodGroup = match.Groups[PeriodMatchGroup]; - if (periodGroup.Captures.Count != 0) + Group? periodGroup = null; + if (UseDateTimeFormat) + periodGroup = match.Groups[DateTimeMatchGroup]; + else + periodGroup = match.Groups[PeriodMatchGroup]; + + if (periodGroup != null && periodGroup.Captures.Count != 0) { var dateTimePart = periodGroup.Captures[0].Value; if (DateTime.TryParseExact( - dateTimePart, - _periodFormat, - CultureInfo.InvariantCulture, - DateTimeStyles.None, - out var dateTime)) + dateTimePart, + _periodFormat, + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out var dateTime)) { period = dateTime; } diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs index 93c02c5..53da9b0 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs @@ -38,6 +38,7 @@ sealed class RollingFileSink : ILogEventSink, IFlushableFileSink, IDisposable, I readonly object _syncRoot = new(); bool _isDisposed; DateTime? _nextCheckpoint; + private DateTime? _lastNow; IFileSink? _currentFile; int? _currentFileSequence; @@ -51,14 +52,15 @@ public RollingFileSink(string path, RollingInterval rollingInterval, bool rollOnFileSizeLimit, FileLifecycleHooks? hooks, - TimeSpan? retainedFileTimeLimit) + TimeSpan? retainedFileTimeLimit, + string? dateTimeFormatFileName) { if (path == null) throw new ArgumentNullException(nameof(path)); if (fileSizeLimitBytes is < 1) throw new ArgumentException("Invalid value provided; file size limit must be at least 1 byte, or null."); if (retainedFileCountLimit is < 1) throw new ArgumentException("Zero or negative value provided; retained file count limit must be at least 1."); if (retainedFileTimeLimit.HasValue && retainedFileTimeLimit < TimeSpan.Zero) throw new ArgumentException("Negative value provided; retained file time limit must be non-negative.", nameof(retainedFileTimeLimit)); - _roller = new PathRoller(path, rollingInterval); + _roller = new PathRoller(path, rollingInterval, dateTimeFormatFileName); _textFormatter = textFormatter; _fileSizeLimitBytes = fileSizeLimitBytes; _retainedFileCountLimit = retainedFileCountLimit; @@ -126,8 +128,7 @@ void AlignCurrentFileTo(DateTime now, bool nextSequence = false) void OpenFile(DateTime now, int? minSequence = null) { - var currentCheckpoint = _roller.GetCurrentCheckpoint(now); - + DateTime? currentCheckpoint = _roller.GetCurrentCheckpoint(now); _nextCheckpoint = _roller.GetNextCheckpoint(now); try @@ -146,17 +147,24 @@ void OpenFile(DateTime now, int? minSequence = null) { } - var latestForThisCheckpoint = _roller - .SelectMatches(existingFiles) - .Where(m => m.DateTime == currentCheckpoint) -#if ENUMERABLE_MAXBY - .MaxBy(m => m.SequenceNumber); -#else - .OrderByDescending(m => m.SequenceNumber) - .FirstOrDefault(); -#endif + int? sequence = GetSequenceForCurrentCheckpint(currentCheckpoint, existingFiles); + if (_roller.UseDateTimeFormat) + { + if (IsLastNowEqualNow(now, _lastNow)) + { + sequence = GetSequenceForDateTimeFormat(now, existingFiles); + if (minSequence == null) + minSequence = 1; + } + else + { + sequence = null; + minSequence = null; + } + + _lastNow = now; + } - var sequence = latestForThisCheckpoint?.SequenceNumber; if (minSequence != null) { if (sequence == null || sequence.Value < minSequence.Value) @@ -280,6 +288,46 @@ bool ShouldRetainFile(RollingLogFile file, int index, DateTime now) return true; } + int? GetSequenceForDateTimeFormat(DateTime now, IEnumerable existingFiles) + { + var match = _roller + .SelectMatches(existingFiles). + Where(m => m.DateTime == now) +#if ENUMERABLE_MAXBY + .MaxBy(m => m.SequenceNumber); +#else + .OrderByDescending(m => m.SequenceNumber) + .FirstOrDefault(); +#endif + return match?.SequenceNumber; + } + + int? GetSequenceForCurrentCheckpint(DateTime? currentCheckpoint, IEnumerable existingFiles) + { + var latestForThisCheckpoint = _roller + .SelectMatches(existingFiles) + .Where(m => m.DateTime == currentCheckpoint) +#if ENUMERABLE_MAXBY + .MaxBy(m => m.SequenceNumber); +#else + .OrderByDescending(m => m.SequenceNumber) + .FirstOrDefault(); +#endif + + return latestForThisCheckpoint?.SequenceNumber; + } + + bool IsLastNowEqualNow(DateTime now, DateTime? lastNow) + { + if (lastNow == null) + return false; + + DateTime ln = lastNow.Value; + + return now.Year == ln.Year && now.Month == ln.Month && now.Day == ln.Day && + now.Hour == ln.Hour && now.Minute == ln.Minute && now.Second == ln.Second; + } + public void Dispose() { lock (_syncRoot) diff --git a/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs b/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs index e34c335..486dd55 100644 --- a/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs +++ b/test/Serilog.Sinks.File.Tests/TemplatedPathRollerTests.cs @@ -1,4 +1,4 @@ -using Xunit; +using Xunit; namespace Serilog.Sinks.File.Tests; @@ -92,5 +92,49 @@ public void MatchingParsesSubstitutions(string template, string newer, string ol var matched = roller.SelectMatches(new[] { older, newer }).OrderByDescending(m => m.DateTime).Select(m => m.Filename).ToArray(); Assert.Equal(new[] { newer, older }, matched); } + + [Fact] + public void MatchingSelectsFilesDateTime() + { + var roller = new PathRoller("log-.txt", RollingInterval.Infinite, "yyyy_MM_dd-HH_mm_ss"); + var matched = roller.SelectMatches(new[] { "log-2025_06_17-10_05_23.txt" }).ToArray(); + Assert.Single(matched); + Assert.Null(matched[0].SequenceNumber); + Assert.Null(matched[0].DateTime); + } + + [Fact] + public void TheDirectorSearchPatternUsesWildcardInPlaceOfDateTime() + { + var roller = new PathRoller("log-.txt", RollingInterval.Infinite, "yyyy_MM_dd-HH_mm_ss"); + Assert.Equal("log-*.txt", roller.DirectorySearchPattern); + } + + [Fact] + public void GetNewFilePathWithDateTimeInTheFileName() + { + var roller = new PathRoller(Path.Combine("Logs", "log-.txt"), RollingInterval.Infinite, "yyyy_MM_dd-HH_mm_ss"); + var now = new DateTime(2013, 7, 14, 3, 24, 9); + roller.GetLogFilePath(now, null, out var path); + AssertEqualAbsolute(Path.Combine("Logs", "log-2013_07_14-03_24_09.txt"), path); + } + + [Fact] + public void GetNewFilePathWithDateTimeInTheFileNameWithoutSeparator() + { + var roller = new PathRoller(Path.Combine("Logs", "log-.txt"), RollingInterval.Infinite, "yyyyMMddHHmmss"); + var now = new DateTime(2025, 6, 18, 3, 24, 9); + roller.GetLogFilePath(now, null, out var path); + AssertEqualAbsolute(Path.Combine("Logs", "log-20250618032409.txt"), path); + } + + [Fact] + public void GetNewFilePathWithDateTimeAndSequenceInTheFileNameWithoutSeparator() + { + var roller = new PathRoller(Path.Combine("Logs", "log-.txt"), RollingInterval.Infinite, "yyyyMMddHHmmss"); + var now = new DateTime(2025, 6, 18, 3, 24, 9); + roller.GetLogFilePath(now, 3, out var path); + AssertEqualAbsolute(Path.Combine("Logs", "log-20250618032409_003.txt"), path); + } } From 72c2b1a95dc390c994d106ecbd7e742357e50d29 Mon Sep 17 00:00:00 2001 From: Alexander Haugg Date: Fri, 20 Jun 2025 17:21:30 +0200 Subject: [PATCH 2/2] finalization added --- README.md | 22 ++++++ .../FileLoggerConfigurationExtensions.cs | 28 +++++++- .../FileLoggerConfigurationExtensionsTests.cs | 71 +++++++++++++++++++ 3 files changed, 119 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1feed22..81bcc4e 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,28 @@ Specifying both `rollingInterval` and `rollOnFileSizeLimit` will cause both poli Old files will be cleaned up as per `retainedFileCountLimit` - the default is 31. +### File name with creation timestamp + +For the `fileSizeLimitBytes` parameter, the `dateTimeFormatFileName` parameter can also be set to obtain the creation timestamp in the file name. +Example of possible formats: +- yyyy-MM-dd_HH-mm-ss +- yyyyMMddHHmmss +- dd_MM_yyyy-HH_mm_ss +- ... + +```csharp + .WriteTo.File("log-.txt", fileSizeLimitBytes: 1024, dateTimeFormatFileName: "yyyy-MM-dd_HH-mm-ss") +``` + +If the file limit occurs in the same second as the last file was created, a counter is appended. +This will create a file set like: + +``` +log-2025-06-20_17-16-35.txt +log-2025-06-20_20-43-16.txt +log-2025-06-20_20-43-16_001.txt +``` + ### XML `` configuration To use the file sink with the [Serilog.Settings.AppSettings](https://github.com/serilog/serilog-settings-appsettings) package, first install that package if you haven't already done so: diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs index b9f074d..1258d17 100644 --- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs @@ -14,6 +14,7 @@ using System.ComponentModel; using System.Text; +using System.Text.RegularExpressions; using Serilog.Configuration; using Serilog.Core; using Serilog.Debugging; @@ -33,6 +34,7 @@ public static class FileLoggerConfigurationExtensions const int DefaultRetainedFileCountLimit = 31; // A long month of logs const long DefaultFileSizeLimitBytes = 1L * 1024 * 1024 * 1024; // 1GB const string DefaultOutputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"; + private static readonly Regex ValidateDateTimeFormatFileName = new Regex("^(?(?:y{4}[_-]?M{2}[_-]?d{2})|(?:d{2}[_-]?M{2}[_-]?y{4}))[_-]?(?