Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 54 additions & 8 deletions src/Serilog.Sinks.File/Sinks/File/PathRoller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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);
}
Expand All @@ -93,11 +105,11 @@ public IEnumerable<RollingLogFile> SelectMatches(IEnumerable<string> 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;
}
Expand All @@ -110,4 +122,38 @@ public IEnumerable<RollingLogFile> SelectMatches(IEnumerable<string> 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);
}
}
20 changes: 20 additions & 0 deletions src/Serilog.Sinks.File/Sinks/PathPatternType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Serilog.Sinks;

/// <summary>
/// Specifies the pattern in log file path (if any)
/// </summary>
public enum PathPatternType
{
/// <summary>
/// The path has a interval pattern in it
/// </summary>
Interval,
/// <summary>
/// The path has a sequence number pattern in it
/// </summary>
SequenceNumber,
/// <summary>
/// The path matches both of the patterns
/// </summary>
Both
}
67 changes: 49 additions & 18 deletions test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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() });
}

Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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)),
Expand Down Expand Up @@ -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)),
Expand Down Expand Up @@ -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)),
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down