Skip to content

Commit 9f05a09

Browse files
Allow the sink to flush for events of a minimum log level
1 parent 53280f5 commit 9f05a09

File tree

4 files changed

+122
-16
lines changed

4 files changed

+122
-16
lines changed

src/Serilog.Sinks.File/FileLoggerConfigurationExtensions.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ public static LoggerConfiguration File(
306306
/// Must be greater than or equal to <see cref="TimeSpan.Zero"/>.
307307
/// Ignored if <paramref see="rollingInterval"/> is <see cref="RollingInterval.Infinite"/>.
308308
/// The default is to retain files indefinitely.</param>
309+
/// <param name="flushAtMinimumLevel">The minimum level for events to flush the sink. The default is <see cref="LevelAlias.Off"/>.</param>
309310
/// <returns>Configuration object allowing method chaining.</returns>
310311
/// <exception cref="ArgumentNullException">When <paramref name="sinkConfiguration"/> is <code>null</code></exception>
311312
/// <exception cref="ArgumentNullException">When <paramref name="formatter"/> is <code>null</code></exception>
@@ -331,15 +332,16 @@ public static LoggerConfiguration File(
331332
int? retainedFileCountLimit = DefaultRetainedFileCountLimit,
332333
Encoding? encoding = null,
333334
FileLifecycleHooks? hooks = null,
334-
TimeSpan? retainedFileTimeLimit = null)
335+
TimeSpan? retainedFileTimeLimit = null,
336+
LogEventLevel flushAtMinimumLevel = LevelAlias.Off)
335337
{
336338
if (sinkConfiguration == null) throw new ArgumentNullException(nameof(sinkConfiguration));
337339
if (formatter == null) throw new ArgumentNullException(nameof(formatter));
338340
if (path == null) throw new ArgumentNullException(nameof(path));
339341

340342
return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, fileSizeLimitBytes, levelSwitch,
341343
buffered, false, shared, flushToDiskInterval, encoding, rollingInterval, rollOnFileSizeLimit,
342-
retainedFileCountLimit, hooks, retainedFileTimeLimit);
344+
retainedFileCountLimit, hooks, retainedFileTimeLimit, flushAtMinimumLevel);
343345
}
344346

345347
/// <summary>
@@ -494,7 +496,7 @@ public static LoggerConfiguration File(
494496
if (path == null) throw new ArgumentNullException(nameof(path));
495497

496498
return ConfigureFile(sinkConfiguration.Sink, formatter, path, restrictedToMinimumLevel, null, levelSwitch, false, true,
497-
false, null, encoding, RollingInterval.Infinite, false, null, hooks, null);
499+
false, null, encoding, RollingInterval.Infinite, false, null, hooks, null, LevelAlias.Off);
498500
}
499501

500502
static LoggerConfiguration ConfigureFile(
@@ -513,7 +515,8 @@ static LoggerConfiguration ConfigureFile(
513515
bool rollOnFileSizeLimit,
514516
int? retainedFileCountLimit,
515517
FileLifecycleHooks? hooks,
516-
TimeSpan? retainedFileTimeLimit)
518+
TimeSpan? retainedFileTimeLimit,
519+
LogEventLevel flushAtMinimumLevel)
517520
{
518521
if (addSink == null) throw new ArgumentNullException(nameof(addSink));
519522
if (formatter == null) throw new ArgumentNullException(nameof(formatter));
@@ -530,7 +533,7 @@ static LoggerConfiguration ConfigureFile(
530533
{
531534
if (rollOnFileSizeLimit || rollingInterval != RollingInterval.Infinite)
532535
{
533-
sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, hooks, retainedFileTimeLimit);
536+
sink = new RollingFileSink(path, formatter, fileSizeLimitBytes, retainedFileCountLimit, encoding, buffered, shared, rollingInterval, rollOnFileSizeLimit, hooks, retainedFileTimeLimit, flushAtMinimumLevel);
534537
}
535538
else
536539
{
@@ -542,7 +545,7 @@ static LoggerConfiguration ConfigureFile(
542545
}
543546
else
544547
{
545-
sink = new FileSink(path, formatter, fileSizeLimitBytes, encoding, buffered, hooks);
548+
sink = new FileSink(path, formatter, fileSizeLimitBytes, encoding, buffered, hooks, flushAtMinimumLevel);
546549
}
547550

548551
}

src/Serilog.Sinks.File/Sinks/File/FileSink.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public sealed class FileSink : IFileSink, IDisposable, ISetLoggingFailureListene
3232
readonly bool _buffered;
3333
readonly object _syncRoot = new();
3434
readonly WriteCountingStream? _countingStreamWrapper;
35+
readonly LogEventLevel _flushAtMinimumLevel;
3536

3637
ILoggingFailureListener _failureListener = SelfLog.FailureListener;
3738

@@ -57,7 +58,7 @@ public sealed class FileSink : IFileSink, IDisposable, ISetLoggingFailureListene
5758
/// <exception cref="ArgumentException">Invalid <paramref name="path"/></exception>
5859
[Obsolete("This type and constructor will be removed from the public API in a future version; use `WriteTo.File()` instead.")]
5960
public FileSink(string path, ITextFormatter textFormatter, long? fileSizeLimitBytes, Encoding? encoding = null, bool buffered = false)
60-
: this(path, textFormatter, fileSizeLimitBytes, encoding, buffered, null)
61+
: this(path, textFormatter, fileSizeLimitBytes, encoding, buffered, null, LevelAlias.Off)
6162
{
6263
}
6364

@@ -68,13 +69,15 @@ internal FileSink(
6869
long? fileSizeLimitBytes,
6970
Encoding? encoding,
7071
bool buffered,
71-
FileLifecycleHooks? hooks)
72+
FileLifecycleHooks? hooks,
73+
LogEventLevel flushAtMinimumLevel)
7274
{
7375
if (path == null) throw new ArgumentNullException(nameof(path));
7476
if (fileSizeLimitBytes is < 1) throw new ArgumentException("Invalid value provided; file size limit must be at least 1 byte, or null.");
7577
_textFormatter = textFormatter ?? throw new ArgumentNullException(nameof(textFormatter));
7678
_fileSizeLimitBytes = fileSizeLimitBytes;
7779
_buffered = buffered;
80+
_flushAtMinimumLevel = flushAtMinimumLevel;
7881

7982
var directory = Path.GetDirectoryName(path);
8083
if (!string.IsNullOrWhiteSpace(directory) && !Directory.Exists(directory))
@@ -124,6 +127,8 @@ bool IFileSink.EmitOrOverflow(LogEvent logEvent)
124127
_textFormatter.Format(logEvent, _output);
125128
if (!_buffered)
126129
_output.Flush();
130+
else if (logEvent.Level >= _flushAtMinimumLevel)
131+
FlushToDisk();
127132

128133
return true;
129134
}

src/Serilog.Sinks.File/Sinks/File/RollingFileSink.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ sealed class RollingFileSink : ILogEventSink, IFlushableFileSink, IDisposable, I
2727
readonly long? _fileSizeLimitBytes;
2828
readonly int? _retainedFileCountLimit;
2929
readonly TimeSpan? _retainedFileTimeLimit;
30+
readonly LogEventLevel _flushAtMinimumLevel;
3031
readonly Encoding? _encoding;
3132
readonly bool _buffered;
3233
readonly bool _shared;
@@ -51,7 +52,8 @@ public RollingFileSink(string path,
5152
RollingInterval rollingInterval,
5253
bool rollOnFileSizeLimit,
5354
FileLifecycleHooks? hooks,
54-
TimeSpan? retainedFileTimeLimit)
55+
TimeSpan? retainedFileTimeLimit,
56+
LogEventLevel flushAtMinimumLevel)
5557
{
5658
if (path == null) throw new ArgumentNullException(nameof(path));
5759
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,
6365
_fileSizeLimitBytes = fileSizeLimitBytes;
6466
_retainedFileCountLimit = retainedFileCountLimit;
6567
_retainedFileTimeLimit = retainedFileTimeLimit;
68+
_flushAtMinimumLevel = flushAtMinimumLevel;
6669
_encoding = encoding;
6770
_buffered = buffered;
6871
_shared = shared;
@@ -176,7 +179,7 @@ void OpenFile(DateTime now, int? minSequence = null)
176179
new SharedFileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding)
177180
:
178181
#pragma warning restore 618
179-
new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered, _hooks);
182+
new FileSink(path, _textFormatter, _fileSizeLimitBytes, _encoding, _buffered, _hooks, _flushAtMinimumLevel);
180183

181184
_currentFileSequence = sequence;
182185

test/Serilog.Sinks.File.Tests/FileSinkTests.cs

Lines changed: 101 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
using System.IO.Compression;
1+
using System.IO.Compression;
22
using System.Text;
33
using Serilog.Core;
4+
using Serilog.Events;
45
using Xunit;
56
using Serilog.Formatting.Json;
67
using Serilog.Sinks.File.Tests.Support;
@@ -146,7 +147,7 @@ public void OnOpenedLifecycleHookCanWrapUnderlyingStream()
146147
var path = tmp.AllocateFilename("txt");
147148
var evt = Some.LogEvent("Hello, world!");
148149

149-
using (var sink = new FileSink(path, new JsonFormatter(), null, null, false, gzipWrapper))
150+
using (var sink = new FileSink(path, new JsonFormatter(), null, null, false, gzipWrapper, LevelAlias.Off))
150151
{
151152
sink.Emit(evt);
152153
sink.Emit(evt);
@@ -178,12 +179,12 @@ public static void OnOpenedLifecycleHookCanWriteFileHeader()
178179
var headerWriter = new FileHeaderWriter("This is the file header");
179180

180181
var path = tmp.AllocateFilename("txt");
181-
using (new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, headerWriter))
182+
using (new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, headerWriter, LevelAlias.Off))
182183
{
183184
// Open and write header
184185
}
185186

186-
using (var sink = new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, headerWriter))
187+
using (var sink = new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, headerWriter, LevelAlias.Off))
187188
{
188189
// Length check should prevent duplicate header here
189190
sink.Emit(Some.LogEvent());
@@ -203,7 +204,7 @@ public static void OnOpenedLifecycleHookCanCaptureFilePath()
203204
var capturePath = new CaptureFilePathHook();
204205

205206
var path = tmp.AllocateFilename("txt");
206-
using (new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, capturePath))
207+
using (new FileSink(path, new JsonFormatter(), null, new UTF8Encoding(false), false, capturePath, LevelAlias.Off))
207208
{
208209
// Open and capture the log file path
209210
}
@@ -223,7 +224,7 @@ public static void OnOpenedLifecycleHookCanEmptyTheFileContents()
223224
sink.Emit(Some.LogEvent());
224225
}
225226

226-
using (var sink = new FileSink(path, new JsonFormatter(), fileSizeLimitBytes: null, encoding: new UTF8Encoding(false), buffered: false, hooks: emptyFileHook))
227+
using (var sink = new FileSink(path, new JsonFormatter(), fileSizeLimitBytes: null, encoding: new UTF8Encoding(false), buffered: false, hooks: emptyFileHook, LevelAlias.Off))
227228
{
228229
// Hook will clear the contents of the file before emitting the log events
229230
sink.Emit(Some.LogEvent());
@@ -235,6 +236,83 @@ public static void OnOpenedLifecycleHookCanEmptyTheFileContents()
235236
Assert.Equal('{', lines[0][0]);
236237
}
237238

239+
[Fact]
240+
public void WhenFlushAtMinimumLevelIsNotReachedLineIsNotFlushed()
241+
{
242+
using var tmp = TempFolder.ForCaller();
243+
var path = tmp.AllocateFilename("txt");
244+
var formatter = new JsonFormatter();
245+
246+
using (var sink = new FileSink(path, formatter, null, null, true, null, LogEventLevel.Fatal))
247+
{
248+
sink.Emit(Some.LogEvent(level: LogEventLevel.Information));
249+
250+
var lines = ReadAllLinesShared(path);
251+
Assert.Empty(lines);
252+
}
253+
254+
var savedLines = System.IO.File.ReadAllLines(path);
255+
Assert.Single(savedLines);
256+
}
257+
258+
[Fact]
259+
public void WhenFlushAtMinimumLevelIsReachedLineIsFlushed()
260+
{
261+
using var tmp = TempFolder.ForCaller();
262+
var path = tmp.AllocateFilename("txt");
263+
var formatter = new JsonFormatter();
264+
265+
using (var sink = new FileSink(path, formatter, null, null, true, null, LogEventLevel.Fatal))
266+
{
267+
sink.Emit(Some.LogEvent(level: LogEventLevel.Fatal));
268+
269+
var lines = ReadAllLinesShared(path);
270+
Assert.Single(lines);
271+
}
272+
273+
var savedLines = System.IO.File.ReadAllLines(path);
274+
Assert.Single(savedLines);
275+
}
276+
277+
[Fact]
278+
public void WhenFlushAtMinimumLevelIsOffLineIsNotFlushed()
279+
{
280+
using var tmp = TempFolder.ForCaller();
281+
var path = tmp.AllocateFilename("txt");
282+
var formatter = new JsonFormatter();
283+
284+
using (var sink = new FileSink(path, formatter, null, null, true, null, LevelAlias.Off))
285+
{
286+
sink.Emit(Some.LogEvent(level: LogEventLevel.Fatal));
287+
288+
var lines = ReadAllLinesShared(path);
289+
Assert.Empty(lines);
290+
}
291+
292+
var savedLines = System.IO.File.ReadAllLines(path);
293+
Assert.Single(savedLines);
294+
}
295+
296+
[Fact]
297+
public void WhenFlushAtMinimumLevelIsReachedMultipleLinesAreFlushed()
298+
{
299+
using var tmp = TempFolder.ForCaller();
300+
var path = tmp.AllocateFilename("txt");
301+
var formatter = new JsonFormatter();
302+
303+
using (var sink = new FileSink(path, formatter, null, null, true, null, LogEventLevel.Error))
304+
{
305+
sink.Emit(Some.LogEvent(level: LogEventLevel.Information));
306+
sink.Emit(Some.LogEvent(level: LogEventLevel.Fatal));
307+
308+
var lines = ReadAllLinesShared(path);
309+
Assert.Equal(2, lines.Length);
310+
}
311+
312+
var savedLines = System.IO.File.ReadAllLines(path);
313+
Assert.Equal(2, savedLines.Length);
314+
}
315+
238316
static void WriteTwoEventsAndCheckOutputFileLength(long? maxBytes, Encoding encoding)
239317
{
240318
using var tmp = TempFolder.ForCaller();
@@ -260,4 +338,21 @@ static void WriteTwoEventsAndCheckOutputFileLength(long? maxBytes, Encoding enco
260338
size = new FileInfo(path).Length;
261339
Assert.Equal(encoding.GetPreamble().Length + eventOuputLength * 2, size);
262340
}
341+
342+
private static string[] ReadAllLinesShared(string path)
343+
{
344+
// ReadAllLines cannot be used here, as it can't read files even if they are opened with FileShare.Read
345+
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
346+
using var reader = new StreamReader(fs);
347+
348+
string? line;
349+
List<string> lines = [];
350+
351+
while ((line = reader.ReadLine()) != null)
352+
{
353+
lines.Add(line);
354+
}
355+
356+
return [.. lines];
357+
}
263358
}

0 commit comments

Comments
 (0)