Skip to content

Add option to retain files based on age #17

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ For the same reason, only **the most recent 31 files** are retained by default (
.WriteTo.RollingFile("log-{Date}.txt", retainedFileCountLimit: null)
```

In addition, it is possible to set a maximum age for the log files that will be retained. To set this limit, pass the `retainedFileAgeLimit` parameter.

```csharp
.WriteTo.RollingFile("log-{Date}.txt", retainedFileAgeLimit: TimeSpan.FromDays(31))
```
Note that this will be applied *after* the limit set by `retainedFileCountLimit`. To ignore the file count limit and only use file age, `retainedFileCountLimit` should be set to `null`.

### XML `<appSettings>` configuration

To use the rolling 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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ public static class RollingFileLoggerConfigurationExtensions
/// <param name="buffered">Indicates if flushing to the output file can be buffered or not. The default
/// is false.</param>
/// <param name="shared">Allow the log files to be shared by multiple processes. The default is false.</param>
/// <param name="retainedFileAgeLimit">The maximum age of log files that will be retained,
/// including the current log file. For unlimited retention, pass null (default).
/// This will be applied after <paramref name="retainedFileCountLimit"/>.</param>
/// <returns>Configuration object allowing method chaining.</returns>
/// <remarks>The file will be written using the UTF-8 encoding without a byte-order mark.</remarks>
public static LoggerConfiguration RollingFile(
Expand All @@ -66,11 +69,12 @@ public static LoggerConfiguration RollingFile(
int? retainedFileCountLimit = DefaultRetainedFileCountLimit,
LoggingLevelSwitch levelSwitch = null,
bool buffered = false,
bool shared = false)
bool shared = false,
TimeSpan? retainedFileAgeLimit = null)
{
var formatter = new MessageTemplateTextFormatter(outputTemplate, formatProvider);
return RollingFile(sinkConfiguration, formatter, pathFormat, restrictedToMinimumLevel, fileSizeLimitBytes,
retainedFileCountLimit, levelSwitch, buffered, shared);
retainedFileCountLimit, levelSwitch, buffered, shared, retainedFileAgeLimit);
}

/// <summary>
Expand All @@ -95,6 +99,9 @@ public static LoggerConfiguration RollingFile(
/// <param name="buffered">Indicates if flushing to the output file can be buffered or not. The default
/// is false.</param>
/// <param name="shared">Allow the log files to be shared by multiple processes. The default is false.</param>
/// <param name="retainedFileAgeLimit">The maximum age of log files that will be retained,
/// including the current log file. For unlimited retention, pass null (default).
/// This will be applied after <paramref name="retainedFileCountLimit"/>.</param>
/// <returns>Configuration object allowing method chaining.</returns>
/// <remarks>The file will be written using the UTF-8 encoding without a byte-order mark.</remarks>
public static LoggerConfiguration RollingFile(
Expand All @@ -106,15 +113,13 @@ public static LoggerConfiguration RollingFile(
int? retainedFileCountLimit = DefaultRetainedFileCountLimit,
LoggingLevelSwitch levelSwitch = null,
bool buffered = false,
bool shared = false)
bool shared = false,
TimeSpan? retainedFileAgeLimit = null)
{
if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration));
if (formatter == null) throw new ArgumentNullException(nameof(formatter));

if (shared && buffered)
throw new ArgumentException("Buffered writes are not available when file sharing is enabled.", nameof(buffered));

var sink = new RollingFileSink(pathFormat, formatter, fileSizeLimitBytes, retainedFileCountLimit, buffered: buffered, shared: shared);
var sink = new RollingFileSink(pathFormat, formatter, fileSizeLimitBytes, retainedFileCountLimit,
buffered: buffered, shared: shared, retainedFileAgeLimit: retainedFileAgeLimit);
return sinkConfiguration.Sink(sink, restrictedToMinimumLevel, levelSwitch);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// 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.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.IO;
using System.Linq;
using Serilog.Debugging;

namespace Serilog.Sinks.RollingFile.RetentionPolicies
{
internal class FileAgeRetentionPolicy : IRetentionPolicy
{
private readonly TemplatedPathRoller _roller;
private readonly TimeSpan _retainedFileAgeLimit;

public FileAgeRetentionPolicy(TemplatedPathRoller roller, TimeSpan retainedFileAgeLimit)
{
if (roller == null)
throw new ArgumentNullException(nameof(roller));

if (retainedFileAgeLimit <= TimeSpan.Zero)
throw new ArgumentException("Zero or negative value provided; retained file age limit must be a positive timespan");

_roller = roller;
_retainedFileAgeLimit = retainedFileAgeLimit;
}

public void Apply(string currentFilePath)
{
var currentFileName = Path.GetFileName(currentFilePath);

// We consider the current file to exist, even if nothing's been written yet,
// 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 });

var newestFirst = _roller
.SelectMatches(potentialMatches)
.OrderByDescending(m => m.Date)
.ThenByDescending(m => m.SequenceNumber)
.Select(m => new { m.Filename, m.Date });

var maxAge = DateTimeOffset.Now - _retainedFileAgeLimit;

var toRemove = newestFirst
.Where(f => StringComparer.OrdinalIgnoreCase.Compare(currentFileName, f.Filename) != 0
&& f.Date < maxAge)
.Select(f => f.Filename)
.ToList();

foreach (var obsolete in toRemove)
{
var fullPath = Path.Combine(_roller.LogFileDirectory, obsolete);
try
{
System.IO.File.Delete(fullPath);
}
catch (Exception ex)
{
SelfLog.WriteLine("Error {0} while removing obsolete file {1}", ex, fullPath);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// 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.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using Serilog.Debugging;
using System;
using System.IO;
using System.Linq;

namespace Serilog.Sinks.RollingFile.RetentionPolicies
{
internal class FileCountRetentionPolicy : IRetentionPolicy
{
private readonly TemplatedPathRoller _roller;
private readonly int? _retainedFileCountLimit;

public FileCountRetentionPolicy(TemplatedPathRoller roller, int? retainedFileCountLimit)
{
if (roller == null)
throw new ArgumentNullException(nameof(roller));

if (retainedFileCountLimit.HasValue && retainedFileCountLimit < 1)
throw new ArgumentException("Zero or negative value provided; retained file count limit must be at least 1");

_roller = roller;
_retainedFileCountLimit = retainedFileCountLimit;
}

public void Apply(string currentFilePath)
{
if (_retainedFileCountLimit == null) return;

var currentFileName = Path.GetFileName(currentFilePath);

// We consider the current file to exist, even if nothing's been written yet,
// 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 });

var newestFirst = _roller
.SelectMatches(potentialMatches)
.OrderByDescending(m => m.Date)
.ThenByDescending(m => m.SequenceNumber)
.Select(m => m.Filename);

var toRemove = newestFirst
.Where(n => StringComparer.OrdinalIgnoreCase.Compare(currentFileName, n) != 0)
.Skip(_retainedFileCountLimit.Value - 1)
.ToList();

foreach (var obsolete in toRemove)
{
var fullPath = Path.Combine(_roller.LogFileDirectory, obsolete);
try
{
System.IO.File.Delete(fullPath);
}
catch (Exception ex)
{
SelfLog.WriteLine("Error {0} while removing obsolete file {1}", ex, fullPath);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// 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.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

namespace Serilog.Sinks.RollingFile.RetentionPolicies
{
internal interface IRetentionPolicy
{
/// <summary>
/// Applies the retention policy to the current file path.
/// </summary>
/// <param name="currentFilePath">Path to the file the policy will be applied to.</param>
void Apply(string currentFilePath);
}
}
69 changes: 29 additions & 40 deletions src/Serilog.Sinks.RollingFile/Sinks/RollingFile/RollingFileSink.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.


using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
Expand All @@ -23,6 +23,7 @@
using Serilog.Events;
using Serilog.Formatting;
using Serilog.Sinks.File;
using Serilog.Sinks.RollingFile.RetentionPolicies;

namespace Serilog.Sinks.RollingFile
{
Expand All @@ -36,7 +37,7 @@ public sealed class RollingFileSink : ILogEventSink, IDisposable
readonly TemplatedPathRoller _roller;
readonly ITextFormatter _textFormatter;
readonly long? _fileSizeLimitBytes;
readonly int? _retainedFileCountLimit;
readonly IList<IRetentionPolicy> _retentionPolicies;
readonly Encoding _encoding;
readonly bool _buffered;
readonly bool _shared;
Expand All @@ -54,11 +55,15 @@ public sealed class RollingFileSink : ILogEventSink, IDisposable
/// <param name="fileSizeLimitBytes">The maximum size, in bytes, to which a log file will be allowed to grow.
/// For unrestricted growth, pass null. The default is 1 GB.</param>
/// <param name="retainedFileCountLimit">The maximum number of log files that will be retained,
/// including the current log file. For unlimited retention, pass null. The default is 31.</param>
/// <param name="encoding">Character encoding used to write the text file. The default is UTF-8 without BOM.</param>
/// including the current log file. For unlimited retention, pass null. The default is 31.
/// Exclusive with <paramref name="retainedFileAgeLimit"/>.</param>
/// <param name="encoding">Character encoding used to write the text file. The default is UTF-8.</param>
/// <param name="buffered">Indicates if flushing to the output file can be buffered or not. The default
/// is false.</param>
/// <param name="shared">Allow the log files to be shared by multiple processes. The default is false.</param>
/// <param name="retainedFileAgeLimit">The maximum age of log files that will be retained,
/// including the current log file. For unlimited retention, pass null (default).
/// This will be applied after <paramref name="retainedFileCountLimit"/>.</param>
/// <returns>Configuration object allowing method chaining.</returns>
/// <remarks>The file will be written using the UTF-8 character set.</remarks>
public RollingFileSink(string pathFormat,
Expand All @@ -67,21 +72,34 @@ public RollingFileSink(string pathFormat,
int? retainedFileCountLimit,
Encoding encoding = null,
bool buffered = false,
bool shared = false)
bool shared = false,
TimeSpan? retainedFileAgeLimit = null)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The age limit is currently added as an optional parameter at the end of the RollingFileSink constructor and the corresponding extension methods, to avoid breaking existing code (though it does break binary compatibility).

Maybe a more elegant alternative could be to add an overloaded constructor that takes in one or more IRetentionPolicy parameters instead of retainedFileCountLimit and retainedFileAgeLimit, and matching extension methods? I'm not sure if this would play nicely with the XML/JSON configuration wiring though. Would that require changes to the Serilog configuration infrastructure? Is it possible to inject that behavior somehow instead?

{
if (pathFormat == null) throw new ArgumentNullException(nameof(pathFormat));
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 (retainedFileAgeLimit.HasValue && retainedFileAgeLimit <= TimeSpan.Zero) throw new ArgumentException("Zero or negative value provided; retained file age limit must be a positive time span");

#if !SHARING
if (shared)
throw new NotSupportedException("File sharing is not supported on this platform.");
#endif

if (shared && buffered)
throw new ArgumentException("Buffering is not available when sharing is enabled.");

_roller = new TemplatedPathRoller(pathFormat);
_textFormatter = textFormatter;
_fileSizeLimitBytes = fileSizeLimitBytes;
_retainedFileCountLimit = retainedFileCountLimit;
_retentionPolicies = new List<IRetentionPolicy>();
if (retainedFileCountLimit.HasValue)
{
_retentionPolicies.Add(new FileCountRetentionPolicy(_roller, retainedFileCountLimit.Value));
}
if (retainedFileAgeLimit.HasValue)
{
_retentionPolicies.Add(new FileAgeRetentionPolicy(_roller, retainedFileAgeLimit.Value));
}
_encoding = encoding;
_buffered = buffered;
_shared = shared;
Expand Down Expand Up @@ -177,45 +195,16 @@ void OpenFile(DateTime now)
throw;
}

ApplyRetentionPolicy(path);
ApplyRetentionPolicies(path);
return;
}
}

void ApplyRetentionPolicy(string currentFilePath)
void ApplyRetentionPolicies(string currentFilePath)
{
if (_retainedFileCountLimit == null) return;

var currentFileName = Path.GetFileName(currentFilePath);

// We consider the current file to exist, even if nothing's been written yet,
// 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 });

var newestFirst = _roller
.SelectMatches(potentialMatches)
.OrderByDescending(m => m.Date)
.ThenByDescending(m => m.SequenceNumber)
.Select(m => m.Filename);

var toRemove = newestFirst
.Where(n => StringComparer.OrdinalIgnoreCase.Compare(currentFileName, n) != 0)
.Skip(_retainedFileCountLimit.Value - 1)
.ToList();

foreach (var obsolete in toRemove)
foreach (var policy in _retentionPolicies)
{
var fullPath = Path.Combine(_roller.LogFileDirectory, obsolete);
try
{
System.IO.File.Delete(fullPath);
}
catch (Exception ex)
{
SelfLog.WriteLine("Error {0} while removing obsolete file {1}", ex, fullPath);
}
policy.Apply(currentFilePath);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ public class RollingFileLoggerConfigurationExtensionsTests
[Fact]
public void BuffferingIsNotAvailableWhenSharingEnabled()
{
#if SHARING
Assert.Throws<ArgumentException>(() =>
new LoggerConfiguration()
new LoggerConfiguration()
.WriteTo.RollingFile("logs", buffered: true, shared: true));
#endif
}
}
}
Loading