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);
+ }
+ }
+}