From 9f05a09754c6554fe1153f447d05babf8ebaf295 Mon Sep 17 00:00:00 2001
From: BernhardMarconato <37772769+BernhardMarconato@users.noreply.github.com>
Date: Sun, 2 Nov 2025 17:28:24 +0100
Subject: [PATCH 1/4] Allow the sink to flush for events of a minimum log level
---
.../FileLoggerConfigurationExtensions.cs | 15 ++-
src/Serilog.Sinks.File/Sinks/File/FileSink.cs | 9 +-
.../Sinks/File/RollingFileSink.cs | 7 +-
.../Serilog.Sinks.File.Tests/FileSinkTests.cs | 107 +++++++++++++++++-
4 files changed, 122 insertions(+), 16 deletions(-)
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];
+ }
}
From 16c634d64aec51ec5c7732321e610d83d54ebbce Mon Sep 17 00:00:00 2001
From: BernhardMarconato <37772769+BernhardMarconato@users.noreply.github.com>
Date: Thu, 13 Nov 2025 20:38:03 +0100
Subject: [PATCH 2/4] Revert "Allow the sink to flush for events of a minimum
log level"
This reverts commit 9f05a09754c6554fe1153f447d05babf8ebaf295.
---
.../FileLoggerConfigurationExtensions.cs | 15 +--
src/Serilog.Sinks.File/Sinks/File/FileSink.cs | 9 +-
.../Sinks/File/RollingFileSink.cs | 7 +-
.../Serilog.Sinks.File.Tests/FileSinkTests.cs | 107 +-----------------
4 files changed, 16 insertions(+), 122 deletions(-)
diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs
index 9c0fb84..e3e8bcf 100644
--- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs
+++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs
@@ -306,7 +306,6 @@ 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
@@ -332,8 +331,7 @@ public static LoggerConfiguration File(
int? retainedFileCountLimit = DefaultRetainedFileCountLimit,
Encoding? encoding = null,
FileLifecycleHooks? hooks = null,
- TimeSpan? retainedFileTimeLimit = null,
- LogEventLevel flushAtMinimumLevel = LevelAlias.Off)
+ TimeSpan? retainedFileTimeLimit = null)
{
if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration));
if (formatter == null) throw new ArgumentNullException(nameof(formatter));
@@ -341,7 +339,7 @@ public static LoggerConfiguration File(
return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch,
buffered, false, shared, flushToDiskInterval, encoding, rollingInterval, rollOnFileSizeLimit,
- retainedFileCountLimit, hooks, retainedFileTimeLimit, flushAtMinimumLevel);
+ retainedFileCountLimit, hooks, retainedFileTimeLimit);
}
///
@@ -496,7 +494,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, LevelAlias.Off);
+ false, null, encoding, RollingInterval.Infinite, false, null, hooks, null);
}
static LoggerConfiguration ConfigureFile(
@@ -515,8 +513,7 @@ static LoggerConfiguration ConfigureFile(
bool rollOnFileSizeLimit,
int? retainedFileCountLimit,
FileLifecycleHooks? hooks,
- TimeSpan? retainedFileTimeLimit,
- LogEventLevel flushAtMinimumLevel)
+ TimeSpan? retainedFileTimeLimit)
{
if (addSink == null) throw new ArgumentNullException(nameof(addSink));
if (formatter == null) throw new ArgumentNullException(nameof(formatter));
@@ -533,7 +530,7 @@ static LoggerConfiguration ConfigureFile(
{
if (rollOnFileSizeLimit || rollingInterval != RollingInterval.Infinite)
{
- sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, hooks, retainedFileTimeLimit, flushAtMinimumLevel);
+ sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, hooks, retainedFileTimeLimit);
}
else
{
@@ -545,7 +542,7 @@ static LoggerConfiguration ConfigureFile(
}
else
{
- sink = new FileSink(path, formatter, fileSizeLimitBytes, encoding, buffered, hooks, flushAtMinimumLevel);
+ sink = new FileSink(path, formatter, fileSizeLimitBytes, encoding, buffered, hooks);
}
}
diff --git a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs
index ed3cc08..32c0cd3 100644
--- a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs
+++ b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs
@@ -32,7 +32,6 @@ public sealed class FileSink : IFileSink, IDisposable, ISetLoggingFailureListene
readonly bool _buffered;
readonly object _syncRoot = new();
readonly WriteCountingStream? _countingStreamWrapper;
- readonly LogEventLevel _flushAtMinimumLevel;
ILoggingFailureListener _failureListener = SelfLog.FailureListener;
@@ -58,7 +57,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, LevelAlias.Off)
+ : this(path, textFormatter, fileSizeLimitBytes, encoding, buffered, null)
{
}
@@ -69,15 +68,13 @@ internal FileSink(
long? fileSizeLimitBytes,
Encoding? encoding,
bool buffered,
- FileLifecycleHooks? hooks,
- LogEventLevel flushAtMinimumLevel)
+ FileLifecycleHooks? hooks)
{
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))
@@ -127,8 +124,6 @@ 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 762bac2..93c02c5 100644
--- a/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs
+++ b/src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs
@@ -27,7 +27,6 @@ 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;
@@ -52,8 +51,7 @@ public RollingFileSink(string path,
RollingInterval rollingInterval,
bool rollOnFileSizeLimit,
FileLifecycleHooks? hooks,
- TimeSpan? retainedFileTimeLimit,
- LogEventLevel flushAtMinimumLevel)
+ TimeSpan? retainedFileTimeLimit)
{
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.");
@@ -65,7 +63,6 @@ public RollingFileSink(string path,
_fileSizeLimitBytes = fileSizeLimitBytes;
_retainedFileCountLimit = retainedFileCountLimit;
_retainedFileTimeLimit = retainedFileTimeLimit;
- _flushAtMinimumLevel = flushAtMinimumLevel;
_encoding = encoding;
_buffered = buffered;
_shared = shared;
@@ -179,7 +176,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, _flushAtMinimumLevel);
+ new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered, _hooks);
_currentFileSequence = sequence;
diff --git a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs
index be45a8f..b42a562 100644
--- a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs
+++ b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs
@@ -1,7 +1,6 @@
-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;
@@ -147,7 +146,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, LevelAlias.Off))
+ using (var sink = new FileSink(path, new JsonFormatter(), null, null, false, gzipWrapper))
{
sink.Emit(evt);
sink.Emit(evt);
@@ -179,12 +178,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, LevelAlias.Off))
+ using (new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, headerWriter))
{
// Open and write header
}
- using (var sink = new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, headerWriter, LevelAlias.Off))
+ using (var sink = new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, headerWriter))
{
// Length check should prevent duplicate header here
sink.Emit(Some.LogEvent());
@@ -204,7 +203,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, LevelAlias.Off))
+ using (new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, capturePath))
{
// Open and capture the log file path
}
@@ -224,7 +223,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, LevelAlias.Off))
+ using (var sink = new FileSink(path, new JsonFormatter(), fileSizeLimitBytes: null, encoding: new UTF8Encoding(false), buffered: false, hooks: emptyFileHook))
{
// Hook will clear the contents of the file before emitting the log events
sink.Emit(Some.LogEvent());
@@ -236,83 +235,6 @@ 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();
@@ -338,21 +260,4 @@ 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];
- }
}
From 957a5b0e98f2a2bb2fc9e0a632ca015743aff4d5 Mon Sep 17 00:00:00 2001
From: BernhardMarconato <37772769+BernhardMarconato@users.noreply.github.com>
Date: Thu, 13 Nov 2025 21:02:16 +0100
Subject: [PATCH 3/4] Always flush sink on Fatal events
---
README.md | 2 +-
.../FileLoggerConfigurationExtensions.cs | 24 +++++-----
src/Serilog.Sinks.File/Sinks/File/FileSink.cs | 5 +-
.../Serilog.Sinks.File.Tests/FileSinkTests.cs | 46 +++++++++++++++++--
4 files changed, 60 insertions(+), 17 deletions(-)
diff --git a/README.md b/README.md
index 1feed22..9eec1d2 100644
--- a/README.md
+++ b/README.md
@@ -189,7 +189,7 @@ Only a limited subset of configuration options are currently available in this m
### Performance
-By default, the file sink will flush each event written through it to disk. To improve write performance, specifying `buffered: true` will permit the underlying stream to buffer writes.
+By default, the file sink will flush each event written through it to disk. To improve write performance, specifying `buffered: true` will permit the underlying stream to buffer writes. However, events with `LogEventLevel.Fatal` will always be flushed to disk immediately.
The [Serilog.Sinks.Async](https://github.com/serilog/serilog-sinks-async) package can be used to wrap the file sink and perform all disk access on a background worker thread.
diff --git a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs
index e3e8bcf..d75124b 100644
--- a/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs
+++ b/src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs
@@ -49,8 +49,8 @@ public static class FileLoggerConfigurationExtensions
/// The approximate maximum size, in bytes, to which a log file will be allowed to grow.
/// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit
/// will be written in full even if it exceeds the limit.
- /// Indicates if flushing to the output file can be buffered or not. The default
- /// is false.
+ /// Indicates if flushing of non-fatal events to the output file
+ /// can be buffered or not. The default is false.
/// 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.
/// Configuration object allowing method chaining.
@@ -89,8 +89,8 @@ public static LoggerConfiguration File(
/// The approximate maximum size, in bytes, to which a log file will be allowed to grow.
/// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit
/// will be written in full even if it exceeds the limit.
- /// Indicates if flushing to the output file can be buffered or not. The default
- /// is false.
+ /// Indicates if flushing of non-fatal events to the output file
+ /// can be buffered or not. The default is false.
/// 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.
/// Configuration object allowing method chaining.
@@ -126,8 +126,8 @@ public static LoggerConfiguration File(
/// The approximate maximum size, in bytes, to which a log file will be allowed to grow.
/// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit
/// will be written in full even if it exceeds the limit.
- /// Indicates if flushing to the output file can be buffered or not. The default
- /// is false.
+ /// Indicates if flushing of non-fatal events to the output file
+ /// can be buffered or not. The default is false.
/// 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.
@@ -175,8 +175,8 @@ public static LoggerConfiguration File(
/// The approximate maximum size, in bytes, to which a log file will be allowed to grow.
/// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit
/// will be written in full even if it exceeds the limit.
- /// Indicates if flushing to the output file can be buffered or not. The default
- /// is false.
+ /// Indicates if flushing of non-fatal events to the output file
+ /// can be buffered or not. The default is false.
/// 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.
@@ -221,8 +221,8 @@ public static LoggerConfiguration File(
/// The approximate maximum size, in bytes, to which a log file will be allowed to grow.
/// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit
/// will be written in full even if it exceeds the limit.
- /// Indicates if flushing to the output file can be buffered or not. The default
- /// is false.
+ /// Indicates if flushing of non-fatal events to the output file
+ /// can be buffered or not. The default is false.
/// 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.
@@ -291,8 +291,8 @@ public static LoggerConfiguration File(
/// The approximate maximum size, in bytes, to which a log file will be allowed to grow.
/// For unrestricted growth, pass null. The default is 1 GB. To avoid writing partial events, the last event within the limit
/// will be written in full even if it exceeds the limit.
- /// Indicates if flushing to the output file can be buffered or not. The default
- /// is false.
+ /// Indicates if flushing of non-fatal events to the output file
+ /// can be buffered or not. The default is false.
/// 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.
diff --git a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs
index 32c0cd3..14c9f56 100644
--- a/src/Serilog.Sinks.File/Sinks/File/FileSink.cs
+++ b/src/Serilog.Sinks.File/Sinks/File/FileSink.cs
@@ -122,7 +122,10 @@ bool IFileSink.EmitOrOverflow(LogEvent logEvent)
}
_textFormatter.Format(logEvent, _output);
- if (!_buffered)
+
+ if (logEvent.Level == LogEventLevel.Fatal)
+ FlushToDisk();
+ else if (!_buffered)
_output.Flush();
return true;
diff --git a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs
index b42a562..fa7ea49 100644
--- a/test/Serilog.Sinks.File.Tests/FileSinkTests.cs
+++ b/test/Serilog.Sinks.File.Tests/FileSinkTests.cs
@@ -1,10 +1,11 @@
-using System.IO.Compression;
-using System.Text;
using Serilog.Core;
-using Xunit;
+using Serilog.Events;
using Serilog.Formatting.Json;
using Serilog.Sinks.File.Tests.Support;
using Serilog.Tests.Support;
+using System.IO.Compression;
+using System.Text;
+using Xunit;
#pragma warning disable 618
@@ -235,6 +236,28 @@ public static void OnOpenedLifecycleHookCanEmptyTheFileContents()
Assert.Equal('{', lines[0][0]);
}
+ [Fact]
+ public void WhenBufferedFatalEventFlushesAllPendingEvents()
+ {
+ using var tmp = TempFolder.ForCaller();
+ var path = tmp.AllocateFilename("txt");
+ var formatter = new JsonFormatter();
+
+ using (var sink = new FileSink(path, formatter, null, null, true))
+ {
+ sink.Emit(Some.LogEvent(level: LogEventLevel.Information));
+ sink.Emit(Some.LogEvent(level: LogEventLevel.Warning));
+
+ var lines = ReadAllLinesShared(path);
+ Assert.Empty(lines);
+
+ sink.Emit(Some.LogEvent(level: LogEventLevel.Fatal));
+
+ lines = ReadAllLinesShared(path);
+ Assert.Equal(3, lines.Length);
+ }
+ }
+
static void WriteTwoEventsAndCheckOutputFileLength(long? maxBytes, Encoding encoding)
{
using var tmp = TempFolder.ForCaller();
@@ -260,4 +283,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];
+ }
}
From 558da59d8ef5651a5e7d7c5e8554c7a49cc5ffc4 Mon Sep 17 00:00:00 2001
From: BernhardMarconato <37772769+BernhardMarconato@users.noreply.github.com>
Date: Thu, 13 Nov 2025 21:03:03 +0100
Subject: [PATCH 4/4] Bump version to 8.0
---
Directory.Version.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Directory.Version.props b/Directory.Version.props
index 8596ae5..2e9cd58 100644
--- a/Directory.Version.props
+++ b/Directory.Version.props
@@ -1,5 +1,5 @@
- 7.0.1
+ 8.0.0