diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs
index e3e8bcf..9c0fb84 100644
--- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs
+++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs
@@ -306,6 +306,7 @@ public static LoggerConfiguration File(
/// Must be greater than or equal to .
/// Ignored if is .
/// The default is to retain files indefinitely.
+ /// The minimum level for events to flush the sink. The default is .
/// Configuration object allowing method chaining.
/// When is null
/// When is null
@@ -331,7 +332,8 @@ public static LoggerConfiguration File(
int? retainedFileCountLimit = DefaultRetainedFileCountLimit,
Encoding? encoding = null,
FileLifecycleHooks? hooks = null,
- TimeSpan? retainedFileTimeLimit = null)
+ TimeSpan? retainedFileTimeLimit = null,
+ LogEventLevel flushAtMinimumLevel = LevelAlias.Off)
{
if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration));
if (formatter == null) throw new ArgumentNullException(nameof(formatter));
@@ -339,7 +341,7 @@ public static LoggerConfiguration File(
return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch,
buffered, false, shared, flushToDiskInterval, encoding, rollingInterval, rollOnFileSizeLimit,
- retainedFileCountLimit, hooks, retainedFileTimeLimit);
+ retainedFileCountLimit, hooks, retainedFileTimeLimit, flushAtMinimumLevel);
}
///
@@ -494,7 +496,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, null);
+ false, null, encoding, RollingInterval.Infinite, false, null, hooks, null, LevelAlias.Off);
}
static LoggerConfiguration ConfigureFile(
@@ -513,7 +515,8 @@ static LoggerConfiguration ConfigureFile(
bool rollOnFileSizeLimit,
int? retainedFileCountLimit,
FileLifecycleHooks? hooks,
- TimeSpan? retainedFileTimeLimit)
+ TimeSpan? retainedFileTimeLimit,
+ LogEventLevel flushAtMinimumLevel)
{
if (addSink == null) throw new ArgumentNullException(nameof(addSink));
if (formatter == null) throw new ArgumentNullException(nameof(formatter));
@@ -530,7 +533,7 @@ static LoggerConfiguration ConfigureFile(
{
if (rollOnFileSizeLimit || rollingInterval != RollingInterval.Infinite)
{
- sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, hooks, retainedFileTimeLimit);
+ sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, hooks, retainedFileTimeLimit, flushAtMinimumLevel);
}
else
{
@@ -542,7 +545,7 @@ static LoggerConfiguration ConfigureFile(
}
else
{
- sink = new FileSink(path, formatter, fileSizeLimitBytes, encoding, buffered, hooks);
+ sink = new FileSink(path, formatter, fileSizeLimitBytes, encoding, buffered, hooks, flushAtMinimumLevel);
}
}
diff --git a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs
index 32c0cd3..ed3cc08 100644
--- a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs
+++ b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs
@@ -32,6 +32,7 @@ public sealed class FileSink : IFileSink, IDisposable, ISetLoggingFailureListene
readonly bool _buffered;
readonly object _syncRoot = new();
readonly WriteCountingStream? _countingStreamWrapper;
+ readonly LogEventLevel _flushAtMinimumLevel;
ILoggingFailureListener _failureListener = SelfLog.FailureListener;
@@ -57,7 +58,7 @@ public sealed class FileSink : IFileSink, IDisposable, ISetLoggingFailureListene
/// Invalid
[Obsolete("This type and constructor will be removed from the public API in a future version; use `WriteTo.File()` instead.")]
public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding? encoding = null, bool buffered = false)
- : this(path, textFormatter, fileSizeLimitBytes, encoding, buffered, null)
+ : this(path, textFormatter, fileSizeLimitBytes, encoding, buffered, null, LevelAlias.Off)
{
}
@@ -68,13 +69,15 @@ internal FileSink(
long? fileSizeLimitBytes,
Encoding? encoding,
bool buffered,
- FileLifecycleHooks? hooks)
+ FileLifecycleHooks? hooks,
+ LogEventLevel flushAtMinimumLevel)
{
if (path == null) throw new ArgumentNullException(nameof(path));
if (fileSizeLimitBytes is < 1) throw new ArgumentException("Invalid value provided; file size limit must be at least 1 byte, or null.");
_textFormatter = textFormatter ?? throw new ArgumentNullException(nameof(textFormatter));
_fileSizeLimitBytes = fileSizeLimitBytes;
_buffered = buffered;
+ _flushAtMinimumLevel = flushAtMinimumLevel;
var directory = Path.GetDirectoryName(path);
if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory))
@@ -124,6 +127,8 @@ bool IFileSink.EmitOrOverflow(LogEvent logEvent)
_textFormatter.Format(logEvent, _output);
if (!_buffered)
_output.Flush();
+ else if (logEvent.Level >= _flushAtMinimumLevel)
+ FlushToDisk();
return true;
}
diff --git a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs
index 93c02c5..762bac2 100644
--- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs
+++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs
@@ -27,6 +27,7 @@ sealed class RollingFileSink : ILogEventSink, IFlushableFileSink, IDisposable, I
readonly long? _fileSizeLimitBytes;
readonly int? _retainedFileCountLimit;
readonly TimeSpan? _retainedFileTimeLimit;
+ readonly LogEventLevel _flushAtMinimumLevel;
readonly Encoding? _encoding;
readonly bool _buffered;
readonly bool _shared;
@@ -51,7 +52,8 @@ public RollingFileSink(string path,
RollingInterval rollingInterval,
bool rollOnFileSizeLimit,
FileLifecycleHooks? hooks,
- TimeSpan? retainedFileTimeLimit)
+ TimeSpan? retainedFileTimeLimit,
+ LogEventLevel flushAtMinimumLevel)
{
if (path == null) throw new ArgumentNullException(nameof(path));
if (fileSizeLimitBytes is < 1) throw new ArgumentException("Invalid value provided; file size limit must be at least 1 byte, or null.");
@@ -63,6 +65,7 @@ public RollingFileSink(string path,
_fileSizeLimitBytes = fileSizeLimitBytes;
_retainedFileCountLimit = retainedFileCountLimit;
_retainedFileTimeLimit = retainedFileTimeLimit;
+ _flushAtMinimumLevel = flushAtMinimumLevel;
_encoding = encoding;
_buffered = buffered;
_shared = shared;
@@ -176,7 +179,7 @@ void OpenFile(DateTime now, int? minSequence = null)
new SharedFileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding)
:
#pragma warning restore 618
- new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered, _hooks);
+ new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered, _hooks, _flushAtMinimumLevel);
_currentFileSequence = sequence;
diff --git a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs
index b42a562..be45a8f 100644
--- a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs
+++ b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs
@@ -1,6 +1,7 @@
-using System.IO.Compression;
+using System.IO.Compression;
using System.Text;
using Serilog.Core;
+using Serilog.Events;
using Xunit;
using Serilog.Formatting.Json;
using Serilog.Sinks.File.Tests.Support;
@@ -146,7 +147,7 @@ public void OnOpenedLifecycleHookCanWrapUnderlyingStream()
var path = tmp.AllocateFilename("txt");
var evt = Some.LogEvent("Hello, world!");
- using (var sink = new FileSink(path, new JsonFormatter(), null, null, false, gzipWrapper))
+ using (var sink = new FileSink(path, new JsonFormatter(), null, null, false, gzipWrapper, LevelAlias.Off))
{
sink.Emit(evt);
sink.Emit(evt);
@@ -178,12 +179,12 @@ public static void OnOpenedLifecycleHookCanWriteFileHeader()
var headerWriter = new FileHeaderWriter("This is the file header");
var path = tmp.AllocateFilename("txt");
- using (new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, headerWriter))
+ using (new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, headerWriter, LevelAlias.Off))
{
// Open and write header
}
- using (var sink = new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, headerWriter))
+ using (var sink = new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, headerWriter, LevelAlias.Off))
{
// Length check should prevent duplicate header here
sink.Emit(Some.LogEvent());
@@ -203,7 +204,7 @@ public static void OnOpenedLifecycleHookCanCaptureFilePath()
var capturePath = new CaptureFilePathHook();
var path = tmp.AllocateFilename("txt");
- using (new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, capturePath))
+ using (new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, capturePath, LevelAlias.Off))
{
// Open and capture the log file path
}
@@ -223,7 +224,7 @@ public static void OnOpenedLifecycleHookCanEmptyTheFileContents()
sink.Emit(Some.LogEvent());
}
- using (var sink = new FileSink(path, new JsonFormatter(), fileSizeLimitBytes: null, encoding: new UTF8Encoding(false), buffered: false, hooks: emptyFileHook))
+ using (var sink = new FileSink(path, new JsonFormatter(), fileSizeLimitBytes: null, encoding: new UTF8Encoding(false), buffered: false, hooks: emptyFileHook, LevelAlias.Off))
{
// Hook will clear the contents of the file before emitting the log events
sink.Emit(Some.LogEvent());
@@ -235,6 +236,83 @@ public static void OnOpenedLifecycleHookCanEmptyTheFileContents()
Assert.Equal('{', lines[0][0]);
}
+ [Fact]
+ public void WhenFlushAtMinimumLevelIsNotReachedLineIsNotFlushed()
+ {
+ using var tmp = TempFolder.ForCaller();
+ var path = tmp.AllocateFilename("txt");
+ var formatter = new JsonFormatter();
+
+ using (var sink = new FileSink(path, formatter, null, null, true, null, LogEventLevel.Fatal))
+ {
+ sink.Emit(Some.LogEvent(level: LogEventLevel.Information));
+
+ var lines = ReadAllLinesShared(path);
+ Assert.Empty(lines);
+ }
+
+ var savedLines = System.IO.File.ReadAllLines(path);
+ Assert.Single(savedLines);
+ }
+
+ [Fact]
+ public void WhenFlushAtMinimumLevelIsReachedLineIsFlushed()
+ {
+ using var tmp = TempFolder.ForCaller();
+ var path = tmp.AllocateFilename("txt");
+ var formatter = new JsonFormatter();
+
+ using (var sink = new FileSink(path, formatter, null, null, true, null, LogEventLevel.Fatal))
+ {
+ sink.Emit(Some.LogEvent(level: LogEventLevel.Fatal));
+
+ var lines = ReadAllLinesShared(path);
+ Assert.Single(lines);
+ }
+
+ var savedLines = System.IO.File.ReadAllLines(path);
+ Assert.Single(savedLines);
+ }
+
+ [Fact]
+ public void WhenFlushAtMinimumLevelIsOffLineIsNotFlushed()
+ {
+ using var tmp = TempFolder.ForCaller();
+ var path = tmp.AllocateFilename("txt");
+ var formatter = new JsonFormatter();
+
+ using (var sink = new FileSink(path, formatter, null, null, true, null, LevelAlias.Off))
+ {
+ sink.Emit(Some.LogEvent(level: LogEventLevel.Fatal));
+
+ var lines = ReadAllLinesShared(path);
+ Assert.Empty(lines);
+ }
+
+ var savedLines = System.IO.File.ReadAllLines(path);
+ Assert.Single(savedLines);
+ }
+
+ [Fact]
+ public void WhenFlushAtMinimumLevelIsReachedMultipleLinesAreFlushed()
+ {
+ using var tmp = TempFolder.ForCaller();
+ var path = tmp.AllocateFilename("txt");
+ var formatter = new JsonFormatter();
+
+ using (var sink = new FileSink(path, formatter, null, null, true, null, LogEventLevel.Error))
+ {
+ sink.Emit(Some.LogEvent(level: LogEventLevel.Information));
+ sink.Emit(Some.LogEvent(level: LogEventLevel.Fatal));
+
+ var lines = ReadAllLinesShared(path);
+ Assert.Equal(2, lines.Length);
+ }
+
+ var savedLines = System.IO.File.ReadAllLines(path);
+ Assert.Equal(2, savedLines.Length);
+ }
+
static void WriteTwoEventsAndCheckOutputFileLength(long? maxBytes, Encoding encoding)
{
using var tmp = TempFolder.ForCaller();
@@ -260,4 +338,21 @@ static void WriteTwoEventsAndCheckOutputFileLength(long? maxBytes, Encoding enco
size = new FileInfo(path).Length;
Assert.Equal(encoding.GetPreamble().Length + eventOuputLength * 2, size);
}
+
+ private static string[] ReadAllLinesShared(string path)
+ {
+ // ReadAllLines cannot be used here, as it can't read files even if they are opened with FileShare.Read
+ using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ using var reader = new StreamReader(fs);
+
+ string? line;
+ List lines = [];
+
+ while ((line = reader.ReadLine()) != null)
+ {
+ lines.Add(line);
+ }
+
+ return [.. lines];
+ }
}