diff --git a/src/Serilog.Sinks.File/Sinks/File/PathRoller.cs b/src/Serilog.Sinks.File/Sinks/File/PathRoller.cs index e6773eb..9e5fb82 100644 --- a/src/Serilog.Sinks.File/Sinks/File/PathRoller.cs +++ b/src/Serilog.Sinks.File/Sinks/File/PathRoller.cs @@ -21,6 +21,9 @@ sealed class PathRoller { const string PeriodMatchGroup = "period"; const string SequenceNumberMatchGroup = "sequence"; + const string IntervalPathPatternMatcher = @"{interval}"; + const string SequenceNumberPathPatternMatcher = @"{sequence_number}"; + readonly string _directory; readonly string _filenamePrefix; @@ -29,11 +32,12 @@ sealed class PathRoller readonly RollingInterval _interval; readonly string _periodFormat; + readonly string _originalPath; public PathRoller(string path, RollingInterval interval) { - if (path == null) throw new ArgumentNullException(nameof(path)); _interval = interval; + _originalPath = path ?? throw new ArgumentNullException(nameof(path)); _periodFormat = interval.GetFormat(); var pathDirectory = Path.GetDirectoryName(path); @@ -64,9 +68,17 @@ public void GetLogFilePath(DateTime date, int? sequenceNumber, out string path) var currentCheckpoint = GetCurrentCheckpoint(date); var tok = currentCheckpoint?.ToString(_periodFormat, CultureInfo.InvariantCulture) ?? ""; + var sequenceNumberFormatted = sequenceNumber.HasValue + ? sequenceNumber.Value.ToString("000", CultureInfo.InvariantCulture) + : null; + if (sequenceNumberFormatted != null) + tok += "_" + sequenceNumberFormatted; - if (sequenceNumber != null) - tok += "_" + sequenceNumber.Value.ToString("000", CultureInfo.InvariantCulture); + if (TryGetPatternMatch(_originalPath, out var pattern)) + { + path = GetPathForPattern(tok, sequenceNumberFormatted ?? ""); + return; + } path = Path.Combine(_directory, _filenamePrefix + tok + _filenameSuffix); } @@ -93,11 +105,11 @@ public IEnumerable SelectMatches(IEnumerable filenames) { 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; } @@ -110,4 +122,38 @@ public IEnumerable SelectMatches(IEnumerable filenames) public DateTime? GetCurrentCheckpoint(DateTime instant) => _interval.GetCurrentCheckpoint(instant); public DateTime? GetNextCheckpoint(DateTime instant) => _interval.GetNextCheckpoint(instant); + + public bool TryGetPatternMatch(string path, out PathPatternType? patternType) + { + patternType = null; + if (path.Contains(IntervalPathPatternMatcher) && path.Contains(SequenceNumberPathPatternMatcher)) + { + patternType = PathPatternType.Both; + return true; + } + + if (path.Contains(IntervalPathPatternMatcher)) + { + patternType = PathPatternType.Interval; + return true; + } + + if (path.Contains(SequenceNumberPathPatternMatcher)) + { + patternType = PathPatternType.SequenceNumber; + return true; + } + + return false; + } + + private string GetPathForPattern(string intervalToken, + string sequenceNumber) + { + var newPrefix = _filenamePrefix.Replace(IntervalPathPatternMatcher, intervalToken).Replace( + SequenceNumberPathPatternMatcher, + sequenceNumber); + + return Path.Combine(_directory, newPrefix + _filenameSuffix); + } } diff --git a/src/Serilog.Sinks.File/Sinks/PathPatternType.cs b/src/Serilog.Sinks.File/Sinks/PathPatternType.cs new file mode 100644 index 0000000..3f5ff11 --- /dev/null +++ b/src/Serilog.Sinks.File/Sinks/PathPatternType.cs @@ -0,0 +1,20 @@ +namespace Serilog.Sinks; + +/// +/// Specifies the pattern in log file path (if any) +/// +public enum PathPatternType +{ + /// + /// The path has a interval pattern in it + /// + Interval, + /// + /// The path has a sequence number pattern in it + /// + SequenceNumber, + /// + /// The path matches both of the patterns + /// + Both +} diff --git a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs index 191e614..3119c64 100644 --- a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs @@ -50,7 +50,8 @@ public void EventsAreWrittenWhenDiskFlushingIsEnabled() { // Doesn't test flushing, but ensures we haven't broken basic logging TestRollingEventSequence( - (pf, wt) => wt.File(pf, flushToDiskInterval: TimeSpan.FromMilliseconds(50), rollingInterval: RollingInterval.Day), + (pf, wt) => wt.File(pf, flushToDiskInterval: TimeSpan.FromMilliseconds(50), + rollingInterval: RollingInterval.Day), new[] { Some.InformationEvent() }); } @@ -71,7 +72,7 @@ public void WhenRetentionCountIsSetOldFilesAreDeleted() TestRollingEventSequence( (pf, wt) => wt.File(pf, retainedFileCountLimit: 2, rollingInterval: RollingInterval.Day), - new[] {e1, e2, e3}, + new[] { e1, e2, e3 }, files => { Assert.Equal(3, files.Count); @@ -90,7 +91,7 @@ public void WhenRetentionTimeIsSetOldFilesAreDeleted() TestRollingEventSequence( (pf, wt) => wt.File(pf, retainedFileTimeLimit: TimeSpan.FromDays(1), rollingInterval: RollingInterval.Day), - new[] {e1, e2, e3}, + new[] { e1, e2, e3 }, files => { Assert.Equal(3, files.Count); @@ -108,8 +109,9 @@ public void WhenRetentionCountAndTimeIsSetOldFilesAreDeletedByTime() e3 = Some.InformationEvent(e2.Timestamp.AddDays(5)); TestRollingEventSequence( - (pf, wt) => wt.File(pf, retainedFileCountLimit: 2, retainedFileTimeLimit: TimeSpan.FromDays(1), rollingInterval: RollingInterval.Day), - new[] {e1, e2, e3}, + (pf, wt) => wt.File(pf, retainedFileCountLimit: 2, retainedFileTimeLimit: TimeSpan.FromDays(1), + rollingInterval: RollingInterval.Day), + new[] { e1, e2, e3 }, files => { Assert.Equal(3, files.Count); @@ -127,8 +129,9 @@ public void WhenRetentionCountAndTimeIsSetOldFilesAreDeletedByCount() e3 = Some.InformationEvent(e2.Timestamp.AddDays(5)); TestRollingEventSequence( - (pf, wt) => wt.File(pf, retainedFileCountLimit: 2, retainedFileTimeLimit: TimeSpan.FromDays(10), rollingInterval: RollingInterval.Day), - new[] {e1, e2, e3}, + (pf, wt) => wt.File(pf, retainedFileCountLimit: 2, retainedFileTimeLimit: TimeSpan.FromDays(10), + rollingInterval: RollingInterval.Day), + new[] { e1, e2, e3 }, files => { Assert.Equal(3, files.Count); @@ -143,12 +146,13 @@ public void WhenRetentionCountAndArchivingHookIsSetOldFilesAreCopiedAndOriginalD { const string archiveDirectory = "OldLogs"; LogEvent e1 = Some.InformationEvent(), - e2 = Some.InformationEvent(e1.Timestamp.AddDays(1)), - e3 = Some.InformationEvent(e2.Timestamp.AddDays(5)); + e2 = Some.InformationEvent(e1.Timestamp.AddDays(1)), + e3 = Some.InformationEvent(e2.Timestamp.AddDays(5)); TestRollingEventSequence( - (pf, wt) => wt.File(pf, retainedFileCountLimit: 2, rollingInterval: RollingInterval.Day, hooks: new ArchiveOldLogsHook(archiveDirectory)), - new[] {e1, e2, e3}, + (pf, wt) => wt.File(pf, retainedFileCountLimit: 2, rollingInterval: RollingInterval.Day, + hooks: new ArchiveOldLogsHook(archiveDirectory)), + new[] { e1, e2, e3 }, files => { Assert.Equal(3, files.Count); @@ -165,7 +169,8 @@ public void WhenFirstOpeningFailedWithLockRetryDelayedUntilNextCheckpoint() var fileName = Some.String() + ".txt"; using var temp = new TempFolder(); using var log = new LoggerConfiguration() - .WriteTo.File(Path.Combine(temp.Path, fileName), rollOnFileSizeLimit: true, fileSizeLimitBytes: 1, rollingInterval: RollingInterval.Minute, hooks: new FailOpeningHook(true, 2, 3, 4)) + .WriteTo.File(Path.Combine(temp.Path, fileName), rollOnFileSizeLimit: true, fileSizeLimitBytes: 1, + rollingInterval: RollingInterval.Minute, hooks: new FailOpeningHook(true, 2, 3, 4)) .CreateLogger(); LogEvent e1 = Some.InformationEvent(new DateTime(2012, 10, 28)), e2 = Some.InformationEvent(e1.Timestamp.AddSeconds(1)), @@ -203,7 +208,8 @@ public void WhenFirstOpeningFailedWithLockRetryDelayed30Minutes() var fileName = Some.String() + ".txt"; using var temp = new TempFolder(); using var log = new LoggerConfiguration() - .WriteTo.File(Path.Combine(temp.Path, fileName), rollOnFileSizeLimit: true, fileSizeLimitBytes: 1, rollingInterval: RollingInterval.Hour, hooks: new FailOpeningHook(true, 2, 3, 4)) + .WriteTo.File(Path.Combine(temp.Path, fileName), rollOnFileSizeLimit: true, fileSizeLimitBytes: 1, + rollingInterval: RollingInterval.Hour, hooks: new FailOpeningHook(true, 2, 3, 4)) .CreateLogger(); LogEvent e1 = Some.InformationEvent(new DateTime(2012, 10, 28)), e2 = Some.InformationEvent(e1.Timestamp.AddSeconds(1)), @@ -240,7 +246,8 @@ public void WhenFirstOpeningFailedWithoutLockRetryDelayed30Minutes() var fileName = Some.String() + ".txt"; using var temp = new TempFolder(); using var log = new LoggerConfiguration() - .WriteTo.File(Path.Combine(temp.Path, fileName), rollOnFileSizeLimit: true, fileSizeLimitBytes: 1, rollingInterval: RollingInterval.Hour, hooks: new FailOpeningHook(false, 2)) + .WriteTo.File(Path.Combine(temp.Path, fileName), rollOnFileSizeLimit: true, fileSizeLimitBytes: 1, + rollingInterval: RollingInterval.Hour, hooks: new FailOpeningHook(false, 2)) .CreateLogger(); LogEvent e1 = Some.InformationEvent(new DateTime(2012, 10, 28)), e2 = Some.InformationEvent(e1.Timestamp.AddSeconds(1)), @@ -281,7 +288,9 @@ public void WhenSizeLimitIsBreachedNewFilesCreated() e2 = Some.InformationEvent(e1.Timestamp), e3 = Some.InformationEvent(e1.Timestamp); - log.Write(e1); log.Write(e2); log.Write(e3); + log.Write(e1); + log.Write(e2); + log.Write(e3); var files = Directory.GetFiles(temp.Path) .OrderBy(p => p, StringComparer.OrdinalIgnoreCase) @@ -309,10 +318,10 @@ public void WhenStreamWrapperSpecifiedIsUsedForRolledFiles() }; using (var log = new LoggerConfiguration() - .WriteTo.File(Path.Combine(temp.Path, fileName), rollOnFileSizeLimit: true, fileSizeLimitBytes: 1, hooks: gzipWrapper) - .CreateLogger()) + .WriteTo.File(Path.Combine(temp.Path, fileName), rollOnFileSizeLimit: true, fileSizeLimitBytes: 1, + hooks: gzipWrapper) + .CreateLogger()) { - foreach (var logEvent in logEvents) { log.Write(logEvent); @@ -374,6 +383,28 @@ public void IfTheLogFolderDoesNotExistItWillBeCreated() } } + + [Fact] + public void IfTheLogPathContainsPlaceholdersTheyWillBeReplacedWithActualValues() + { + var path = "logs/my_log_file_{interval}_{sequence_number}_formatted"; + var expectedPath = $"logs/my_log_file_{DateTimeOffset.Now:yyyyMMddHH}__formatted"; + Logger? log = null; + try + { + log = new LoggerConfiguration().WriteTo.File(path,rollingInterval:RollingInterval.Hour).CreateLogger(); + log.Write(Some.InformationEvent()); + log.Write(Some.LogEvent(DateTimeOffset.Now,LogEventLevel.Warning)); + log.Warning(expectedPath); + Assert.True(System.IO.File.Exists(expectedPath)); + } + finally + { + log?.Dispose(); + System.IO.File.Delete(expectedPath); + } + } + static void TestRollingEventSequence(params LogEvent[] events) { TestRollingEventSequence(