diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs
index 490803d..ad6b80b 100644
--- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs
+++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs
@@ -165,7 +165,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.
@@ -181,7 +181,7 @@ public static LoggerConfiguration File(
/// Allow the log file to be shared by multiple processes. The default is false.
/// If provided, a full disk flush will be performed periodically at the specified interval.
/// The interval at which logging will roll over to a new file.
- /// If true
, a new file will be created when the file size limit is reached. Filenames
+ /// If true
, a new file will be created when the file size limit is reached. Filenames
/// will have a number appended in the format _NNN
, with the first filename given no number.
/// The maximum number of log files that will be retained,
/// including the current log file. For unlimited retention, pass null. The default is 31.
@@ -227,12 +227,16 @@ public static LoggerConfiguration File(
/// Allow the log file to be shared by multiple processes. The default is false.
/// If provided, a full disk flush will be performed periodically at the specified interval.
/// The interval at which logging will roll over to a new file.
- /// If true
, a new file will be created when the file size limit is reached. Filenames
+ /// If true
, a new file will be created when the file size limit is reached. Filenames
/// will have a number appended in the format _NNN
, with the first filename given no number.
/// The maximum number of log files that will be retained,
/// including the current log file. For unlimited retention, pass null. The default is 31.
/// Character encoding used to write the text file. The default is UTF-8 without BOM.
/// Optionally enables hooking into log file lifecycle events.
+ /// The maximum time after the end of an interval that a rolling log file will be retained.
+ /// Must be greater than or equal to .
+ /// Ignored if is .
+ /// The default is to retain files indefinitely.
/// Configuration object allowing method chaining.
public static LoggerConfiguration File(
this LoggerSinkConfiguration sinkConfiguration,
@@ -249,7 +253,8 @@ public static LoggerConfiguration File(
bool rollOnFileSizeLimit = false,
int? retainedFileCountLimit = DefaultRetainedFileCountLimit,
Encoding encoding = null,
- FileLifecycleHooks hooks = null)
+ FileLifecycleHooks hooks = null,
+ TimeSpan? retainedFileTimeLimit = null)
{
if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration));
if (path == null) throw new ArgumentNullException(nameof(path));
@@ -258,7 +263,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);
+ rollingInterval, rollOnFileSizeLimit, retainedFileCountLimit, encoding, hooks, retainedFileTimeLimit);
}
///
@@ -267,7 +272,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.
@@ -289,6 +294,10 @@ public static LoggerConfiguration File(
/// including the current log file. For unlimited retention, pass null. The default is 31.
/// Character encoding used to write the text file. The default is UTF-8 without BOM.
/// Optionally enables hooking into log file lifecycle events.
+ /// The maximum time after the end of an interval that a rolling log file will be retained.
+ /// Must be greater than or equal to .
+ /// Ignored if is .
+ /// The default is to retain files indefinitely.
/// Configuration object allowing method chaining.
public static LoggerConfiguration File(
this LoggerSinkConfiguration sinkConfiguration,
@@ -304,7 +313,8 @@ public static LoggerConfiguration File(
bool rollOnFileSizeLimit = false,
int? retainedFileCountLimit = DefaultRetainedFileCountLimit,
Encoding encoding = null,
- FileLifecycleHooks hooks = null)
+ FileLifecycleHooks hooks = null,
+ TimeSpan? retainedFileTimeLimit = null)
{
if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration));
if (formatter == null) throw new ArgumentNullException(nameof(formatter));
@@ -312,7 +322,7 @@ public static LoggerConfiguration File(
return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch,
buffered, false, shared, flushToDiskInterval, encoding, rollingInterval, rollOnFileSizeLimit,
- retainedFileCountLimit, hooks);
+ retainedFileCountLimit, hooks, retainedFileTimeLimit);
}
///
@@ -432,7 +442,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);
+ false, null, encoding, RollingInterval.Infinite, false, null, hooks, null);
}
static LoggerConfiguration ConfigureFile(
@@ -450,13 +460,15 @@ static LoggerConfiguration ConfigureFile(
RollingInterval rollingInterval,
bool rollOnFileSizeLimit,
int? retainedFileCountLimit,
- FileLifecycleHooks hooks)
+ FileLifecycleHooks hooks,
+ TimeSpan? retainedFileTimeLimit)
{
if (addSink == null) throw new ArgumentNullException(nameof(addSink));
if (formatter == null) throw new ArgumentNullException(nameof(formatter));
if (path == null) throw new ArgumentNullException(nameof(path));
if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative.", nameof(fileSizeLimitBytes));
if (retainedFileCountLimit.HasValue && retainedFileCountLimit < 1) throw new ArgumentException("At least one file must be retained.", nameof(retainedFileCountLimit));
+ if (retainedFileTimeLimit.HasValue && retainedFileTimeLimit < TimeSpan.Zero) throw new ArgumentException("Negative value provided; retained file time limit must be non-negative.", nameof(retainedFileTimeLimit));
if (shared && buffered) throw new ArgumentException("Buffered writes are not available when file sharing is enabled.", nameof(buffered));
if (shared && hooks != null) throw new ArgumentException("File lifecycle hooks are not currently supported for shared log files.", nameof(hooks));
@@ -464,7 +476,7 @@ static LoggerConfiguration ConfigureFile(
if (rollOnFileSizeLimit || rollingInterval != RollingInterval.Infinite)
{
- sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, hooks);
+ sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, hooks, retainedFileTimeLimit);
}
else
{
diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs
index 43e5fad..e6d29d5 100644
--- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs
+++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs
@@ -20,6 +20,7 @@
using Serilog.Debugging;
using Serilog.Events;
using Serilog.Formatting;
+using System.Collections.Generic;
namespace Serilog.Sinks.File
{
@@ -29,6 +30,7 @@ sealed class RollingFileSink : ILogEventSink, IFlushableFileSink, IDisposable
readonly ITextFormatter _textFormatter;
readonly long? _fileSizeLimitBytes;
readonly int? _retainedFileCountLimit;
+ readonly TimeSpan? _retainedFileTimeLimit;
readonly Encoding _encoding;
readonly bool _buffered;
readonly bool _shared;
@@ -50,16 +52,19 @@ public RollingFileSink(string path,
bool shared,
RollingInterval rollingInterval,
bool rollOnFileSizeLimit,
- FileLifecycleHooks hooks)
+ FileLifecycleHooks hooks,
+ TimeSpan? retainedFileTimeLimit)
{
if (path == null) throw new ArgumentNullException(nameof(path));
- if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative.");
- if (retainedFileCountLimit.HasValue && retainedFileCountLimit < 1) throw new ArgumentException("Zero or negative value provided; retained file count limit must be at least 1.");
+ if (fileSizeLimitBytes.HasValue && fileSizeLimitBytes < 0) throw new ArgumentException("Negative value provided; file size limit must be non-negative");
+ if (retainedFileCountLimit.HasValue && retainedFileCountLimit < 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);
_textFormatter = textFormatter;
_fileSizeLimitBytes = fileSizeLimitBytes;
_retainedFileCountLimit = retainedFileCountLimit;
+ _retainedFileTimeLimit = retainedFileTimeLimit;
_encoding = encoding;
_buffered = buffered;
_shared = shared;
@@ -166,14 +171,14 @@ void OpenFile(DateTime now, int? minSequence = null)
throw;
}
- ApplyRetentionPolicy(path);
+ ApplyRetentionPolicy(path, now);
return;
}
}
- void ApplyRetentionPolicy(string currentFilePath)
+ void ApplyRetentionPolicy(string currentFilePath, DateTime now)
{
- if (_retainedFileCountLimit == null) return;
+ if (_retainedFileCountLimit == null && _retainedFileTimeLimit == null) return;
var currentFileName = Path.GetFileName(currentFilePath);
@@ -181,17 +186,17 @@ void ApplyRetentionPolicy(string currentFilePath)
// because files are only opened on response to an event being processed.
var potentialMatches = Directory.GetFiles(_roller.LogFileDirectory, _roller.DirectorySearchPattern)
.Select(Path.GetFileName)
- .Union(new [] { currentFileName });
+ .Union(new[] { currentFileName });
var newestFirst = _roller
.SelectMatches(potentialMatches)
.OrderByDescending(m => m.DateTime)
- .ThenByDescending(m => m.SequenceNumber)
- .Select(m => m.Filename);
+ .ThenByDescending(m => m.SequenceNumber);
var toRemove = newestFirst
- .Where(n => StringComparer.OrdinalIgnoreCase.Compare(currentFileName, n) != 0)
- .Skip(_retainedFileCountLimit.Value - 1)
+ .Where(n => StringComparer.OrdinalIgnoreCase.Compare(currentFileName, n.Filename) != 0)
+ .SkipWhile((f, i) => ShouldRetainFile(f, i, now))
+ .Select(x => x.Filename)
.ToList();
foreach (var obsolete in toRemove)
@@ -209,6 +214,20 @@ void ApplyRetentionPolicy(string currentFilePath)
}
}
+ bool ShouldRetainFile(RollingLogFile file, int index, DateTime now)
+ {
+ if (_retainedFileCountLimit.HasValue && index >= _retainedFileCountLimit.Value)
+ return false;
+
+ if (_retainedFileTimeLimit.HasValue && file.DateTime.HasValue &&
+ file.DateTime.Value < now.Subtract(_retainedFileTimeLimit.Value))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
public void Dispose()
{
lock (_syncRoot)
diff --git a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs
index 70408b3..5d0879f 100644
--- a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs
+++ b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
@@ -71,6 +71,63 @@ public void WhenRetentionCountIsSetOldFilesAreDeleted()
});
}
+ [Fact]
+ public void WhenRetentionTimeIsSetOldFilesAreDeleted()
+ {
+ LogEvent e1 = Some.InformationEvent(DateTime.Today.AddDays(-5)),
+ e2 = Some.InformationEvent(e1.Timestamp.AddDays(2)),
+ e3 = Some.InformationEvent(e2.Timestamp.AddDays(5));
+
+ TestRollingEventSequence(
+ (pf, wt) => wt.File(pf, retainedFileTimeLimit: TimeSpan.FromDays(1), rollingInterval: RollingInterval.Day),
+ new[] {e1, e2, e3},
+ files =>
+ {
+ Assert.Equal(3, files.Count);
+ Assert.True(!System.IO.File.Exists(files[0]));
+ Assert.True(!System.IO.File.Exists(files[1]));
+ Assert.True(System.IO.File.Exists(files[2]));
+ });
+ }
+
+ [Fact]
+ public void WhenRetentionCountAndTimeIsSetOldFilesAreDeletedByTime()
+ {
+ LogEvent e1 = Some.InformationEvent(DateTime.Today.AddDays(-5)),
+ e2 = Some.InformationEvent(e1.Timestamp.AddDays(2)),
+ 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},
+ files =>
+ {
+ Assert.Equal(3, files.Count);
+ Assert.True(!System.IO.File.Exists(files[0]));
+ Assert.True(!System.IO.File.Exists(files[1]));
+ Assert.True(System.IO.File.Exists(files[2]));
+ });
+ }
+
+ [Fact]
+ public void WhenRetentionCountAndTimeIsSetOldFilesAreDeletedByCount()
+ {
+ LogEvent e1 = Some.InformationEvent(DateTime.Today.AddDays(-5)),
+ e2 = Some.InformationEvent(e1.Timestamp.AddDays(2)),
+ 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},
+ files =>
+ {
+ Assert.Equal(3, files.Count);
+ Assert.True(!System.IO.File.Exists(files[0]));
+ Assert.True(System.IO.File.Exists(files[1]));
+ Assert.True(System.IO.File.Exists(files[2]));
+ });
+ }
+
[Fact]
public void WhenRetentionCountAndArchivingHookIsSetOldFilesAreCopiedAndOriginalDeleted()
{
@@ -88,7 +145,6 @@ public void WhenRetentionCountAndArchivingHookIsSetOldFilesAreCopiedAndOriginalD
Assert.True(!System.IO.File.Exists(files[0]));
Assert.True(System.IO.File.Exists(files[1]));
Assert.True(System.IO.File.Exists(files[2]));
-
Assert.True(System.IO.File.Exists(ArchiveOldLogsHook.AddTopDirectory(files[0], archiveDirectory)));
});
}