diff --git a/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs index 3dbddeb..fbaf133 100644 --- a/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs +++ b/src/Serilog.Sinks.File/Sinks/File/FileLifecycleHooks.cs @@ -19,6 +19,7 @@ namespace Serilog.Sinks.File { /// /// Enables hooking into log file lifecycle events. + /// Hooks run synchronously and therefore may affect responsiveness of the application if long operations are performed. /// public abstract class FileLifecycleHooks { @@ -35,5 +36,12 @@ public abstract class FileLifecycleHooks /// The encoding to use when reading/writing to the stream. /// The Serilog should use when writing events to the log file. public virtual Stream OnFileOpened(Stream underlyingStream, Encoding encoding) => underlyingStream; + + /// + /// Called before an obsolete (rolling) log file is deleted. + /// This can be used to copy old logs to an archive location or send to a backup server. + /// + /// The full path to the file being deleted. + public virtual void OnFileDeleting(string path) {} } } diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs index 2db6f24..43e5fad 100644 --- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs +++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs @@ -199,11 +199,12 @@ void ApplyRetentionPolicy(string currentFilePath) var fullPath = Path.Combine(_roller.LogFileDirectory, obsolete); try { + _hooks?.OnFileDeleting(fullPath); System.IO.File.Delete(fullPath); } catch (Exception ex) { - SelfLog.WriteLine("Error {0} while removing obsolete log file {1}", ex, fullPath); + SelfLog.WriteLine("Error {0} while processing obsolete log file {1}", ex, fullPath); } } } diff --git a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs index 2e9f613..70408b3 100644 --- a/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/RollingFileSinkTests.cs @@ -71,6 +71,28 @@ public void WhenRetentionCountIsSetOldFilesAreDeleted() }); } + [Fact] + public void WhenRetentionCountAndArchivingHookIsSetOldFilesAreCopiedAndOriginalDeleted() + { + const string archiveDirectory = "OldLogs"; + LogEvent e1 = Some.InformationEvent(), + 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}, + 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])); + + Assert.True(System.IO.File.Exists(ArchiveOldLogsHook.AddTopDirectory(files[0], archiveDirectory))); + }); + } + [Fact] public void WhenSizeLimitIsBreachedNewFilesCreated() { diff --git a/test/Serilog.Sinks.File.Tests/Support/ArchiveOldLogsHook.cs b/test/Serilog.Sinks.File.Tests/Support/ArchiveOldLogsHook.cs new file mode 100644 index 0000000..96dcb8c --- /dev/null +++ b/test/Serilog.Sinks.File.Tests/Support/ArchiveOldLogsHook.cs @@ -0,0 +1,35 @@ +using System; +using System.IO; +using System.Text; + +namespace Serilog.Sinks.File.Tests.Support +{ + internal class ArchiveOldLogsHook : FileLifecycleHooks + { + private readonly string _relativeArchiveDir; + + public ArchiveOldLogsHook(string relativeArchiveDir) + { + _relativeArchiveDir = relativeArchiveDir; + } + + public override void OnFileDeleting(string path) + { + base.OnFileDeleting(path); + var newFile = AddTopDirectory(path, _relativeArchiveDir, true); + System.IO.File.Copy(path, newFile, false); + } + + public static string AddTopDirectory(string path, string directoryToAdd, bool createOnNonExist = false) + { + string file = Path.GetFileName(path); + string directory = Path.Combine(Path.GetDirectoryName(path) ?? throw new InvalidOperationException(), directoryToAdd); + + if (createOnNonExist && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + return Path.Combine(directory, file); + } + } +}